Commit b1eccdf2 by Andy Armstrong Committed by cahrens

Replace unit page with the container page.

STUD-1754
parent 3796f8c6
...@@ -397,27 +397,3 @@ def create_other_user(_step, name, has_extra_perms, role_name): ...@@ -397,27 +397,3 @@ def create_other_user(_step, name, has_extra_perms, role_name):
def log_out(_step): def log_out(_step):
world.visit('logout') 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 @@ ...@@ -6,7 +6,7 @@
# pylint: disable=W0613 # pylint: disable=W0613
from lettuce import world, step 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" DISPLAY_NAME = "Display Name"
...@@ -48,7 +48,7 @@ def add_a_multi_step_component(step, is_advanced, category): ...@@ -48,7 +48,7 @@ def add_a_multi_step_component(step, is_advanced, category):
def see_a_multi_step_component(step, category): def see_a_multi_step_component(step, category):
# Wait for all components to finish rendering # 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)) world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes))
for idx, step_hash in enumerate(step.hashes): for idx, step_hash in enumerate(step.hashes):
...@@ -79,7 +79,7 @@ def see_a_problem_component(step, category): ...@@ -79,7 +79,7 @@ def see_a_problem_component(step, category):
assert_true(world.is_css_present(component_css), assert_true(world.is_css_present(component_css),
'No problem was added to the unit.') '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) actual_text = world.css_text(problem_css)
assert_in(category.upper(), actual_text) assert_in(category.upper(), actual_text)
...@@ -93,7 +93,7 @@ def add_component_category(step, component, category): ...@@ -93,7 +93,7 @@ def add_component_category(step, component, category):
@step(u'I delete all components$') @step(u'I delete all components$')
def delete_all_components(step): 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') step.given('I delete "' + str(count) + '" component')
...@@ -124,7 +124,7 @@ def delete_components(step, number): ...@@ -124,7 +124,7 @@ def delete_components(step, number):
@step(u'I see no components') @step(u'I see no components')
def see_no_components(steps): 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') @step(u'I delete a component')
...@@ -162,8 +162,9 @@ def see_component_in_position(step, display_name, index): ...@@ -162,8 +162,9 @@ def see_component_in_position(step, display_name, index):
@step(u'I see the display name is "([^"]*)"') @step(u'I see the display name is "([^"]*)"')
def check_component_display_name(step, display_name): def check_component_display_name(step, display_name):
label = world.css_text(".component-header") # The display name for the unit uses the same structure, must differentiate by level-element.
assert display_name == label 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 "([^"]*)"') @step(u'I change the display name to "([^"]*)"')
......
...@@ -122,9 +122,9 @@ def ensure_settings_visible(): ...@@ -122,9 +122,9 @@ def ensure_settings_visible():
@world.absorb @world.absorb
def edit_component(): def edit_component(index=0):
world.wait_for(lambda _driver: world.css_visible('a.edit-button')) 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() world.wait_for_ajax_complete()
......
...@@ -55,7 +55,7 @@ def i_click_on_error_dialog(step): ...@@ -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 # 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 page in the course (there should only be one).
vertical_usage_key = course_key.make_usage_key("vertical", None) 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 # 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. # check that we visited a vertical URL.
if vertical_url.endswith("/None"): if vertical_url.endswith("/None"):
......
...@@ -81,38 +81,6 @@ Feature: CMS.Problem Editor ...@@ -81,38 +81,6 @@ Feature: CMS.Problem Editor
When I edit and select Settings When I edit and select Settings
Then Edit High Level Source is visible 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 Scenario: Cheat sheet visible on toggle
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
And I can edit the problem And I can edit the problem
......
...@@ -305,15 +305,13 @@ def i_can_edit_problem(_step): ...@@ -305,15 +305,13 @@ def i_can_edit_problem(_step):
@step(u'I edit first blank advanced problem for annotation response$') @step(u'I edit first blank advanced problem for annotation response$')
def i_edit_blank_problem_for_annotation_response(_step): 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 = """ text = """
<problem> <problem>
<annotationresponse> <annotationresponse>
<annotationinput><text>Text of annotation</text></annotationinput> <annotationinput><text>Text of annotation</text></annotationinput>
</annotationresponse> </annotationresponse>
</problem>""" </problem>"""
world.browser.execute_script(edit_css)
world.wait_for_ajax_complete()
type_in_codemirror(0, text) type_in_codemirror(0, text)
world.save_component() world.save_component()
......
...@@ -95,7 +95,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -95,7 +95,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
# just pick one vertical # just pick one vertical
descriptor = store.get_items(course.id, category='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) self.assertEqual(resp.status_code, 200)
for expected in expected_types: for expected in expected_types:
...@@ -120,7 +120,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -120,7 +120,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
# just pick one vertical # just pick one vertical
usage_key = course_items[0].id.make_usage_key('vertical', None) 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) self.assertEqual(resp.status_code, 400)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
...@@ -926,7 +926,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -926,7 +926,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
# Assert is here to make sure that the course being tested actually has verticals (units) to check. # Assert is here to make sure that the course being tested actually has verticals (units) to check.
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
for descriptor in items: 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) self.assertEqual(resp.status_code, 200)
...@@ -1293,7 +1293,7 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1293,7 +1293,7 @@ class ContentStoreTest(ContentStoreTestCase):
# go look at the Edit page # go look at the Edit page
unit_key = course_key.make_usage_key('vertical', 'test_vertical') 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) self.assertEqual(resp.status_code, 200)
def delete_item(category, name): def delete_item(category, name):
......
...@@ -11,7 +11,6 @@ from django.conf import settings ...@@ -11,7 +11,6 @@ from django.conf import settings
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from edxmako.shortcuts import render_to_response 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.django import modulestore
from xmodule.modulestore import PublishState from xmodule.modulestore import PublishState
...@@ -23,7 +22,7 @@ from xblock.plugin import PluginMissingError ...@@ -23,7 +22,7 @@ from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist from xblock.runtime import Mixologist
from contentstore.utils import get_lms_link_for_item, compute_publish_state 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 models.settings.course_grading import CourseGradingModel
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
...@@ -34,14 +33,13 @@ from django.utils.translation import ugettext as _ ...@@ -34,14 +33,13 @@ from django.utils.translation import ugettext as _
__all__ = ['OPEN_ENDED_COMPONENT_TYPES', __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY', 'ADVANCED_COMPONENT_POLICY_KEY',
'subsection_handler', 'subsection_handler',
'unit_handler',
'container_handler', 'container_handler',
'component_handler' 'component_handler'
] ]
log = logging.getLogger(__name__) 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'] COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video']
# Constants for determining if these components should be enabled for this course # Constants for determining if these components should be enabled for this course
...@@ -135,84 +133,6 @@ def _load_mixed_class(category): ...@@ -135,84 +133,6 @@ def _load_mixed_class(category):
return mixologist.mix(component_class) 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 # pylint: disable=unused-argument
@require_GET @require_GET
@login_required @login_required
...@@ -235,25 +155,37 @@ def container_handler(request, usage_key_string): ...@@ -235,25 +155,37 @@ def container_handler(request, usage_key_string):
component_templates = get_component_templates(course) component_templates = get_component_templates(course)
ancestor_xblocks = [] ancestor_xblocks = []
parent = get_parent_xblock(xblock) 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) ancestor_xblocks.append(parent)
parent = get_parent_xblock(parent) parent = get_parent_xblock(parent)
ancestor_xblocks.reverse() ancestor_xblocks.reverse()
unit = ancestor_xblocks[0] if ancestor_xblocks else None subsection = get_parent_xblock(unit) if unit else None
unit_publish_state = compute_publish_state(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', { return render_to_response('container.html', {
'context_course': course, # Needed only for display of menus at top of page. 'context_course': course, # Needed only for display of menus at top of page.
'xblock': xblock, 'xblock': xblock,
'unit_publish_state': unit_publish_state, 'unit_publish_state': unit_publish_state,
'xblock_locator': xblock.location, '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, 'ancestor_xblocks': ancestor_xblocks,
'component_templates': json.dumps(component_templates), 'component_templates': json.dumps(component_templates),
}) })
else: else:
return HttpResponseBadRequest("Only supports html requests") return HttpResponseBadRequest("Only supports HTML requests")
def get_component_templates(course): def get_component_templates(course):
...@@ -285,16 +217,6 @@ def get_component_templates(course): ...@@ -285,16 +217,6 @@ def get_component_templates(course):
'video': _("Video") '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 = [] component_templates = []
categories = set() categories = set()
# The component_templates array is in the order of "advanced" (if present), followed # The component_templates array is in the order of "advanced" (if present), followed
...@@ -305,7 +227,7 @@ def get_component_templates(course): ...@@ -305,7 +227,7 @@ def get_component_templates(course):
# add the default template with localized display name # add the default template with localized display name
# TODO: Once mixins are defined per-application, rather than per-runtime, # TODO: Once mixins are defined per-application, rather than per-runtime,
# this should use a cms mixed-in class. (cpennington) # 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)) templates_for_category.append(create_template_dict(display_name, category))
categories.add(category) categories.add(category)
...@@ -328,7 +250,7 @@ def get_component_templates(course): ...@@ -328,7 +250,7 @@ def get_component_templates(course):
for advanced_problem_type in ADVANCED_PROBLEM_TYPES: for advanced_problem_type in ADVANCED_PROBLEM_TYPES:
component = advanced_problem_type['component'] component = advanced_problem_type['component']
boilerplate_name = advanced_problem_type['boilerplate_name'] 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)) templates_for_category.append(create_template_dict(component_display_name, component, boilerplate_name))
categories.add(component) categories.add(component)
...@@ -350,7 +272,7 @@ def get_component_templates(course): ...@@ -350,7 +272,7 @@ def get_component_templates(course):
if category in ADVANCED_COMPONENT_TYPES and not category in categories: if category in ADVANCED_COMPONENT_TYPES and not category in categories:
# boilerplates not supported for advanced components # boilerplates not supported for advanced components
try: 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( advanced_component_templates['templates'].append(
create_template_dict( create_template_dict(
component_display_name, component_display_name,
......
from __future__ import absolute_import
import logging import logging
from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_string, render_to_response from edxmako.shortcuts import render_to_string, render_to_response
from xblock.core import XBlock
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.utils import reverse_course_url, reverse_usage_url
...@@ -11,7 +16,7 @@ __all__ = ['edge', 'event', 'landing'] ...@@ -11,7 +16,7 @@ __all__ = ['edge', 'event', 'landing']
EDITING_TEMPLATES = [ EDITING_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal", "basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "add-xblock-component", "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 # points to the temporary course landing page with log in and sign up
...@@ -70,8 +75,8 @@ def xblock_has_own_studio_page(xblock): ...@@ -70,8 +75,8 @@ def xblock_has_own_studio_page(xblock):
are a few exceptions: are a few exceptions:
1. Courses 1. Courses
2. Verticals that are either: 2. Verticals that are either:
- themselves treated as units (in which case they are shown on a unit page) - themselves treated as units
- a direct child of a unit (in which case they are shown on a container page) - a direct child of a unit
3. XBlocks with children, except for: 3. XBlocks with children, except for:
- sequentials (aka subsections) - sequentials (aka subsections)
- chapters (aka sections) - chapters (aka sections)
...@@ -83,7 +88,7 @@ def xblock_has_own_studio_page(xblock): ...@@ -83,7 +88,7 @@ def xblock_has_own_studio_page(xblock):
elif category == 'vertical': elif category == 'vertical':
parent_xblock = get_parent_xblock(xblock) parent_xblock = get_parent_xblock(xblock)
return is_unit(parent_xblock) if parent_xblock else False return is_unit(parent_xblock) if parent_xblock else False
elif category in ('sequential', 'chapter'): elif category == 'sequential':
return False return False
# All other xblocks with children have their own page # All other xblocks with children have their own page
...@@ -97,12 +102,30 @@ def xblock_studio_url(xblock): ...@@ -97,12 +102,30 @@ def xblock_studio_url(xblock):
if not xblock_has_own_studio_page(xblock): if not xblock_has_own_studio_page(xblock):
return None return None
category = xblock.category category = xblock.category
parent_xblock = get_parent_xblock(xblock) if category in ('course', 'chapter'):
parent_category = parent_xblock.category if parent_xblock else None
if category == 'course':
return reverse_course_url('course_handler', xblock.location.course_key) 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: else:
return reverse_usage_url('container_handler', xblock.location) 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): ...@@ -348,7 +348,7 @@ def export_handler(request, course_key_string):
'raw_err_msg': str(exc), 'raw_err_msg': str(exc),
'failed_module': failed_item, 'failed_module': failed_item,
'unit': unit, '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), 'course_home_url': reverse_course_url("course_handler", course_key),
'export_url': export_url 'export_url': export_url
}) })
......
...@@ -21,18 +21,16 @@ from xblock.fragment import Fragment ...@@ -21,18 +21,16 @@ from xblock.fragment import Fragment
import xmodule import xmodule
from xmodule.tabs import StaticTab, CourseTabList 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.django import modulestore
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
from xmodule.modulestore.inheritance import own_metadata 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 util.json_request import expect_json, JsonResponse
from .access import has_course_access from .access import has_course_access
from .helpers import xblock_has_own_studio_page from contentstore.views.helpers import is_unit
from contentstore.utils import compute_publish_state
from contentstore.views.preview import get_preview_fragment from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
...@@ -187,7 +185,6 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -187,7 +185,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
xblock = store.get_item(usage_key) xblock = store.get_item(usage_key)
is_read_only = _is_xblock_read_only(xblock) is_read_only = _is_xblock_read_only(xblock)
container_views = ['container_preview', 'reorderable_container_child_preview'] 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 # wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly # can bind to it correctly
...@@ -204,8 +201,8 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -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)})) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
store.update_item(xblock, request.user.id) store.update_item(xblock, request.user.id)
elif view_name in (unit_views + container_views): elif view_name in (PREVIEW_VIEWS + container_views):
is_container_view = (view_name in 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 # Determine the items to be shown as reorderable. Note that the view
# 'reorderable_container_child_preview' is only rendered for xblocks that # 'reorderable_container_child_preview' is only rendered for xblocks that
...@@ -215,27 +212,21 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -215,27 +212,21 @@ def xblock_view_handler(request, usage_key_string, view_name):
if view_name == 'reorderable_container_child_preview': if view_name == 'reorderable_container_child_preview':
reorderable_items.add(xblock.location) reorderable_items.add(xblock.location)
# Only show the new style HTML for the container view, i.e. for non-verticals # Set up the context to be passed to each XBlock's render method.
# Note: this special case logic can be removed once the unit page is replaced
# with the new container view.
context = { 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, 'read_only': is_read_only,
'root_xblock': xblock if (view_name == 'container_preview') else None, 'root_xblock': xblock if (view_name == 'container_preview') else None,
'reorderable_items': reorderable_items 'reorderable_items': reorderable_items
} }
fragment = get_preview_fragment(request, xblock, context) fragment = get_preview_fragment(request, xblock, context)
# For old-style pages (such as unit and static pages), wrap the preview with
# the component div. Note that the container view recursively adds headers # Note that the container view recursively adds headers into the preview fragment,
# into the preview fragment, so we don't want to add another header here. # so only the "Pages" view requires that this extra wrapper be included.
if not is_container_view: if is_pages_view:
# For non-leaf xblocks, show the special rendering which links to the new container page. fragment.content = render_to_string('component.html', {
if xblock_has_own_studio_page(xblock):
template = 'container_xblock_component.html'
else:
template = 'component.html'
fragment.content = render_to_string(template, {
'xblock_context': context, 'xblock_context': context,
'xblock': xblock, 'xblock': xblock,
'locator': usage_key, 'locator': usage_key,
...@@ -263,10 +254,12 @@ def _is_xblock_read_only(xblock): ...@@ -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. 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). # We allow direct editing of xblocks in DIRECT_ONLY_CATEGORIES (for example, static pages).
if xblock.category in DIRECT_ONLY_CATEGORIES: # if xblock.category in DIRECT_ONLY_CATEGORIES:
return False # return False
component_publish_state = compute_publish_state(xblock) # component_publish_state = compute_publish_state(xblock)
return component_publish_state == PublishState.public # 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, 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): ...@@ -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. 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. # Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
if context.get('container_view', None) and view in PREVIEW_VIEWS: if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS:
root_xblock = context.get('root_xblock') root_xblock = context.get('root_xblock')
is_root = root_xblock and xblock.location == root_xblock.location is_root = root_xblock and xblock.location == root_xblock.location
is_reorderable = _is_xblock_reorderable(xblock, context) is_reorderable = _is_xblock_reorderable(xblock, context)
......
...@@ -3,9 +3,7 @@ Unit tests for the container page. ...@@ -3,9 +3,7 @@ Unit tests for the container page.
""" """
import re import re
from contentstore.utils import compute_publish_state
from contentstore.views.tests.utils import StudioPageTestCase from contentstore.views.tests.utils import StudioPageTestCase
from xmodule.modulestore import PublishState
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import ItemFactory from xmodule.modulestore.tests.factories import ItemFactory
...@@ -35,10 +33,13 @@ class ContainerPageTestCase(StudioPageTestCase): ...@@ -35,10 +33,13 @@ class ContainerPageTestCase(StudioPageTestCase):
'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location) 'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location)
), ),
expected_breadcrumbs=( expected_breadcrumbs=(
r'<a href="/unit/{}"\s*' r'<a href="/course/{course}" class="navigation-item navigation-link navigation-parent">\s*Week 1\s*</a>\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*' r'<span class="navigation-item navigation-parent">\s*Lesson 1\s*</span>\s*'
r'<a href="#" class="navigation-link navigation-current">Split Test</a>' r'<a href="/container/{unit}" class="navigation-item navigation-link navigation-parent">\s*Unit\s*</a>'
).format(re.escape(unicode(self.vertical.location))) ).format(
course=re.escape(unicode(self.course.id)),
unit=re.escape(unicode(self.vertical.location)),
),
) )
def test_container_on_container_html(self): def test_container_on_container_html(self):
...@@ -57,15 +58,15 @@ class ContainerPageTestCase(StudioPageTestCase): ...@@ -57,15 +58,15 @@ class ContainerPageTestCase(StudioPageTestCase):
'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_container.location) 'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_container.location)
), ),
expected_breadcrumbs=( expected_breadcrumbs=(
r'<a href="/unit/{unit}"\s*' r'<a href="/course/{course}" class="navigation-item navigation-link navigation-parent">\s*Week 1\s*</a>\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*' r'<span class="navigation-item navigation-parent">\s*Lesson 1\s*</span>\s*'
r'<a href="/container/{split_test}"\s*' r'<a href="/container/{unit}" class="navigation-item navigation-link navigation-parent">\s*Unit\s*</a>\s*'
r'class="navigation-link navigation-parent">Split Test</a>\s*' r'<a href="/container/{split_test}" class="navigation-item navigation-link navigation-parent">\s*Split Test\s*</a>'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'
).format( ).format(
course=re.escape(unicode(self.course.id)),
unit=re.escape(unicode(self.vertical.location)), unit=re.escape(unicode(self.vertical.location)),
split_test=re.escape(unicode(self.child_container.location)) split_test=re.escape(unicode(self.child_container.location))
) ),
) )
# Test the draft version of the container # Test the draft version of the container
...@@ -82,19 +83,9 @@ class ContainerPageTestCase(StudioPageTestCase): ...@@ -82,19 +83,9 @@ class ContainerPageTestCase(StudioPageTestCase):
and the breadcrumbs trail is correct. and the breadcrumbs trail is correct.
""" """
html = self.get_page_html(xblock) html = self.get_page_html(xblock)
publish_state = compute_publish_state(xblock)
self.assertIn(expected_section_tag, html) self.assertIn(expected_section_tag, html)
# Verify the navigation link at the top of the page is correct.
self.assertRegexpMatches(html, expected_breadcrumbs) 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): def test_public_container_preview_html(self):
""" """
Verify that a public xblock's container preview returns the expected HTML. Verify that a public xblock's container preview returns the expected HTML.
...@@ -102,23 +93,17 @@ class ContainerPageTestCase(StudioPageTestCase): ...@@ -102,23 +93,17 @@ class ContainerPageTestCase(StudioPageTestCase):
published_unit = self.store.publish(self.vertical.location, self.user.id) published_unit = self.store.publish(self.vertical.location, self.user.id)
published_child_container = self.store.get_item(self.child_container.location) published_child_container = self.store.get_item(self.child_container.location)
published_child_vertical = self.store.get_item(self.child_vertical.location) published_child_vertical = self.store.get_item(self.child_vertical.location)
self.validate_preview_html(published_unit, self.container_view, 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)
self.validate_preview_html(published_child_container, self.container_view, 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_child_vertical, self.reorderable_child_view,
can_edit=False, can_reorder=False, can_add=False)
def test_draft_container_preview_html(self): def test_draft_container_preview_html(self):
""" """
Verify that a draft xblock's container preview returns the expected HTML. Verify that a draft xblock's container preview returns the expected HTML.
""" """
self.validate_preview_html(self.vertical, self.container_view, 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)
self.validate_preview_html(self.child_container, self.container_view, 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.child_vertical, self.reorderable_child_view,
can_edit=True, can_reorder=True, can_add=True)
def test_public_child_container_preview_html(self): def test_public_child_container_preview_html(self):
""" """
...@@ -126,25 +111,11 @@ class ContainerPageTestCase(StudioPageTestCase): ...@@ -126,25 +111,11 @@ class ContainerPageTestCase(StudioPageTestCase):
""" """
empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test') 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) 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, self.validate_preview_html(published_empty_child_container, self.reorderable_child_view, can_add=False)
can_reorder=False, can_edit=False, can_add=False)
def test_draft_child_container_preview_html(self): 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. 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') empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test')
self.validate_preview_html(empty_child_container, self.reorderable_child_view, self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False)
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
)
...@@ -3,7 +3,7 @@ Unit tests for helpers.py. ...@@ -3,7 +3,7 @@ Unit tests for helpers.py.
""" """
from contentstore.tests.utils import CourseTestCase 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 from xmodule.modulestore.tests.factories import ItemFactory
...@@ -11,6 +11,7 @@ class HelpersTestCase(CourseTestCase): ...@@ -11,6 +11,7 @@ class HelpersTestCase(CourseTestCase):
""" """
Unit tests for helpers.py. Unit tests for helpers.py.
""" """
def test_xblock_studio_url(self): def test_xblock_studio_url(self):
# Verify course URL # Verify course URL
...@@ -20,18 +21,19 @@ class HelpersTestCase(CourseTestCase): ...@@ -20,18 +21,19 @@ class HelpersTestCase(CourseTestCase):
# Verify chapter URL # Verify chapter URL
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
display_name="Week 1") 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 # Verify lesson URL
sequential = ItemFactory.create(parent_location=chapter.location, category='sequential', sequential = ItemFactory.create(parent_location=chapter.location, category='sequential',
display_name="Lesson 1") display_name="Lesson 1")
self.assertIsNone(xblock_studio_url(sequential)) self.assertIsNone(xblock_studio_url(sequential))
# Verify vertical URL # Verify unit URL
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical', vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
display_name='Unit') display_name='Unit')
self.assertEqual(xblock_studio_url(vertical), self.assertEqual(xblock_studio_url(vertical),
u'/unit/i4x://MITx/999/vertical/Unit') u'/container/i4x://MITx/999/vertical/Unit')
# Verify child vertical URL # Verify child vertical URL
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical', child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
...@@ -43,3 +45,23 @@ class HelpersTestCase(CourseTestCase): ...@@ -43,3 +45,23 @@ class HelpersTestCase(CourseTestCase):
video = ItemFactory.create(parent_location=child_vertical.location, category="video", video = ItemFactory.create(parent_location=child_vertical.location, category="video",
display_name="My Video") display_name="My Video")
self.assertIsNone(xblock_studio_url(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): ...@@ -293,7 +293,7 @@ class ExportTestCase(CourseTestCase):
""" """
fake_xblock = ItemFactory.create(parent_location=self.course.location, category='aawefawef') fake_xblock = ItemFactory.create(parent_location=self.course.location, category='aawefawef')
self.store.publish(fake_xblock.location, self.user.id) 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): def test_export_failure_subsection_level(self):
""" """
...@@ -305,7 +305,7 @@ class ExportTestCase(CourseTestCase): ...@@ -305,7 +305,7 @@ class ExportTestCase(CourseTestCase):
category='aawefawef' 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): def _verify_export_failure(self, expectedText):
""" Export failure helper method. """ """ Export failure helper method. """
......
...@@ -38,7 +38,11 @@ class GetPreviewHtmlTestCase(TestCase): ...@@ -38,7 +38,11 @@ class GetPreviewHtmlTestCase(TestCase):
request.session = {} request.session = {}
# Call get_preview_fragment directly. # 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. # Verify student view html is returned, and the usage ID is as expected.
self.assertRegexpMatches( self.assertRegexpMatches(
......
...@@ -21,35 +21,18 @@ class UnitPageTestCase(StudioPageTestCase): ...@@ -21,35 +21,18 @@ class UnitPageTestCase(StudioPageTestCase):
category="video", display_name="My Video") category="video", display_name="My Video")
self.store = modulestore() 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): def test_public_component_preview_html(self):
""" """
Verify that a public xblock's preview returns the expected HTML. Verify that a public xblock's preview returns the expected HTML.
""" """
published_video = self.store.publish(self.video.location, self.user.id) published_video = self.store.publish(self.video.location, self.user.id)
self.validate_preview_html(self.video, STUDENT_VIEW, self.validate_preview_html(self.video, STUDENT_VIEW, can_add=False)
can_edit=True, can_reorder=True, can_add=False)
def test_draft_component_preview_html(self): def test_draft_component_preview_html(self):
""" """
Verify that a draft xblock's preview returns the expected HTML. Verify that a draft xblock's preview returns the expected HTML.
""" """
self.validate_preview_html(self.video, STUDENT_VIEW, self.validate_preview_html(self.video, STUDENT_VIEW, can_add=False)
can_edit=True, can_reorder=True, can_add=False)
def test_public_child_container_preview_html(self): def test_public_child_container_preview_html(self):
""" """
...@@ -61,8 +44,7 @@ class UnitPageTestCase(StudioPageTestCase): ...@@ -61,8 +44,7 @@ class UnitPageTestCase(StudioPageTestCase):
ItemFactory.create(parent_location=child_container.location, ItemFactory.create(parent_location=child_container.location,
category='html', display_name='grandchild') category='html', display_name='grandchild')
published_child_container = self.store.publish(child_container.location, self.user.id) published_child_container = self.store.publish(child_container.location, self.user.id)
self.validate_preview_html(published_child_container, STUDENT_VIEW, self.validate_preview_html(published_child_container, STUDENT_VIEW, can_add=False)
can_reorder=True, can_edit=True, can_add=False)
def test_draft_child_container_preview_html(self): def test_draft_child_container_preview_html(self):
""" """
...@@ -74,5 +56,4 @@ class UnitPageTestCase(StudioPageTestCase): ...@@ -74,5 +56,4 @@ class UnitPageTestCase(StudioPageTestCase):
ItemFactory.create(parent_location=child_container.location, ItemFactory.create(parent_location=child_container.location,
category='html', display_name='grandchild') category='html', display_name='grandchild')
draft_child_container = self.store.get_item(child_container.location) draft_child_container = self.store.get_item(child_container.location)
self.validate_preview_html(draft_child_container, STUDENT_VIEW, self.validate_preview_html(draft_child_container, STUDENT_VIEW, can_add=False)
can_reorder=True, can_edit=True, can_add=False)
...@@ -41,19 +41,16 @@ class StudioPageTestCase(CourseTestCase): ...@@ -41,19 +41,16 @@ class StudioPageTestCase(CourseTestCase):
resp_content = json.loads(resp.content) resp_content = json.loads(resp.content)
return resp_content['html'] 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. Verify that the specified xblock's preview has the expected HTML elements.
""" """
html = self.get_preview_html(xblock, view_name) 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>' drag_handle_html = '<span data-tooltip="Drag to reorder" class="drag-handle action"></span>'
if can_reorder: self.assertIn(drag_handle_html, html)
self.assertIn(drag_handle_html, html)
else:
self.assertNotIn(drag_handle_html, html)
# Verify that there are no action buttons for public blocks # Verify that there are no action buttons for public blocks
expected_button_html = [ expected_button_html = [
...@@ -62,10 +59,7 @@ class StudioPageTestCase(CourseTestCase): ...@@ -62,10 +59,7 @@ class StudioPageTestCase(CourseTestCase):
'<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">' '<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">'
] ]
for button_html in expected_button_html: for button_html in expected_button_html:
if can_edit: self.assertIn(button_html, html)
self.assertIn(button_html, html)
else:
self.assertNotIn(button_html, html)
def validate_html_for_add_buttons(self, html, can_add=True): def validate_html_for_add_buttons(self, html, can_add=True):
""" """
......
...@@ -225,7 +225,6 @@ define([ ...@@ -225,7 +225,6 @@ define([
"js/spec/views/group_configuration_spec", "js/spec/views/group_configuration_spec",
"js/spec/views/container_spec", "js/spec/views/container_spec",
"js/spec/views/unit_spec",
"js/spec/views/xblock_spec", "js/spec/views/xblock_spec",
"js/spec/views/xblock_editor_spec", "js/spec/views/xblock_editor_spec",
......
...@@ -241,7 +241,7 @@ function createNewUnit(e) { ...@@ -241,7 +241,7 @@ function createNewUnit(e) {
function(data) { function(data) {
// redirect to the edit page // redirect to the edit page
window.location = "/unit/" + data['locator']; window.location = "/container/" + data['locator'];
}); });
} }
......
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", "js/views/container", "js/models/xblock_info", "jquery.simulate",
"xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], "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 () { describe("Container View", function () {
...@@ -34,9 +34,10 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers ...@@ -34,9 +34,10 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
}; };
beforeEach(function () { 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>'); 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({ model = new XBlockInfo({
id: rootLocator, id: rootLocator,
display_name: 'Test AB Test', display_name: 'Test AB Test',
...@@ -51,6 +52,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers ...@@ -51,6 +52,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
}); });
afterEach(function () { afterEach(function () {
edit_helpers.uninstallMockXBlock();
containerView.remove(); containerView.remove();
}); });
...@@ -186,11 +188,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers ...@@ -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. // Drag the first component in Group B to the first group.
dragComponentAbove(groupBComponent1, groupAComponent1); dragComponentAbove(groupBComponent1, groupAComponent1);
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
respondToRequest(requests, 0, 200); respondToRequest(requests, 0, 200);
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
respondToRequest(requests, 1, 200); respondToRequest(requests, 1, 200);
view_helpers.verifyNotificationHidden(notificationSpy); edit_helpers.verifyNotificationHidden(notificationSpy);
}); });
it('does not hide saving message if failure', function () { 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 ...@@ -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. // Drag the first component in Group B to the first group.
dragComponentAbove(groupBComponent1, groupAComponent1); dragComponentAbove(groupBComponent1, groupAComponent1);
view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving');
respondToRequest(requests, 0, 500); 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. // Since the first reorder call failed, the removal will not be called.
verifyNumReorderCalls(requests, 1); verifyNumReorderCalls(requests, 1);
......
define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", 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) { function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) {
describe("ContainerPage", function() { describe("ContainerPage", function() {
var lastRequest, renderContainerPage, expectComponents, respondWithHtml, var lastRequest, renderContainerPage, expectComponents, respondWithHtml,
model, containerPage, requests, model, containerPage, requests, initialDisplayName,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'), mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'), mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'), mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
beforeEach(function () { beforeEach(function () {
var newDisplayName = 'New Display Name';
edit_helpers.installEditTemplates(); edit_helpers.installEditTemplates();
edit_helpers.installTemplate('xblock-string-field-editor');
appendSetFixtures(mockContainerPage); appendSetFixtures(mockContainerPage);
edit_helpers.installMockXBlock({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
initialDisplayName = 'Test Container';
model = new XBlockInfo({ model = new XBlockInfo({
id: 'locator-container', id: 'locator-container',
display_name: 'Test Container', display_name: initialDisplayName,
category: 'vertical' category: 'vertical'
}); });
containerPage = new ContainerPage({ containerPage = new ContainerPage({
...@@ -26,6 +38,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -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]; }; lastRequest = function() { return requests[requests.length - 1]; };
respondWithHtml = function(html) { respondWithHtml = function(html) {
...@@ -55,9 +71,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -55,9 +71,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe("Initial display", function() { describe("Initial display", function() {
it('can render itself', function() { it('can render itself', function() {
renderContainerPage(mockContainerXBlockHtml, this); renderContainerPage(mockContainerXBlockHtml, this);
expect(containerPage.$el.select('.xblock-header')).toBeTruthy(); expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock')).not.toHaveClass('is-hidden'); expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.no-container-content')).toHaveClass('is-hidden');
}); });
it('shows a loading indicator', function() { it('shows a loading indicator', function() {
...@@ -70,25 +85,27 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -70,25 +85,27 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
}); });
describe("Editing the container", function() { describe("Editing the container", function() {
var newDisplayName = 'New Display Name'; var updatedDisplayName = 'Updated Test Container',
inlineEditDisplayName, displayNameElement, displayNameInput;
beforeEach(function () { beforeEach(function() {
edit_helpers.installMockXBlock({ displayNameElement = containerPage.$('.page-header-title');
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
}); });
afterEach(function() { afterEach(function() {
edit_helpers.uninstallMockXBlock();
edit_helpers.cancelModalIfShowing(); 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() { it('can edit itself', function() {
var editButtons, var editButtons;
updatedTitle = 'Updated Test Container';
renderContainerPage(mockContainerXBlockHtml, this); renderContainerPage(mockContainerXBlockHtml, this);
// Click the root edit button // Click the root edit button
...@@ -118,26 +135,49 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -118,26 +135,49 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
resources: [] resources: []
}); });
// Expect the title and breadcrumb to be updated // Expect the title to have been updated
expect(containerPage.$('.page-header-title').text().trim()).toBe(updatedTitle); expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
expect(containerPage.$('.page-header .subtitle a').last().text().trim()).toBe(updatedTitle);
}); });
});
describe("Editing an xblock", function() { it('can inline edit the display name', function() {
var newDisplayName = 'New Display Name'; 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);
});
beforeEach(function () { it('does not change the title when a display name update fails', function() {
edit_helpers.installMockXBlock({ renderContainerPage(mockContainerXBlockHtml, this);
data: "<p>Some HTML</p>", inlineEditDisplayName(updatedDisplayName);
metadata: { displayNameInput.change();
display_name: newDisplayName 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() {
afterEach(function() { afterEach(function() {
edit_helpers.uninstallMockXBlock();
edit_helpers.cancelModalIfShowing(); edit_helpers.cancelModalIfShowing();
}); });
...@@ -190,6 +230,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -190,6 +230,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
}); });
modal = $('.edit-xblock-modal'); modal = $('.edit-xblock-modal');
expect(modal.length).toBe(1);
// Click on the settings tab // Click on the settings tab
modal.find('.settings-button').click(); modal.find('.settings-button').click();
// Change the display name's text // Change the display name's text
...@@ -426,7 +467,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ...@@ -426,7 +467,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
}); });
describe('createNewComponent ', function () { describe('createNewComponent ', function () {
var clickNewComponent, verifyComponents; var clickNewComponent;
clickNewComponent = function (index) { clickNewComponent = function (index) {
containerPage.$(".new-component .new-component-type a.single-template")[index].click(); containerPage.$(".new-component .new-component-type a.single-template")[index].click();
......
...@@ -85,7 +85,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper ...@@ -85,7 +85,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper
}); });
// Give the mock xblock a save method... // Give the mock xblock a save method...
editor.xblock.save = window.MockDescriptor.save; editor.xblock.save = window.MockDescriptor.save;
editor.save(); editor.model.save(editor.getXModuleData());
request = requests[requests.length - 1]; request = requests[requests.length - 1];
response = JSON.parse(request.requestBody); response = JSON.parse(request.requestBody);
expect(response.metadata.display_name).toBe(testDisplayName); expect(response.metadata.display_name).toBe(testDisplayName);
......
...@@ -83,8 +83,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers ...@@ -83,8 +83,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
// Add templates needed by the settings editor // Add templates needed by the settings editor
modal_helpers.installTemplate('metadata-editor'); modal_helpers.installTemplate('metadata-editor');
modal_helpers.installTemplate('metadata-number-entry'); modal_helpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry');
modal_helpers.installTemplate('metadata-string-entry'); modal_helpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry');
}; };
showEditModal = function(requests, xblockElement, model, mockHtml, options) { showEditModal = function(requests, xblockElement, model, mockHtml, options) {
......
...@@ -9,9 +9,11 @@ define(['jquery', 'js/views/feedback_notification', 'js/views/feedback_prompt'], ...@@ -9,9 +9,11 @@ define(['jquery', 'js/views/feedback_notification', 'js/views/feedback_prompt'],
verifyNotificationHidden, createPromptSpy, confirmPrompt, verifyPromptShowing, verifyNotificationHidden, createPromptSpy, confirmPrompt, verifyPromptShowing,
verifyPromptHidden; verifyPromptHidden;
installTemplate = function(templateName, isFirst) { installTemplate = function(templateName, isFirst, templateId) {
var template = readFixtures(templateName + '.underscore'), var template = readFixtures(templateName + '.underscore');
if (!templateId) {
templateId = templateName + '-tpl'; templateId = templateName + '-tpl';
}
if (isFirst) { if (isFirst) {
setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template)); setFixtures($('<script>', { id: templateId, type: 'text/template' }).text(template));
......
...@@ -147,10 +147,19 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", ...@@ -147,10 +147,19 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
}, },
save: function(event) { save: function(event) {
var self = this,
editorView = this.editorView,
xblockInfo = this.xblockInfo,
data = editorView.getXModuleData();
event.preventDefault(); event.preventDefault();
this.editorView.save({ if (data) {
success: _.bind(this.onSave, this) this.runOperationShowingMessage(gettext('Saving&hellip;'),
}); function() {
return xblockInfo.save(data);
}).done(function() {
self.onSave();
});
}
}, },
onSave: function() { onSave: function() {
...@@ -177,7 +186,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", ...@@ -177,7 +186,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
if (xblockWrapperElement.length > 0) { if (xblockWrapperElement.length > 0) {
xblockElement = xblockWrapperElement.find('.xblock'); xblockElement = xblockWrapperElement.find('.xblock');
displayName = xblockWrapperElement.find('.xblock-header .header-details .xblock-display-name').text().trim(); 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) { if (!displayName) {
displayName = this.xblockElement.find('.component-header').text().trim(); displayName = this.xblockElement.find('.component-header').text().trim();
} }
......
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
* This page allows the user to understand and manipulate the xblock and its children. * This page allows the user to understand and manipulate the xblock and its children.
*/ */
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/container", 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"], "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_string_field_editor"],
function ($, _, gettext, BaseView, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, XBlockInfo,
XBlockStringFieldEditor) {
var XBlockContainerPage = BaseView.extend({ var XBlockContainerPage = BaseView.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
...@@ -12,6 +14,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai ...@@ -12,6 +14,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
initialize: function() { initialize: function() {
BaseView.prototype.initialize.call(this); BaseView.prototype.initialize.call(this);
this.nameEditor = new XBlockStringFieldEditor({
el: this.$('.wrapper-xblock-field'),
model: this.model
});
this.nameEditor.render();
this.xblockView = new ContainerView({ this.xblockView = new ContainerView({
el: this.$('.wrapper-xblock'), el: this.$('.wrapper-xblock'),
model: this.model, model: this.model,
...@@ -36,12 +43,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai ...@@ -36,12 +43,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
// Render the xblock // Render the xblock
xblockView.render({ xblockView.render({
success: function(xblock) { success: function() {
xblockView.xblock.runtime.notify("page-shown", self); xblockView.xblock.runtime.notify("page-shown", self);
xblockView.$el.removeClass('is-hidden'); xblockView.$el.removeClass('is-hidden');
self.renderAddXBlockComponents(); self.renderAddXBlockComponents();
self.onXBlockRefresh(xblockView); self.onXBlockRefresh(xblockView);
self.refreshTitle(); self.refreshDisplayName();
loadingElement.addClass('is-hidden'); loadingElement.addClass('is-hidden');
self.delegateEvents(); self.delegateEvents();
} }
...@@ -56,10 +63,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai ...@@ -56,10 +63,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
return this.xblockView.model.urlRoot; return this.xblockView.model.urlRoot;
}, },
refreshTitle: function() { refreshDisplayName: function() {
var title = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim(); var displayName = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
this.$('.page-header-title').text(title); this.model.set('display_name', displayName);
this.$('.page-header .subtitle a').last().text(title);
}, },
onXBlockRefresh: function(xblockView) { onXBlockRefresh: function(xblockView) {
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
* XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between * XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between
* the available modes. * the available modes.
*/ */
define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/xblock", define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata", "js/collections/metadata",
"js/views/metadata", "js/collections/metadata", "jquery.inputnumber"], "jquery.inputnumber"],
function ($, _, gettext, NotificationView, XBlockView, MetadataView, MetadataCollection) { function ($, _, gettext, XBlockView, MetadataView, MetadataCollection) {
var XBlockEditorView = XBlockView.extend({ var XBlockEditorView = XBlockView.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
...@@ -88,26 +88,6 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js ...@@ -88,26 +88,6 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
return this.metadataEditor; return this.metadataEditor;
}, },
save: function(options) {
var xblockInfo = this.model,
data,
saving;
data = this.getXModuleData();
if (data) {
saving = new NotificationView.Mini({
title: gettext('Saving&hellip;')
});
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. * Returns the data saved for the xmodule. Note that this *does not* work for XBlocks.
*/ */
......
/**
* 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&hellip;'),
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 { ...@@ -331,11 +331,13 @@ p, ul, ol, dl {
bottom: -($baseline*1.5); bottom: -($baseline*1.5);
} }
.navigation-link { // breadcrumb navigation
.navigation-item {
@extend %cont-truncated; @extend %cont-truncated;
display: inline-block; display: inline-block;
vertical-align: bottom; // correct for extra padding in FF vertical-align: bottom; // correct for extra padding in FF
max-width: 250px; max-width: 250px;
color: $gray;
&.navigation-current { &.navigation-current {
@extend %ui-disabled; @extend %ui-disabled;
...@@ -348,7 +350,12 @@ p, ul, ol, dl { ...@@ -348,7 +350,12 @@ p, ul, ol, dl {
} }
} }
.navigation-link:before {
.navigation-link:hover {
color: $blue;
}
.navigation-item:before {
content: " / "; content: " / ";
margin: ($baseline/4); margin: ($baseline/4);
color: $gray; color: $gray;
...@@ -358,7 +365,7 @@ p, ul, ol, dl { ...@@ -358,7 +365,7 @@ p, ul, ol, dl {
} }
} }
.navigation .navigation-link:first-child:before { .navigation .navigation-item:first-child:before {
content: ""; content: "";
margin: 0; margin: 0;
} }
......
...@@ -25,15 +25,6 @@ ...@@ -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) // needed to override ui-window styling for dragging state (outline selectors get too specific)
.courseware-section.is-dragging { .courseware-section.is-dragging {
box-shadow: 0 1px 2px 0 $shadow-d1 !important; box-shadow: 0 1px 2px 0 $shadow-d1 !important;
...@@ -81,4 +72,4 @@ body b { ...@@ -81,4 +72,4 @@ body b {
.CodeMirror { .CodeMirror {
overflow: visible !important; overflow: visible !important;
} }
} }
\ No newline at end of file
...@@ -13,18 +13,19 @@ ...@@ -13,18 +13,19 @@
border-bottom: none; border-bottom: none;
padding-bottom: 0; padding-bottom: 0;
.page-header { .page-header-title {
@extend %t-title; @extend %t-title;
@include font-size(28); @include font-size(28);
@include line-height(32); @include line-height(32);
font-weight: 600;
}
.subtitle .navigation-link { .page-header-title-edit {
color: $gray; @extend %t-title4;
background: none repeat scroll 0 0 $white;
&:hover { border: 0;
color: $blue; box-shadow: 0 0 2px 2px $shadow inset;
} font-weight: 600;
}
} }
} }
...@@ -129,6 +130,158 @@ ...@@ -129,6 +130,158 @@
padding: ($baseline*.75) ($baseline*.75) ($baseline) ($baseline*.75); 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;
}
}
}
}
} }
} }
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%def name="online_help_token()">
<%
if is_unit_page:
return "unit"
else:
return "container"
%>
</%def>
<%! <%!
import json import json
from xmodule.modulestore import PublishState 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 _ 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> <%block name="bodyclass">is-signedin course container view-container</%block>
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
...@@ -53,28 +61,32 @@ main_xblock_info = { ...@@ -53,28 +61,32 @@ main_xblock_info = {
<%block name="content"> <%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"> <header class="mast has-actions has-navigation has-subtitle">
<h1 class="page-header"> <div class="page-header">
<small class="navigation navigation-parents subtitle"> <small class="navigation navigation-parents subtitle">
% for ancestor in ancestor_xblocks: % for ancestor in ancestor_xblocks:
<% <%
ancestor_url = xblock_studio_url(ancestor) ancestor_url = xblock_studio_url(ancestor)
%> %>
% if ancestor_url: % if ancestor_url:
<a href="${ancestor_url}" <a href="${ancestor_url}" class="navigation-item navigation-link navigation-parent">
class="navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a> ${ancestor.display_name_with_default | h}
</a>
% else:
<span class="navigation-item navigation-parent">${ancestor.display_name_with_default | h}</span>
% endif % endif
% endfor % endfor
<a href="#" class="navigation-link navigation-current">${xblock.display_name_with_default | h}</a>
</small> </small>
<span class="page-header-title">${xblock.display_name_with_default | h}</span> <div class="wrapper-xblock-field" data-field="display_name">
</h1> <h1 class="page-header-title is-editable xblock-field-value">${xblock.display_name_with_default | h}</h1>
</div>
</div>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3> <h3 class="sr">${_("Page Actions")}</h3>
<ul> <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"> <li class="action-item action-edit nav-item">
<a href="#" class="button edit-button action-button"> <a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i> <i class="icon-pencil"></i>
...@@ -99,42 +111,44 @@ main_xblock_info = { ...@@ -99,42 +111,44 @@ main_xblock_info = {
</div> </div>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
% if unit: % if not is_unit_page:
% if unit_publish_state == PublishState.public: <div class="bit">
<div class="bit-publishing published"> <h3 class="title-3">${_("What can I do on this page?")}</h3>
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Published")}</h3> <ul class="list-details">
<p class="copy"> <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>
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>
</div> </div>
% else: % endif
<div class="bit-publishing draft"> % if is_unit_page:
<h3 class="title pub-status"><span class="sr">${_("Publishing Status")} </span>${_("Draft")}</h3> <div class="unit-location">
<p class="copy"> <h4 class="header">${_("Unit Location")}</h4>
<% <div class="wrapper-unit-id content-bit">
unit_link=u'<a href="{unit_address}">{unit_display_name}</a>'.format( <h5 class="title">Unit Location ID</h5>
unit_address=xblock_studio_url(unit), <p class="unit-id">
unit_display_name=unit.display_name_with_default, <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>
${_('You can edit the content of this page, and your changes will be published with unit {unit_link}.').format(unit_link=unit_link)} </div>
</p> <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> </div>
% endif
% 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> </aside>
</section> </section>
</div> </div>
......
<%!
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 @@ ...@@ -2,13 +2,14 @@
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category=""> <div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
<header class="mast has-actions has-navigation"> <header class="mast has-actions has-navigation">
<h1 class="page-header"> <div class="page-header">
<small class="navigation navigation-parents subtitle"> <small class="navigation navigation-parents subtitle">
<a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-link navigation-parent">Unit 1</a> <a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-item navigation-link navigation-parent">Unit 1</a>
<a href="#" class="navigation-link navigation-current">Test Container</a>
</small> </small>
<span class="page-header-title">Test Container</span> <div class="wrapper-xblock-field is-editable" data-field="display_name">
</h1> <h1 class="page-header-title xblock-field-value">Test Container</h1>
</div>
</div>
<nav class="nav-actions"> <nav class="nav-actions">
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
...@@ -31,9 +32,6 @@ ...@@ -31,9 +32,6 @@
<article class="content-primary window"> <article class="content-primary window">
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container"> <section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container">
</section> </section>
<div class="no-container-content is-hidden">
<p>This page has no content yet.</p>
</div>
<div class="ui-loading is-hidden"> <div class="ui-loading is-hidden">
<p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p> <p><span class="spin"><i class="icon-refresh"></i></span> <span class="copy">Loading...</span></p>
</div> </div>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</div> </div>
</header> </header>
<article class="xblock-render"> <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"> data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
</header> </header>
<article class="xblock-render"> <article class="xblock-render">
<div class="xblock" data-block-type="vertical"> <div class="xblock">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element"
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
</header> </header>
<article class="xblock-render"> <article class="xblock-render">
<div class="xblock" data-block-type="vertical"> <div class="xblock">
<ol class="reorderable-container"> <ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1"> <li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element" <section class="wrapper-xblock level-element"
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
</div> </div>
</header> </header>
<article class="xblock-render"> <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 class="reorderable-container">
</ol> </ol>
</div> </div>
......
<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>
<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 @@ ...@@ -10,7 +10,7 @@
</div> </div>
</header> </header>
<article class="xblock-render"> <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"> data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container"> <ol class="reorderable-container">
</ol> </ol>
......
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock" <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"> <div class="mock-xblock editor-with-buttons">
<h3>Mock XBlock Editor</h3> <h3>Mock XBlock Editor</h3>
......
<div class="xblock xblock-studio_view" data-runtime-version="1" data-usage-id="i4x:;_;_edX;_mock" <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"> <div class="mock-xblock">
<h3>Mock XBlock Editor</h3> <h3>Mock XBlock Editor</h3>
......
<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/"> <div class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
<section class="editor-with-tabs"> <section class="editor-with-tabs">
<div class="edit-header"> <div class="edit-header">
......
<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 class="wrapper-comp-editor is-active" id="editor-tab" data-base-asset-url="/c4x/AndyA/ABT101/asset/">
</div> </div>
<section class="sequence-edit"> <section class="sequence-edit">
......
<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"> <section class="sequence-edit">
<script id="metadata-editor-tpl" type="text/template"> <script id="metadata-editor-tpl" type="text/template">
<ul class="list-input settings-list"> <ul class="list-input settings-list">
......
<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>
<%! 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) ...@@ -8,16 +8,17 @@ xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element" section_class = "level-nesting" if show_inline else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else "" collapsible_class = "is-collapsible" if xblock.has_children else ""
label = xblock.display_name or xblock.scope_ids.block_type
%> %>
% if not is_root: % if not is_root:
% if is_reorderable: % 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: % 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 % 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 % endif
<header class="xblock-header xblock-header-${xblock.category}"> <header class="xblock-header xblock-header-${xblock.category}">
...@@ -29,7 +30,7 @@ collapsible_class = "is-collapsible" if xblock.has_children else "" ...@@ -29,7 +30,7 @@ collapsible_class = "is-collapsible" if xblock.has_children else ""
<span class="sr">${_('Expand or Collapse')}</span> <span class="sr">${_('Expand or Collapse')}</span>
</a> </a>
% endif % endif
<span class="xblock-display-name">${xblock.display_name_with_default | h}</span> <span class="xblock-display-name">${label | h}</span>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<ul class="actions-list"> <ul class="actions-list">
......
<%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 ...@@ -108,7 +108,7 @@ from django.core.urlresolvers import reverse
</div> </div>
</header> </header>
<article class="xblock-render"> <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> <h2>Video</h2>
......
<%! from django.utils.translation import ugettext as _ %> <%! 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 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 ...@@ -24,7 +25,7 @@ This def will enumerate through a passed in subsection and list all of the units
selected_class = '' selected_class = ''
%> %>
<div class="section-item ${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> <span class="unit-name">${unit.display_name_with_default}</span>
</a> </a>
% if actions: % if actions:
......
...@@ -75,7 +75,6 @@ urlpatterns += patterns( ...@@ -75,7 +75,6 @@ urlpatterns += patterns(
url(r'^course/{}?$'.format(settings.COURSE_KEY_PATTERN), 'course_handler', name='course_handler'), 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'^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'^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'^container/{}$'.format(settings.USAGE_KEY_PATTERN), 'container_handler'),
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_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'), url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
......
...@@ -37,11 +37,6 @@ REQUIREJS_WAIT = { ...@@ -37,11 +37,6 @@ REQUIREJS_WAIT = {
"jquery", "js/base", "js/models/course", "js/models/settings/advanced", "jquery", "js/base", "js/models/course", "js/models/settings/advanced",
"js/views/settings/advanced", "codemirror"], "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 # Content - Outline
# Note that calling your org, course number, or display name, 'course' will mess this up # Note that calling your org, course number, or display name, 'course' will mess this up
re.compile('^Course Outline \|'): [ re.compile('^Course Outline \|'): [
......
...@@ -174,8 +174,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context): ...@@ -174,8 +174,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
if is_studio_course and is_mongo_course: 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 # 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 + '/container/' + unicode(block.location)
edit_link = "//" + settings.CMS_BASE + '/unit/' + unicode(block.location)
# return edit link in rendered HTML for display # 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})) 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): ...@@ -171,7 +171,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
Context for rendering the studio "author_view". Context for rendering the studio "author_view".
""" """
return { return {
'container_view': True,
'reorderable_items': set(), 'reorderable_items': set(),
'root_xblock': root_xblock, 'root_xblock': root_xblock,
} }
......
...@@ -13,7 +13,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest): ...@@ -13,7 +13,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest):
""" """
reorderable_items = set() reorderable_items = set()
context = { context = {
'container_view': True,
'reorderable_items': reorderable_items, 'reorderable_items': reorderable_items,
'read_only': False, 'read_only': False,
'root_xblock': self.vertical, 'root_xblock': self.vertical,
......
...@@ -57,7 +57,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest): ...@@ -57,7 +57,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
""" """
# Vertical shouldn't render children on the unit page # Vertical shouldn't render children on the unit page
context = { context = {
'container_view': False, 'is_unit_page': True
} }
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
self.assertNotIn(self.test_html_1, html) self.assertNotIn(self.test_html_1, html)
...@@ -66,7 +66,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest): ...@@ -66,7 +66,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
# Vertical should render reorderable children on the container page # Vertical should render reorderable children on the container page
reorderable_items = set() reorderable_items = set()
context = { context = {
'container_view': True, 'is_unit_page': False,
'reorderable_items': reorderable_items, 'reorderable_items': reorderable_items,
} }
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
......
...@@ -45,9 +45,13 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule): ...@@ -45,9 +45,13 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
Renders the Studio preview view, which supports drag and drop. Renders the Studio preview view, which supports drag and drop.
""" """
fragment = Fragment() 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 # 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. # a more concise version that appears alongside the "View =>" link-- unless it is
if context.get('container_view'): # 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) self.render_children(context, fragment, can_reorder=True, can_add=True)
return fragment return fragment
......
...@@ -15,7 +15,7 @@ class ContainerPage(PageObject): ...@@ -15,7 +15,7 @@ class ContainerPage(PageObject):
""" """
Container page in Studio Container page in Studio
""" """
NAME_SELECTOR = 'a.navigation-current' NAME_SELECTOR = '.page-header-title'
def __init__(self, browser, locator): def __init__(self, browser, locator):
super(ContainerPage, self).__init__(browser) super(ContainerPage, self).__init__(browser)
...@@ -126,16 +126,8 @@ class ContainerPage(PageObject): ...@@ -126,16 +126,8 @@ class ContainerPage(PageObject):
def edit(self): def edit(self):
""" """
Clicks the "edit" button for the first component on the page. 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() return _click_edit(self)
EmptyPromise(
lambda: self.q(css='.xblock-studio_view').present,
'Wait for the Studio editor to be present'
).fulfill()
return self
def add_missing_groups(self): def add_missing_groups(self):
""" """
...@@ -164,7 +156,7 @@ class XBlockWrapper(PageObject): ...@@ -164,7 +156,7 @@ class XBlockWrapper(PageObject):
""" """
url = None url = None
BODY_SELECTOR = '.studio-xblock-wrapper' BODY_SELECTOR = '.studio-xblock-wrapper'
NAME_SELECTOR = '.header-details' NAME_SELECTOR = '.xblock-display-name'
def __init__(self, browser, locator): def __init__(self, browser, locator):
super(XBlockWrapper, self).__init__(browser) super(XBlockWrapper, self).__init__(browser)
...@@ -210,3 +202,33 @@ class XBlockWrapper(PageObject): ...@@ -210,3 +202,33 @@ class XBlockWrapper(PageObject):
@property @property
def preview_selector(self): def preview_selector(self):
return self._bounded_selector('.xblock-student_view,.xblock-author_view') 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 ...@@ -5,7 +5,7 @@ from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise from bok_choy.promise import EmptyPromise
from .course_page import CoursePage from .course_page import CoursePage
from .unit import UnitPage from .container import ContainerPage
class CourseOutlineContainer(object): class CourseOutlineContainer(object):
...@@ -84,10 +84,10 @@ class CourseOutlineUnit(CourseOutlineChild): ...@@ -84,10 +84,10 @@ class CourseOutlineUnit(CourseOutlineChild):
def go_to(self): def go_to(self):
""" """
Open the unit page linked to by this unit link, and return Open the container page linked to by this unit link, and return
an initialized :class:`.UnitPage` for that unit. 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): def is_browser_on_page(self):
return self.q(css=self.BODY_SELECTOR).present return self.q(css=self.BODY_SELECTOR).present
......
"""
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): ...@@ -73,7 +73,7 @@ class XBlockAcidBase(WebAppTest):
subsection = self.outline.section('Test Section').subsection('Test Subsection') subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to() 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) self.validate_acid_block_preview(acid_block)
def test_acid_block_editor(self): def test_acid_block_editor(self):
...@@ -85,9 +85,7 @@ class XBlockAcidBase(WebAppTest): ...@@ -85,9 +85,7 @@ class XBlockAcidBase(WebAppTest):
subsection = self.outline.section('Test Section').subsection('Test Subsection') subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to() unit = subsection.toggle_expand().unit('Test Unit').go_to()
unit.edit_draft() acid_block = AcidView(self.browser, unit.xblocks[0].edit().editor_selector)
acid_block = AcidView(self.browser, unit.components[0].edit().editor_selector)
self.assertTrue(acid_block.init_fn_passed) self.assertTrue(acid_block.init_fn_passed)
self.assertTrue(acid_block.resource_url_passed) self.assertTrue(acid_block.resource_url_passed)
self.assertTrue(acid_block.scope_passed('content')) self.assertTrue(acid_block.scope_passed('content'))
...@@ -141,15 +139,11 @@ class XBlockAcidParentBase(XBlockAcidBase): ...@@ -141,15 +139,11 @@ class XBlockAcidParentBase(XBlockAcidBase):
self.outline.visit() self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection') subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to() 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) acid_block = AcidView(self.browser, container.xblocks[0].preview_selector)
self.validate_acid_block_preview(acid_block) 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): class XBlockAcidEmptyParentTest(XBlockAcidParentBase):
""" """
...@@ -212,7 +206,6 @@ class XBlockAcidChildTest(XBlockAcidParentBase): ...@@ -212,7 +206,6 @@ class XBlockAcidChildTest(XBlockAcidParentBase):
self.user = course_fix.user self.user = course_fix.user
@skip('This will fail until we fix support of children in pure XBlocks') @skip('This will fail until we fix support of children in pure XBlocks')
def test_acid_block_preview(self): def test_acid_block_preview(self):
super(XBlockAcidChildTest, self).test_acid_block_preview() super(XBlockAcidChildTest, self).test_acid_block_preview()
......
...@@ -34,17 +34,16 @@ class ContainerBase(StudioCourseTest): ...@@ -34,17 +34,16 @@ class ContainerBase(StudioCourseTest):
self.course_info['run'] 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. Go to the nested container page.
If make_draft is true, the unit page (accessed on way to container page) will be put into draft mode.
""" """
unit = self.go_to_unit_page(make_draft) unit = self.go_to_unit_page()
container = unit.components[0].go_to_container() # The 0th entry is the unit page itself.
container = unit.xblocks[1].go_to_container()
return container return container
def go_to_unit_page(self, make_draft=False): def go_to_unit_page(self):
""" """
Go to the test unit page. Go to the test unit page.
...@@ -52,10 +51,7 @@ class ContainerBase(StudioCourseTest): ...@@ -52,10 +51,7 @@ class ContainerBase(StudioCourseTest):
""" """
self.outline.visit() self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection') subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to() return subsection.toggle_expand().unit('Test Unit').go_to()
if make_draft:
unit.edit_draft()
return unit
def verify_ordering(self, container, expected_orderings): def verify_ordering(self, container, expected_orderings):
""" """
...@@ -83,13 +79,13 @@ class ContainerBase(StudioCourseTest): ...@@ -83,13 +79,13 @@ class ContainerBase(StudioCourseTest):
""" """
Perform the supplied action and then verify the resulting ordering. 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) action(container)
self.verify_ordering(container, expected_ordering) self.verify_ordering(container, expected_ordering)
# Reload the page to see that the change was persisted. # 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) self.verify_ordering(container, expected_ordering)
...@@ -101,9 +97,9 @@ class NestedVerticalTest(ContainerBase): ...@@ -101,9 +97,9 @@ class NestedVerticalTest(ContainerBase):
Sets up a course structure with nested verticals. Sets up a course structure with nested verticals.
""" """
self.container_title = "" self.container_title = ""
self.group_a = "Expand or Collapse\nGroup A" self.group_a = "Group A"
self.group_b = "Expand or Collapse\nGroup B" self.group_b = "Group B"
self.group_empty = "Expand or Collapse\nGroup Empty" self.group_empty = "Group Empty"
self.group_a_item_1 = "Group A Item 1" self.group_a_item_1 = "Group A Item 1"
self.group_a_item_2 = "Group A Item 2" self.group_a_item_2 = "Group A Item 2"
self.group_b_item_1 = "Group B Item 1" self.group_b_item_1 = "Group B Item 1"
...@@ -360,13 +356,13 @@ class EditContainerTest(NestedVerticalTest): ...@@ -360,13 +356,13 @@ class EditContainerTest(NestedVerticalTest):
""" """
Test the "edit" button on a container appearing on the unit page. Test the "edit" button on a container appearing on the unit page.
""" """
unit = self.go_to_unit_page(make_draft=True) unit = self.go_to_unit_page()
component = unit.components[0] component = unit.xblocks[1]
self.modify_display_name_and_verify(component) self.modify_display_name_and_verify(component)
def test_edit_container_on_container_page(self): def test_edit_container_on_container_page(self):
""" """
Test the "edit" button on a container appearing on the container page. 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) self.modify_display_name_and_verify(container)
...@@ -57,7 +57,7 @@ class SplitTestMixin(object): ...@@ -57,7 +57,7 @@ class SplitTestMixin(object):
def verify_add_missing_groups_button_not_present(self, container): 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(): def missing_groups_button_not_present():
button_present = container.missing_groups_button_present() button_present = container.missing_groups_button_present()
...@@ -105,9 +105,9 @@ class SplitTest(ContainerBase, SplitTestMixin): ...@@ -105,9 +105,9 @@ class SplitTest(ContainerBase, SplitTestMixin):
Returns the container page. 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') add_advanced_component(unit, 0, 'split_test')
container = self.go_to_container_page() container = self.go_to_nested_container_page()
container.edit() container.edit()
component_editor = ComponentEditorView(self.browser, container.locator) component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta') component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
...@@ -119,16 +119,16 @@ class SplitTest(ContainerBase, SplitTestMixin): ...@@ -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): def test_create_and_select_group_configuration(self):
""" """
Tests creating a split test instance on the unit page, and then Tests creating a split test instance on the unit page, and then
assigning the group configuration. 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') add_advanced_component(unit, 0, 'split_test')
container = self.go_to_container_page() container = self.go_to_nested_container_page()
container.edit() container.edit()
component_editor = ComponentEditorView(self.browser, container.locator) component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta') component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
...@@ -136,14 +136,14 @@ class SplitTest(ContainerBase, SplitTestMixin): ...@@ -136,14 +136,14 @@ class SplitTest(ContainerBase, SplitTestMixin):
# Switch to the other group configuration. Must navigate again to the container page so # Switch to the other group configuration. Must navigate again to the container page so
# that there is only a single "editor" on the page. # 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() container.edit()
component_editor = ComponentEditorView(self.browser, container.locator) component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration 0,1,2') 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']) self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
# Reload the page to make sure the groups were persisted. # 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']) 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") @skip("This fails periodically where it fails to trigger the add missing groups action.Dis")
...@@ -161,7 +161,7 @@ class SplitTest(ContainerBase, SplitTestMixin): ...@@ -161,7 +161,7 @@ class SplitTest(ContainerBase, SplitTestMixin):
self.verify_groups(container, ['alpha', 'gamma'], ['beta']) self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
# Reload the page to make sure the groups were persisted. # 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']) self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
@skip("Disabling as this fails intermittently. STUD-2003") @skip("Disabling as this fails intermittently. STUD-2003")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment