Commit 20675857 by Andy Armstrong

Merge pull request #3999 from edx/christina/updating-split-test

Ability to change the associated group configuration for a split test
parents 46541eb8 4b4736cc
......@@ -7,6 +7,9 @@ the top. Include a label indicating the component affected.
Studio: Move Peer Assessment into advanced problems menu.
Studio: Support creation and editing of split_test instances (Content Experiments)
entirely in Studio. STUD-1658.
Blades: Add context-aware video index. BLD-933
Blades: Fix bug with incorrect link format and redirection. BLD-1049
......
......@@ -23,6 +23,7 @@ import xmodule
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW
from util.json_request import expect_json, JsonResponse
from util.string_utils import str_to_bool
......@@ -180,15 +181,15 @@ def xblock_view_handler(request, usage_key_string, view_name):
xblock = store.get_item(usage_key)
is_read_only = _is_xblock_read_only(xblock)
container_views = ['container_preview', 'reorderable_container_child_preview']
unit_views = ['student_view']
unit_views = PREVIEW_VIEWS
# wrap the generated fragment in the xmodule_editor div so that the javascript
# can bind to it correctly
xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime', usage_id_serializer=unicode))
if view_name == 'studio_view':
if view_name == STUDIO_VIEW:
try:
fragment = xblock.render('studio_view')
fragment = xblock.render(STUDIO_VIEW)
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
......@@ -213,7 +214,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
# Note: this special case logic can be removed once the unit page is replaced
# with the new container view.
context = {
'runtime_type': 'studio',
'container_view': is_container_view,
'read_only': is_read_only,
'root_xblock': xblock if (view_name == 'container_preview') else None,
......
......@@ -10,6 +10,7 @@ from django.contrib.auth.decorators import login_required
from edxmako.shortcuts import render_to_string
from xmodule_modifiers import replace_static_urls, wrap_xblock, wrap_fragment
from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore.django import modulestore, ModuleI18nService
......@@ -21,13 +22,14 @@ from xblock.exceptions import NoSuchHandlerError
from xblock.fragment import Fragment
from lms.lib.xblock.field_data import LmsFieldData
from cms.lib.xblock.field_data import CmsFieldData
from cms.lib.xblock.runtime import local_resource_url
from util.sandboxing import can_execute_unsafe_code
import static_replace
from .session_kv_store import SessionKeyValueStore
from .helpers import render_from_lms, xblock_has_own_studio_page
from .helpers import render_from_lms
from contentstore.views.access import get_user_role
......@@ -143,15 +145,20 @@ def _preview_module_system(request, descriptor):
def _load_preview_module(request, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor.
Return a preview XModule instantiated from the supplied descriptor. Will use mutable fields
if XModule supports an author_view. Otherwise, will use immutable fields and student_view.
request: The active django request
descriptor: An XModuleDescriptor
"""
student_data = KvsFieldData(SessionKeyValueStore(request))
if _has_author_view(descriptor):
field_data = CmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
else:
field_data = LmsFieldData(descriptor._field_data, student_data) # pylint: disable=protected-access
descriptor.bind_for_student(
_preview_module_system(request, descriptor),
LmsFieldData(descriptor._field_data, student_data), # pylint: disable=protected-access
field_data
)
return descriptor
......@@ -169,7 +176,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
Wraps the results of rendering an XBlock view in a div which adds a header and Studio action buttons.
"""
# Only add the Studio wrapper when on the container page. The unit page will remain as is for now.
if context.get('container_view', None) and view == 'student_view':
if context.get('container_view', None) and view in PREVIEW_VIEWS:
root_xblock = context.get('root_xblock')
is_root = root_xblock and xblock.location == root_xblock.location
is_reorderable = _is_xblock_reorderable(xblock, context)
......@@ -187,14 +194,25 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
def get_preview_fragment(request, descriptor, context):
"""
Returns the HTML returned by the XModule's student_view,
Returns the HTML returned by the XModule's student_view or author_view (if available),
specified by the descriptor and idx.
"""
module = _load_preview_module(request, descriptor)
preview_view = AUTHOR_VIEW if _has_author_view(module) else STUDENT_VIEW
try:
fragment = module.render("student_view", context)
fragment = module.render(preview_view, context)
except Exception as exc: # pylint: disable=W0703
log.warning("Unable to render student_view for %r", module, exc_info=True)
log.warning("Unable to render %s for %r", preview_view, module, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
return fragment
def _has_author_view(descriptor):
"""
Returns True if the xmodule linked to the descriptor supports "author_view".
If False, "student_view" and LmsFieldData should be used.
"""
return getattr(descriptor, 'has_author_view', False)
......@@ -22,6 +22,7 @@ from student.tests.factories import UserFactory
from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.x_module import STUDIO_VIEW, STUDENT_VIEW
from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locations import Location
from xmodule.partitions.partitions import Group, UserPartition
......@@ -106,7 +107,7 @@ class GetItem(ItemTest):
self.assertNotIn('wrapper-xblock', html)
# Verify that the header and article tags are still added
self.assertIn('<header class="xblock-header">', html)
self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
self.assertIn('<article class="xblock-render">', html)
def test_get_container_fragment(self):
......@@ -122,7 +123,7 @@ class GetItem(ItemTest):
# Verify that the Studio nesting wrapper has been added
self.assertIn('level-nesting', html)
self.assertIn('<header class="xblock-header">', html)
self.assertIn('<header class="xblock-header xblock-header-vertical">', html)
self.assertIn('<article class="xblock-render">', html)
# Verify that the Studio element wrapper has been added
......@@ -712,12 +713,12 @@ class TestEditItem(ItemTest):
self.assertNotEqual(draft.data, published.data)
# Get problem by 'xblock_handler'
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": "student_view"})
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDENT_VIEW})
resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
self.assertEqual(resp.status_code, 200)
# Activate the editing view
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": "studio_view"})
view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDIO_VIEW})
resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
self.assertEqual(resp.status_code, 200)
......@@ -811,7 +812,15 @@ class TestEditSplitModule(ItemTest):
self.assertEqual(partition_id, split_test.user_partition_id)
return split_test
def test_split_create_groups(self):
def _assert_children(self, expected_number):
"""
Verifies the number of children of the split_test instance.
"""
split_test = self.get_item_from_modulestore(self.split_test_usage_key, True)
self.assertEqual(expected_number, len(split_test.children))
return split_test
def test_create_groups(self):
"""
Test that verticals are created for the experiment groups when
a spit test module is edited.
......@@ -833,16 +842,14 @@ class TestEditSplitModule(ItemTest):
self.assertEqual("alpha", vertical_0.display_name)
self.assertEqual("beta", vertical_1.display_name)
# Verify that the group_id_to child mapping is correct.
# Verify that the group_id_to_child mapping is correct.
self.assertEqual(2, len(split_test.group_id_to_child))
split_test.group_id_to_child['0'] = vertical_0.location
split_test.group_id_to_child['1'] = vertical_1.location
self.assertEqual(vertical_0.location, split_test.group_id_to_child['0'])
self.assertEqual(vertical_1.location, split_test.group_id_to_child['1'])
def test_split_change_user_partition_id(self):
def test_change_user_partition_id(self):
"""
Test what happens when the user_partition_id is changed to a different experiment.
This is not currently supported by the Studio UI.
"""
# Set to first experiment.
split_test = self._update_partition_id(0)
......@@ -852,21 +859,23 @@ class TestEditSplitModule(ItemTest):
# Set to second experiment
split_test = self._update_partition_id(1)
# We don't currently remove existing children.
# We don't remove existing children.
self.assertEqual(5, len(split_test.children))
self.assertEqual(initial_vertical_0_location, split_test.children[0])
self.assertEqual(initial_vertical_1_location, split_test.children[1])
vertical_0 = self.get_item_from_modulestore(split_test.children[2], True)
vertical_1 = self.get_item_from_modulestore(split_test.children[3], True)
vertical_2 = self.get_item_from_modulestore(split_test.children[4], True)
# Verify that the group_id_to child mapping is correct.
self.assertEqual(3, len(split_test.group_id_to_child))
split_test.group_id_to_child['0'] = vertical_0.location
split_test.group_id_to_child['1'] = vertical_1.location
split_test.group_id_to_child['2'] = vertical_2.location
self.assertEqual(vertical_0.location, split_test.group_id_to_child['0'])
self.assertEqual(vertical_1.location, split_test.group_id_to_child['1'])
self.assertEqual(vertical_2.location, split_test.group_id_to_child['2'])
self.assertNotEqual(initial_vertical_0_location, vertical_0.location)
self.assertNotEqual(initial_vertical_1_location, vertical_1.location)
def test_split_same_user_partition_id(self):
def test_change_same_user_partition_id(self):
"""
Test that nothing happens when the user_partition_id is set to the same value twice.
"""
......@@ -880,7 +889,7 @@ class TestEditSplitModule(ItemTest):
self.assertEqual(2, len(split_test.children))
self.assertEqual(initial_group_id_to_child, split_test.group_id_to_child)
def test_split_non_existent_user_partition_id(self):
def test_change_non_existent_user_partition_id(self):
"""
Test that nothing happens when the user_partition_id is set to a value that doesn't exist.
......@@ -896,6 +905,80 @@ class TestEditSplitModule(ItemTest):
self.assertEqual(2, len(split_test.children))
self.assertEqual(initial_group_id_to_child, split_test.group_id_to_child)
def test_delete_children(self):
"""
Test that deleting a child in the group_id_to_child map updates the map.
Also test that deleting a child not in the group_id_to_child_map behaves properly.
"""
# Set to first experiment.
self._update_partition_id(0)
split_test = self._assert_children(2)
vertical_1_usage_key = split_test.children[1]
# Add an extra child to the split_test
resp = self.create_xblock(category='html', parent_usage_key=self.split_test_usage_key)
extra_child_usage_key = self.response_usage_key(resp)
self._assert_children(3)
# Remove the first child (which is part of the group configuration).
resp = self.client.ajax_post(
self.split_test_update_url,
data={'children': [unicode(vertical_1_usage_key), unicode(extra_child_usage_key)]}
)
self.assertEqual(resp.status_code, 200)
split_test = self._assert_children(2)
# Check that group_id_to_child was updated appropriately
group_id_to_child = split_test.group_id_to_child
self.assertEqual(1, len(group_id_to_child))
self.assertEqual(vertical_1_usage_key, group_id_to_child['1'])
# Remove the "extra" child and make sure that group_id_to_child did not change.
resp = self.client.ajax_post(
self.split_test_update_url,
data={'children': [unicode(vertical_1_usage_key)]}
)
self.assertEqual(resp.status_code, 200)
split_test = self._assert_children(1)
self.assertEqual(group_id_to_child, split_test.group_id_to_child)
def test_add_groups(self):
"""
Test the "fix up behavior" when groups are missing (after a group is added to a group configuration).
This test actually belongs over in common, but it relies on a mutable modulestore.
TODO: move tests that can go over to common after the mixed modulestore work is done. # pylint: disable=fixme
"""
# Set to first group configuration.
split_test = self._update_partition_id(0)
# Add a group to the first group configuration.
split_test.user_partitions = [
UserPartition(
0, 'first_partition', 'First Partition',
[Group("0", 'alpha'), Group("1", 'beta'), Group("2", 'pie')]
)
]
self.store.update_item(split_test, self.user.id)
# group_id_to_child and children have not changed yet.
split_test = self._assert_children(2)
group_id_to_child = split_test.group_id_to_child
self.assertEqual(2, len(group_id_to_child))
# Call add_missing_groups method to add the missing group.
split_test.add_missing_groups(None)
split_test = self._assert_children(3)
self.assertNotEqual(group_id_to_child, split_test.group_id_to_child)
group_id_to_child = split_test.group_id_to_child
self.assertEqual(split_test.children[2], group_id_to_child["2"])
# Call add_missing_groups again -- it should be a no-op.
split_test.add_missing_groups(None)
split_test = self._assert_children(3)
self.assertEqual(group_id_to_child, split_test.group_id_to_child)
@ddt.ddt
class TestComponentHandler(TestCase):
......
......@@ -4,7 +4,7 @@ import json
from contentstore.views import tabs
from contentstore.tests.utils import CourseTestCase
from django.test import TestCase
from xmodule.modulestore.django import loc_mapper
from xmodule.x_module import STUDENT_VIEW
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.tabs import CourseTabList, WikiTab
from contentstore.utils import reverse_course_url
......@@ -178,7 +178,7 @@ class TabsPageTests(CourseTestCase):
"""
Verify that the static tab renders itself with the correct HTML
"""
preview_url = '/xblock/{}/student_view'.format(self.test_tab.location)
preview_url = '/xblock/{}/{}'.format(self.test_tab.location, STUDENT_VIEW)
resp = self.client.get(preview_url, HTTP_ACCEPT='application/json')
self.assertEqual(resp.status_code, 200)
......
......@@ -5,6 +5,7 @@ Unit tests for the unit page.
from contentstore.views.tests.utils import StudioPageTestCase
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.x_module import STUDENT_VIEW
class UnitPageTestCase(StudioPageTestCase):
......@@ -38,7 +39,7 @@ class UnitPageTestCase(StudioPageTestCase):
"""
Verify that a public xblock's preview returns the expected HTML.
"""
self.validate_preview_html(self.video, 'student_view',
self.validate_preview_html(self.video, STUDENT_VIEW,
can_edit=True, can_reorder=True, can_add=False)
def test_draft_component_preview_html(self):
......@@ -47,7 +48,7 @@ class UnitPageTestCase(StudioPageTestCase):
"""
modulestore('draft').convert_to_draft(self.vertical.location)
draft_video = modulestore('draft').convert_to_draft(self.video.location)
self.validate_preview_html(draft_video, 'student_view',
self.validate_preview_html(draft_video, STUDENT_VIEW,
can_edit=True, can_reorder=True, can_add=False)
def test_public_child_container_preview_html(self):
......@@ -59,7 +60,7 @@ class UnitPageTestCase(StudioPageTestCase):
category='split_test', display_name='Split Test')
ItemFactory.create(parent_location=child_container.location,
category='html', display_name='grandchild')
self.validate_preview_html(child_container, 'student_view',
self.validate_preview_html(child_container, STUDENT_VIEW,
can_reorder=True, can_edit=True, can_add=False)
def test_draft_child_container_preview_html(self):
......@@ -73,5 +74,5 @@ class UnitPageTestCase(StudioPageTestCase):
category='html', display_name='grandchild')
modulestore('draft').convert_to_draft(self.vertical.location)
draft_child_container = modulestore('draft').get_item(child_container.location)
self.validate_preview_html(draft_child_container, 'student_view',
self.validate_preview_html(draft_child_container, STUDENT_VIEW,
can_reorder=True, can_edit=True, can_add=False)
"""
:class:`~xblock.field_data.FieldData` subclasses used by the CMS
"""
from xblock.field_data import SplitFieldData
from xblock.fields import Scope
class CmsFieldData(SplitFieldData):
"""
A :class:`~xblock.field_data.FieldData` that
reads all UserScope.ONE and UserScope.ALL fields from `student_data`
and all UserScope.NONE fields from `authored_data`. It allows writing to`authored_data`.
"""
def __init__(self, authored_data, student_data):
# Make sure that we don't repeatedly nest CmsFieldData instances
if isinstance(authored_data, CmsFieldData):
authored_data = authored_data._authored_data # pylint: disable=protected-access
self._authored_data = authored_data
self._student_data = student_data
super(CmsFieldData, self).__init__({
Scope.content: authored_data,
Scope.settings: authored_data,
Scope.parent: authored_data,
Scope.children: authored_data,
Scope.user_state_summary: student_data,
Scope.user_state: student_data,
Scope.user_info: student_data,
Scope.preferences: student_data,
})
......@@ -233,6 +233,8 @@ define([
"js/spec/views/modals/base_modal_spec",
"js/spec/views/modals/edit_xblock_spec",
"js/spec/xblock/cms.runtime.v1_spec",
# these tests are run separately in the cms-squire suite, due to process
# isolation issues with Squire.js
# "coffee/spec/views/assets_spec"
......
define [
"jquery", "xblock/runtime.v1", "URI", "gettext",
"jquery", "backbone", "xblock/runtime.v1", "URI", "gettext",
"js/utils/modal", "js/views/feedback_notification"
], ($, XBlock, URI, gettext, ModalUtils, NotificationView) ->
@PreviewRuntime = {}
], ($, Backbone, XBlock, URI, gettext, ModalUtils, NotificationView) ->
@BaseRuntime = {}
class PreviewRuntime.v1 extends XBlock.Runtime.v1
class BaseRuntime.v1 extends XBlock.Runtime.v1
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/preview/xblock").segment($(element).data('usage-id'))
uri = URI(@handlerPrefix).segment($(element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
if query? then uri.search(query)
uri.toString()
@StudioRuntime = {}
class StudioRuntime.v1 extends XBlock.Runtime.v1
constructor: () ->
super()
@savingNotification = new NotificationView.Mini
title: gettext('Saving&hellip;')
@alert = new NotificationView.Error
title: "OpenAssessment Save Error",
closeIcon: false,
shown: false
@dispatcher = _.clone(Backbone.Events)
@listenTo('save', @_handleSave)
@listenTo('cancel', @_handleCancel)
@listenTo('error', @_handleError)
@listenTo('modal-shown', (data) ->
@modal = data)
@listenTo('modal-hidden', () ->
@modal = null)
@listenTo('page-shown', (data) ->
@page = data)
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/xblock").segment($(element).data('usage-id'))
.segment('handler')
.segment(handlerName)
if suffix? then uri.segment(suffix)
if query? then uri.search(query)
uri.toString()
# Notify the Studio client-side runtime so it can update
# the UI in a consistent way. Currently, this is used
# for save / cancel when editing an XBlock.
# Although native XBlocks should handle their own persistence,
# Studio still needs to update the UI in a consistent way
# (showing the "Saving..." notification, closing the modal editing dialog, etc.)
# Notify the Studio client-side runtime of an event so that it can update the UI in a consistent way.
notify: (name, data) ->
if name == 'save'
if 'state' of data
@dispatcher.trigger(name, data)
# Starting to save, so show the "Saving..." notification
if data.state == 'start'
@savingNotification.show()
# Listen to a Studio event and invoke the specified callback when it is triggered.
listenTo: (name, callback) ->
@dispatcher.bind(name, callback, this)
# Finished saving, so hide the "Saving..." notification
else if data.state == 'end'
@_hideAlerts()
# Refresh the view for the xblock represented by the specified element.
refreshXBlock: (element) ->
if @page
@page.refreshXBlock(element)
# Notify the modal that the save has completed so that it can hide itself
# and then refresh the xblock.
if @modal
@modal.onSave()
_handleError: (data) ->
message = data.message || data.msg
if message
# TODO: remove 'Open Assessment' specific default title
title = data.title || gettext("OpenAssessment Save Error")
@alert = new NotificationView.Error
title: title
message: message
closeIcon: false
shown: false
@alert.show()
@savingNotification.hide()
_handleSave: (data) ->
# Starting to save, so show a notification
if data.state == 'start'
message = data.message || gettext('Saving&hellip;')
@notification = new NotificationView.Mini
title: message
@notification.show()
else if name == 'edit-modal-shown'
@modal = data
# Finished saving, so hide the notification and refresh appropriately
else if data.state == 'end'
@_hideAlerts()
else if name == 'edit-modal-hidden'
@modal = null
# Notify the modal that the save has completed so that it can hide itself
# and then refresh the xblock.
if @modal and @modal.onSave
@modal.onSave()
# ... else ask it to refresh the newly saved xblock
else if data.element
@refreshXBlock(data.element)
else if name == 'cancel'
@_hideAlerts()
if @modal
@modal.cancel()
@notification.hide()
else if name == 'error'
if 'msg' of data
@alert.options.message = data.msg
@alert.show()
_handleCancel: () ->
@_hideAlerts()
if @modal
@modal.cancel()
@notify('modal-hidden')
_hideAlerts: () ->
# Hide any alerts that are being shown
if @alert.options.shown
if @alert && @alert.options.shown
@alert.hide()
@PreviewRuntime = {}
class PreviewRuntime.v1 extends BaseRuntime.v1
handlerPrefix: '/preview/xblock'
@StudioRuntime = {}
class StudioRuntime.v1 extends BaseRuntime.v1
handlerPrefix: '/xblock'
define(["jquery", "underscore", "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"],
function ($, _, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) {
function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) {
describe("ContainerPage", function() {
var lastRequest, renderContainerPage, expectComponents, respondWithHtml,
......@@ -96,7 +96,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
editButtons.first().click();
// Expect a request to be made to show the studio view for the container
expect(lastRequest().url.startsWith('/xblock/locator-container/studio_view')).toBeTruthy();
expect(str.startsWith(lastRequest().url, '/xblock/locator-container/studio_view')).toBeTruthy();
create_sinon.respondWithJson(requests, {
html: mockContainerXBlockHtml,
resources: []
......@@ -112,7 +112,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
expect(edit_helpers.isShowingModal()).toBeFalsy();
// Expect the last request be to refresh the container page
expect(lastRequest().url.startsWith('/xblock/locator-container/container_preview')).toBeTruthy();
expect(str.startsWith(lastRequest().url, '/xblock/locator-container/container_preview')).toBeTruthy();
create_sinon.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
......@@ -149,7 +149,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
expect(editButtons.length).toBe(6);
editButtons[0].click();
// Make sure that the correct xblock is requested to be edited
expect(lastRequest().url.startsWith('/xblock/locator-component-A1/studio_view')).toBeTruthy();
expect(str.startsWith(lastRequest().url, '/xblock/locator-component-A1/studio_view')).toBeTruthy();
create_sinon.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
......
define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/module_info",
define(["jquery", "underscore.string", "jasmine", "coffee/src/views/unit", "js/models/module_info",
"js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "jasmine-stealth"],
function ($, _, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) {
function ($, str, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) {
var requests, unitView, initialize, lastRequest, respondWithHtml, verifyComponents, i,
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
......@@ -150,7 +150,7 @@ define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/m
});
describe("Disabled edit/publish links during ajax call", function() {
var link, i,
var link,
draft_states = [
{
state: "draft",
......@@ -204,7 +204,7 @@ define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/m
expect(editButtons.length).toBe(2);
editButtons[1].click();
// Make sure that the correct xblock is requested to be edited
expect(lastRequest().url.startsWith('/xblock/loc_2/studio_view')).toBeTruthy();
expect(str.startsWith(lastRequest().url, '/xblock/loc_2/studio_view')).toBeTruthy();
create_sinon.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
......
define(["js/spec_helpers/edit_helpers", "js/views/modals/base_modal", "xblock/cms.runtime.v1"],
function (edit_helpers, BaseModal) {
describe("Studio Runtime v1", function() {
var runtime;
beforeEach(function () {
edit_helpers.installEditTemplates();
runtime = new window.StudioRuntime.v1();
});
it('allows events to be listened to', function() {
var canceled = false;
runtime.listenTo('cancel', function() {
canceled = true;
});
expect(canceled).toBeFalsy();
runtime.notify('cancel', {});
expect(canceled).toBeTruthy();
});
it('shows save notifications', function() {
var title = "Mock saving...",
notificationSpy = edit_helpers.createNotificationSpy();
runtime.notify('save', {
state: 'start',
message: title
});
edit_helpers.verifyNotificationShowing(notificationSpy, title);
runtime.notify('save', {
state: 'end'
});
edit_helpers.verifyNotificationHidden(notificationSpy);
});
it('shows error messages', function() {
var title = "Mock Error",
message = "This is a mock error.",
notificationSpy = edit_helpers.createNotificationSpy("Error");
runtime.notify('error', {
title: title,
message: message
});
edit_helpers.verifyNotificationShowing(notificationSpy, title);
});
describe("Modal Dialogs", function() {
var MockModal, modal, showMockModal;
MockModal = BaseModal.extend({
getContentHtml: function() {
return readFixtures('mock/mock-modal.underscore');
}
});
showMockModal = function() {
modal = new MockModal({
title: "Mock Modal"
});
modal.show();
};
beforeEach(function () {
edit_helpers.installEditTemplates();
});
afterEach(function() {
edit_helpers.hideModalIfShowing(modal);
});
it('cancels a modal dialog', function () {
showMockModal();
runtime.notify('modal-shown', modal);
expect(edit_helpers.isShowingModal(modal)).toBeTruthy();
runtime.notify('cancel');
expect(edit_helpers.isShowingModal(modal)).toBeFalsy();
});
});
});
});
......@@ -21,8 +21,8 @@ define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sino
appendSetFixtures('<div id="page-notification"></div>');
};
createNotificationSpy = function() {
var notificationSpy = spyOnConstructor(NotificationView, "Mini", ["show", "hide"]);
createNotificationSpy = function(type) {
var notificationSpy = spyOnConstructor(NotificationView, type || "Mini", ["show", "hide"]);
notificationSpy.show.andReturn(notificationSpy);
return notificationSpy;
};
......
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification"],
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification",
"jquery.ui"], // The container view uses sortable, which is provided by jquery.ui.
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
var reorderableClass = '.reorderable-container',
sortableInitializedClass = '.ui-sortable',
......
......@@ -71,7 +71,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
// Notify the runtime that the modal has been shown
if (runtime) {
this.runtime = runtime;
runtime.notify("edit-modal-shown", this);
runtime.notify('modal-shown', this);
}
// Update the modal's header
......@@ -166,12 +166,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
// Notify the runtime that the modal has been hidden
if (this.runtime) {
this.runtime.notify('edit-modal-hidden');
this.runtime.notify('modal-hidden');
}
// Completely clear the contents of the modal
this.undelegateEvents();
this.$el.html("");
},
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
......@@ -180,7 +176,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
displayName;
if (xblockWrapperElement.length > 0) {
xblockElement = xblockWrapperElement.find('.xblock');
displayName = xblockWrapperElement.find('.xblock-header .header-details').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 (!displayName) {
displayName = this.xblockElement.find('.component-header').text().trim();
......
......@@ -2,11 +2,9 @@
* XBlockContainerPage is used to display Studio's container page for an xblock which has children.
* This page allows the user to understand and manipulate the xblock and its children.
*/
define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
"js/views/baseview", "js/views/container", "js/views/xblock", "js/views/components/add_xblock",
"js/views/modals/edit_xblock", "js/models/xblock_info"],
function ($, _, gettext, NotificationView, BaseView, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, XBlockInfo) {
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/container",
"js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock", "js/models/xblock_info"],
function ($, _, gettext, BaseView, ContainerView, XBlockView, AddXBlockComponent, EditXBlockModal, XBlockInfo) {
var XBlockContainerPage = BaseView.extend({
// takes XBlockInfo as a model
......@@ -30,12 +28,16 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
// Hide both blocks until we know which one to show
xblockView.$el.addClass('is-hidden');
// Add actions to any top level buttons, e.g. "Edit" of the container itself
self.addButtonActions(this.$el);
if (!options || !options.refresh) {
// Add actions to any top level buttons, e.g. "Edit" of the container itself.
// Do not add the actions on "refresh" though, as the handlers are already registered.
self.addButtonActions(this.$el);
}
// Render the xblock
xblockView.render({
success: function(xblock) {
xblockView.xblock.runtime.notify("page-shown", self);
xblockView.$el.removeClass('is-hidden');
self.renderAddXBlockComponents();
self.onXBlockRefresh(xblockView);
......@@ -55,7 +57,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
},
refreshTitle: function() {
var title = this.$('.xblock-header .header-details span').first().text().trim();
var title = this.$('.xblock-header .header-details .xblock-display-name').first().text().trim();
this.$('.page-header-title').text(title);
this.$('.page-header .subtitle a').last().text(title);
},
......@@ -112,12 +114,16 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
buttonPanel = target.closest('.add-xblock-component'),
listPanel = buttonPanel.prev(),
scrollOffset = this.getScrollOffset(buttonPanel),
placeholderElement = $('<div></div>').appendTo(listPanel),
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').appendTo(listPanel),
requestData = _.extend(template, {
parent_locator: parentLocator
});
return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset));
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset))
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
});
},
duplicateComponent: function(xblockElement) {
......@@ -129,14 +135,18 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
this.runOperationShowingMessage(gettext('Duplicating&hellip;'),
function() {
var scrollOffset = self.getScrollOffset(xblockElement),
placeholderElement = $('<div></div>').insertAfter(xblockElement),
placeholderElement = $('<div class="studio-xblock-wrapper"></div>').insertAfter(xblockElement),
parentElement = self.findXBlockElement(parent),
requestData = {
duplicate_source_locator: xblockElement.data('locator'),
parent_locator: parentElement.data('locator')
};
return $.postJSON(self.getURLRoot() + '/', requestData,
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset));
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset))
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
});
});
},
......@@ -153,16 +163,21 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
url: self.getURLRoot() + "/" +
xblockElement.data('locator') + "?" +
$.param({recurse: true, all_versions: false})
}).success(function() {
// get the parent so we can remove this component from its parent.
var parent = self.findXBlockElement(xblockElement.parent());
xblockElement.remove();
self.xblockView.updateChildren(parent);
});
}).success(_.bind(self.onDelete, self, xblockElement));
});
});
},
onDelete: function(xblockElement) {
// get the parent so we can remove this component from its parent.
var xblockView = this.xblockView,
xblock = xblockView.xblock,
parent = this.findXBlockElement(xblockElement.parent());
xblockElement.remove();
xblockView.updateChildren(parent);
xblock.runtime.notify('deleted-child', parent.data('locator'));
},
onNewXBlock: function(xblockElement, scrollOffset, data) {
this.setScrollOffset(xblockElement, scrollOffset);
xblockElement.data('locator', data.locator);
......@@ -173,13 +188,14 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
* Refreshes the specified xblock's display. If the xblock is an inline child of a
* reorderable container then the element will be refreshed inline. If not, then the
* parent container will be refreshed instead.
* @param xblockElement The element representing the xblock to be refreshed.
* @param element An element representing the xblock to be refreshed.
*/
refreshXBlock: function(xblockElement) {
var parentElement = xblockElement.parent(),
refreshXBlock: function(element) {
var xblockElement = this.findXBlockElement(element),
parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({ });
this.render({refresh: true});
} else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement);
} else {
......
......@@ -233,7 +233,7 @@
@include transition(all $tmg-f3 linear 0s);
display: block;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
padding: 3px ($baseline/2);
color: $gray-l1;
&:hover {
......
......@@ -28,6 +28,11 @@
display: inline-block;
width: 50%;
vertical-align: middle;
.xblock-display-name {
display: inline-block;
vertical-align: middle;
}
}
.header-actions {
......@@ -147,47 +152,51 @@
padding: ($baseline/2) ($baseline*.75);
color: $white;
.message-text {
display: inline-block;
width: 93%;
vertical-align: top;
}
[class^="icon-"] {
font-style: normal;
}
&.information {
@extend %t-copy-sub1;
background-color: $gray-l5;
color: $gray-d2;
}
&.warning {
&.validation {
background-color: $gray-d2;
padding: ($baseline/2) $baseline;
color: $white;
.icon-warning-sign {
margin-right: ($baseline/2);
color: $orange;
a {
color: $blue-l2;
}
.message-text {
display: inline-block;
width: 93%;
vertical-align: top;
&.has-warnings {
border-bottom: 3px solid $orange;
.icon-warning-sign {
margin-right: ($baseline/2);
color: $orange;
}
}
}
&.error {
background-color: $gray-d2;
padding: ($baseline/2) $baseline;
color: $white;
&.has-errors {
border-bottom: 3px solid $red-l2;
.icon-exclamation-sign {
margin-right: ($baseline/2);
color: $red-l2;
.icon-exclamation-sign {
margin-right: ($baseline/2);
color: $red-l2;
}
}
}
}
.xblock-message-list {
margin-bottom: 0;
}
.xblock-message-actions {
@extend %actions-header;
padding: ($baseline/2) $baseline;
background-color: $gray-d1;
}
}
......@@ -64,7 +64,7 @@
.no-container-content {
@extend %ui-well;
padding: ($baseline*2);
padding: $baseline;
background-color: $gray-l4;
text-align: center;
color: $gray;
......@@ -78,7 +78,7 @@
@extend %t-action4;
padding: 8px 20px 10px;
text-align: center;
margin-left: $baseline;
margin: ($baseline/2) 0 ($baseline/2) $baseline;
[class^="icon-"] {
margin-right: ($baseline/2);
......@@ -158,6 +158,8 @@ body.view-container .content-primary {
// CASE: page level xblock rendering
&.level-page {
margin: 0;
box-shadow: none;
border: 0;
.xblock-header {
display: none;
......@@ -166,15 +168,36 @@ body.view-container .content-primary {
.xblock-message {
border-radius: 3px 3px 0 0;
&.validation {
padding-top: ($baseline*.75);
}
.xblock-message-list {
margin: ($baseline/5) ($baseline*2.5);
list-style-type: disc;
color: $gray-l3;
}
.xblock-message-item {
padding-bottom: ($baseline/4);
}
&.information {
@extend %t-copy-base;
margin-bottom: $baseline;
border-bottom: 1px solid $gray-l4;
padding: ($baseline/2) ($baseline*.75);
padding: 0 0 ($baseline/2) 0;
background-color: $gray-l5;
color: $gray-d1;
}
}
.no-container-content {
.xblock-message-list {
margin: 0;
list-style-type: none;
color: $gray-d2;
}
}
}
// CASE: nesting level xblock rendering
......@@ -250,6 +273,74 @@ body.view-container .content-primary {
display: none;
}
}
.wrapper-xblock-message {
.xblock-message {
border-radius: 0 0 3px 3px;
.xblock-message-list {
margin: 0;
list-style-type: none;
}
&.information {
@extend %t-copy-sub2;
padding: 0 0 ($baseline/2) $baseline;
color: $gray-l1;
}
&.validation.has-warnings {
border: 0;
border-top: 3px solid $orange;
}
&.validation.has-errors {
border: 0;
border-top: 3px solid $red-l2;
}
}
}
}
}
// groups in experiments
.wrapper-groups {
.title {
@extend %t-title7;
margin-left: ($baseline/2);
color: $gray-l1;
}
&.is-active {
// Don't show delete buttons on active groups
.wrapper-xblock.level-nesting > .xblock-header .action-delete {
display: none;
}
}
&.is-inactive {
margin: ($baseline*1.5) 0 0 0;
border-top: 2px dotted $gray-l2;
padding: ($baseline*.75) 0;
background-color: $gray-l4;
.wrapper-xblock.level-nesting {
@include transition(all $tmg-f2 linear 0s);
opacity: .7;
&:hover {
opacity: 1;
}
}
.new-component-item {
display: none;
}
}
}
......
......@@ -203,6 +203,16 @@ body.course.unit,
padding-top: 0;
color: $gray-l1;
}
&.has-warnings {
border: 0;
border-top: 3px solid $orange;
}
&.has-errors {
border: 0;
border-top: 3px solid $red-l2;
}
}
}
......
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span>Test Container</span>
<span class="xblock-display-name">Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......@@ -22,7 +22,7 @@
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span>Group A</span>
<span class="xblock-display-name">Group A</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......@@ -131,7 +131,7 @@
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span>Group B</span>
<span class="xblock-display-name">Group B</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
......@@ -4,7 +4,7 @@
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span>Empty Vertical Test</span>
<span class="xblock-display-name">Empty Vertical Test</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span>Updated Test Container</span>
<span class="xblock-display-name">Updated Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
<li class="studio-xblock-wrapper is-draggable">
<header class="xblock-header">
<div class="header-details">
<span>Mock XBlock</span>
<span class="xblock-display-name">Mock XBlock</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
<li class="studio-xblock-wrapper is-draggable">
<header class="xblock-header">
<div class="header-details">
<span>Mock XBlock</span>
<span class="xblock-display-name">Mock XBlock</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
......@@ -8,7 +8,7 @@
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">${_('Expand or Collapse')}</span>
</a>
<span>${xblock.display_name_with_default | h}</span>
<span class="xblock-display-name">${xblock.display_name_with_default | h}</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......
......@@ -20,7 +20,7 @@ collapsible_class = "is-collapsible" if xblock.has_children else ""
<section class="wrapper-xblock ${section_class} ${collapsible_class}" data-course-key="${xblock.location.course_key}">
% endif
<header class="xblock-header">
<header class="xblock-header xblock-header-${xblock.category}">
<div class="xblock-header-primary">
<div class="header-details">
% if show_inline:
......@@ -29,7 +29,7 @@ collapsible_class = "is-collapsible" if xblock.has_children else ""
<span class="sr">${_('Expand or Collapse')}</span>
</a>
% endif
<span>${xblock.display_name_with_default | h}</span>
<span class="xblock-display-name">${xblock.display_name_with_default | h}</span>
</div>
<div class="header-actions">
<ul class="actions-list">
......@@ -47,13 +47,13 @@ collapsible_class = "is-collapsible" if xblock.has_children else ""
<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>
% endif
<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>
% if is_reorderable:
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
......
<%! from django.utils.translation import ugettext as _ %>
<%include file="metadata-edit.html" />
% if disable_user_partition_editing:
<div class="message setting-message">
% if not selected_partition:
<p>${_("This content experiment refers to a group configuration that has been deleted.")}</p>
% else:
<p>${_("This content experiment uses group configuration '{0}'.".format("<strong>"+str(selected_partition.name)+"</strong>"))}</p>
% endif
<p class="tip setting-help">${_("After you select the group configuration and save the content experiment, you cannot change this setting.")}</p>
</div>
% endif
......@@ -15,8 +15,7 @@ from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule
from lms.lib.xblock.runtime import quote_slashes
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule, PREVIEW_VIEWS, STUDIO_VIEW
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.modulestore.django import modulestore
......@@ -60,10 +59,10 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
css_classes = ['xblock', 'xblock-' + view]
if isinstance(block, (XModule, XModuleDescriptor)):
if view == 'student_view':
if view in PREVIEW_VIEWS:
# The block is acting as an XModule
css_classes.append('xmodule_display')
elif view == 'studio_view':
elif view == STUDIO_VIEW:
# The block is acting as an XModuleDescriptor
css_classes.append('xmodule_edit')
......
......@@ -8,7 +8,7 @@ from lazy import lazy
from lxml import etree
from pkg_resources import resource_string
from xmodule.x_module import XModule
from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.seq_module import SequenceDescriptor
from xblock.fields import Scope, ReferenceList
from xmodule.modulestore.exceptions import ItemNotFoundError
......@@ -160,7 +160,7 @@ class ConditionalModule(ConditionalFields, XModule):
context)
return json.dumps({'html': [html], 'message': bool(message)})
html = [child.render('student_view').content for child in self.get_display_items()]
html = [child.render(STUDENT_VIEW).content for child in self.get_display_items()]
return json.dumps({'html': html})
......
......@@ -13,7 +13,7 @@ from pkg_resources import resource_string
from lxml import etree
from xmodule.x_module import XModule
from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.raw_module import RawDescriptor
from xblock.fields import Scope, String, Integer, Boolean, Dict, List
......@@ -113,7 +113,7 @@ class CrowdsourceHinterModule(CrowdsourceHinterFields, XModule):
try:
child = self.get_display_items()[0]
out = child.render('student_view').content
out = child.render(STUDENT_VIEW).content
# The event listener uses the ajax url to find the child.
child_id = child.id
except IndexError:
......
.setting-message {
margin: ($baseline/2) $baseline;
border-top: 3px solid $gray-l2;
background-color: $gray-l5;
padding: $baseline;
}
.setting-help {
@include font-size(12);
font-color: $gray-l6;
}
/* JavaScript for editing operations that can be done on the split test author view. */
window.SplitTestAuthorView = function (runtime, element) {
var $element = $(element);
$element.find('.add-missing-groups-button').click(function () {
runtime.notify('save', {
state: 'start',
element: element,
message: gettext('Creating missing groups&hellip;')
});
$.post(runtime.handlerUrl(element, 'add_missing_groups')).done(function() {
runtime.notify('save', {
state: 'end',
element: element
});
});
});
// Listen to delete events so that the view can refresh when the last inactive group is removed.
runtime.listenTo('deleted-child', function(parentLocator) {
var splitTestLocator = $element.closest('.studio-xblock-wrapper').data('locator'),
inactiveGroups = $element.find('.is-inactive .studio-xblock-wrapper');
if (splitTestLocator === parentLocator && inactiveGroups.length === 0) {
runtime.refreshXBlock($element);
}
});
return {};
};
import logging
import random
from xmodule.x_module import XModule
from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.seq_module import SequenceDescriptor
from lxml import etree
......@@ -83,7 +83,7 @@ class RandomizeModule(RandomizeFields, XModule):
# raise error instead? In fact, could complain on descriptor load...
return Fragment(content=u"<div>Nothing to randomize between</div>")
return self.child.render('student_view', context)
return self.child.render(STUDENT_VIEW, context)
def get_icon_class(self):
return self.child.get_icon_class() if self.child else 'other'
......
......@@ -11,7 +11,7 @@ from .exceptions import NotFoundError
from .fields import Date
from .mako_module import MakoModuleDescriptor
from .progress import Progress
from .x_module import XModule
from .x_module import XModule, STUDENT_VIEW
from .xml_module import XmlDescriptor
log = logging.getLogger(__name__)
......@@ -85,7 +85,7 @@ class SequenceModule(SequenceFields, XModule):
for child in self.get_display_items():
progress = child.get_progress()
rendered_child = child.render('student_view', context)
rendered_child = child.render(STUDENT_VIEW, context)
fragment.add_frag_resources(rendered_child)
titles = child.get_content_titles()
......
"""
Mixin to support editing in Studio.
"""
from xmodule.x_module import module_attr, STUDENT_VIEW, AUTHOR_VIEW
class StudioEditableModule(object):
"""
Helper methods for supporting Studio editing of xblocks/xmodules.
Helper methods for supporting Studio editing of xmodules.
This class is only intended to be used with an XModule, as it assumes the existence of
self.descriptor and self.system.
"""
def render_children(self, context, fragment, can_reorder=False, can_add=False, view_name='student_view'):
def render_children(self, context, fragment, can_reorder=False, can_add=False):
"""
Renders the children of the module with HTML appropriate for Studio. If can_reorder is True,
then the children will be rendered to support drag and drop.
......@@ -22,7 +23,7 @@ class StudioEditableModule(object):
if can_reorder:
context['reorderable_items'].add(child.location)
child_module = self.system.get_module(child) # pylint: disable=E1101
rendered_child = child_module.render(view_name, context)
rendered_child = child_module.render(StudioEditableModule.get_preview_view_name(child_module), context)
fragment.add_frag_resources(rendered_child)
contents.append({
......@@ -36,3 +37,21 @@ class StudioEditableModule(object):
'can_add': can_add,
'can_reorder': can_reorder,
}))
@staticmethod
def get_preview_view_name(block):
"""
Helper method for getting preview view name (student_view or author_view) for a given module.
"""
return AUTHOR_VIEW if hasattr(block, AUTHOR_VIEW) else STUDENT_VIEW
class StudioEditableDescriptor(object):
"""
Helper mixin for supporting Studio editing of xmodules.
This class is only intended to be used with an XModule Descriptor. This class assumes that the associated
XModule will have an "author_view" method for returning an editable preview view of the module.
"""
author_view = module_attr(AUTHOR_VIEW)
has_author_view = True
......@@ -25,6 +25,7 @@ from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from opaque_keys.edx.locations import Location
from xmodule.tests import get_test_system, test_util_open_ended
from xmodule.progress import Progress
from xmodule.x_module import STUDENT_VIEW
from xmodule.tests.test_util_open_ended import (
DummyModulestore, TEST_STATE_SA_IN,
MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID,
......@@ -1041,7 +1042,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
self._handle_ajax("next_problem", {})
self.assertEqual(self._module().current_task_number, 0)
html = self._module().render('student_view').content
html = self._module().render(STUDENT_VIEW).content
self.assertIsInstance(html, basestring)
rubric = self._handle_ajax("get_combined_rubric", {})
......@@ -1098,7 +1099,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
# Move to the next step in the problem
self._handle_ajax("next_problem", {})
self.assertEqual(self._module().current_task_number, 1)
self._module().render('student_view')
self._module().render(STUDENT_VIEW)
# Try to get the rubric from the module
self._handle_ajax("get_combined_rubric", {})
......@@ -1131,7 +1132,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
self.assertEqual(module.current_task_number, 1)
# Get html and other data client will request
module.render('student_view')
module.render(STUDENT_VIEW)
self._handle_ajax("skip_post_assessment", {})
......@@ -1167,7 +1168,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
# Move to the next step in the problem
self._handle_ajax("next_problem", {})
self.assertEqual(self._module().current_task_number, 1)
self._module().render('student_view')
self._module().render(STUDENT_VIEW)
# Try to get the rubric from the module
self._handle_ajax("get_combined_rubric", {})
......@@ -1198,7 +1199,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
self.assertEqual(module.current_task_number, 1)
# Get html and other data client will request
module.render('student_view')
module.render(STUDENT_VIEW)
self._handle_ajax("skip_post_assessment", {})
......@@ -1268,7 +1269,7 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
self._handle_ajax("next_problem", {})
self.assertEqual(self._module().current_task_number, 0)
html = self._module().render('student_view').content
html = self._module().render(STUDENT_VIEW).content
self.assertIsInstance(html, basestring)
# Module should now be done
......
......@@ -11,6 +11,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, CourseLocationGenerator
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.tests import DATA_DIR, get_test_system, get_test_descriptor_system
from xmodule.x_module import STUDENT_VIEW
ORG = 'test_org'
......@@ -129,7 +130,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
modules = ConditionalFactory.create(self.test_system)
# because get_test_system returns the repr of the context dict passed to render_template,
# we reverse it here
html = modules['cond_module'].render('student_view').content
html = modules['cond_module'].render(STUDENT_VIEW).content
expected = modules['cond_module'].xmodule_runtime.render_template('conditional_ajax.html', {
'ajax_url': modules['cond_module'].xmodule_runtime.ajax_url,
'element_id': u'i4x-edX-conditional_test-conditional-SampleConditional',
......@@ -219,7 +220,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
print "module children: ", module.get_children()
print "module display items (children): ", module.get_display_items()
html = module.render('student_view').content
html = module.render(STUDENT_VIEW).content
print "html type: ", type(html)
print "html: ", html
html_expect = module.xmodule_runtime.render_template(
......
......@@ -8,6 +8,7 @@ import copy
from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.vertical_module import VerticalModule, VerticalDescriptor
from xmodule.x_module import STUDENT_VIEW
from xblock.field_data import DictFieldData
from xblock.fragment import Fragment
from xblock.core import XBlock
......@@ -245,7 +246,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
return [FakeChild()]
mock_module.get_display_items = fake_get_display_items
out_html = mock_module.render('student_view').content
out_html = mock_module.render(STUDENT_VIEW).content
self.assertTrue('This is supposed to be test html.' in out_html)
self.assertTrue('i4x://this/is/a/fake/id' in out_html)
......@@ -262,7 +263,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
return []
mock_module.get_display_items = fake_get_display_items
out_html = mock_module.render('student_view').content
out_html = mock_module.render(STUDENT_VIEW).content
self.assertTrue('Error in loading crowdsourced hinter' in out_html)
@unittest.skip("Needs to be finished.")
......@@ -273,7 +274,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
NOT WORKING RIGHT NOW
"""
mock_module = VerticalWithModulesFactory.create()
out_html = mock_module.render('student_view').content
out_html = mock_module.render(STUDENT_VIEW).content
self.assertTrue('Test numerical problem.' in out_html)
self.assertTrue('Another test numerical problem.' in out_html)
......
......@@ -6,7 +6,7 @@ from xmodule.tests import get_test_system
from xmodule.error_module import ErrorDescriptor, ErrorModule, NonStaffErrorDescriptor
from xmodule.modulestore.xml import CourseLocationGenerator
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from xmodule.x_module import XModuleDescriptor, XModule
from xmodule.x_module import XModuleDescriptor, XModule, STUDENT_VIEW
from mock import MagicMock, Mock, patch
from xblock.runtime import Runtime, IdReader
from xblock.field_data import FieldData
......@@ -39,7 +39,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
)
self.assertIsInstance(descriptor, ErrorDescriptor)
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, 'student_view').content
context_repr = self.system.render(descriptor, STUDENT_VIEW).content
self.assertIn(self.error_msg, context_repr)
self.assertIn(repr(self.valid_xml), context_repr)
......@@ -53,7 +53,7 @@ class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor, self.error_msg)
self.assertIsInstance(error_descriptor, ErrorDescriptor)
error_descriptor.xmodule_runtime = self.system
context_repr = self.system.render(error_descriptor, 'student_view').content
context_repr = self.system.render(error_descriptor, STUDENT_VIEW).content
self.assertIn(self.error_msg, context_repr)
self.assertIn(repr(descriptor), context_repr)
......@@ -80,7 +80,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
CourseLocationGenerator(self.course_id)
)
descriptor.xmodule_runtime = self.system
context_repr = self.system.render(descriptor, 'student_view').content
context_repr = self.system.render(descriptor, STUDENT_VIEW).content
self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(repr(self.valid_xml), context_repr)
......@@ -94,7 +94,7 @@ class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
descriptor, self.error_msg)
self.assertIsInstance(error_descriptor, ErrorDescriptor)
error_descriptor.xmodule_runtime = self.system
context_repr = self.system.render(error_descriptor, 'student_view').content
context_repr = self.system.render(error_descriptor, STUDENT_VIEW).content
self.assertNotIn(self.error_msg, context_repr)
self.assertNotIn(str(descriptor), context_repr)
......
......@@ -3,6 +3,7 @@ Tests for StudioEditableModule.
"""
from xmodule.tests.test_vertical import BaseVerticalModuleTest
from xmodule.x_module import AUTHOR_VIEW
class StudioEditableModuleTestCase(BaseVerticalModuleTest):
......@@ -12,7 +13,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest):
"""
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
'read_only': False,
......@@ -20,6 +20,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest):
}
# Both children of the vertical should be rendered as reorderable
self.module_system.render(self.vertical, 'student_view', context).content
self.module_system.render(self.vertical, AUTHOR_VIEW, context).content # pylint: disable=expression-not-assigned
self.assertIn(self.vertical.get_children()[0].location, reorderable_items)
self.assertIn(self.vertical.get_children()[1].location, reorderable_items)
......@@ -6,6 +6,7 @@ from fs.memoryfs import MemoryFS
from xmodule.tests import get_test_system
from xmodule.tests.xml import XModuleXmlImportTest
from xmodule.tests.xml import factories as xml
from xmodule.x_module import STUDENT_VIEW, AUTHOR_VIEW
class BaseVerticalModuleTest(XModuleXmlImportTest):
......@@ -46,30 +47,28 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
"""
Test the rendering of the student view.
"""
html = self.module_system.render(self.vertical, 'student_view', {}).content
html = self.module_system.render(self.vertical, STUDENT_VIEW, {}).content
self.assertIn(self.test_html_1, html)
self.assertIn(self.test_html_2, html)
def test_render_studio_view(self):
"""
Test the rendering of the Studio view
Test the rendering of the Studio author view
"""
# Vertical shouldn't render children on the unit page
context = {
'runtime_type': 'studio',
'container_view': False,
}
html = self.module_system.render(self.vertical, 'student_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_2, html)
# Vertical should render reorderable children on the container page
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
}
html = self.module_system.render(self.vertical, 'student_view', context).content
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
self.assertIn(self.test_html_1, html)
self.assertIn(self.test_html_2, html)
......@@ -26,7 +26,7 @@ from xblock.fields import ScopeIds
from opaque_keys.edx.locations import Location
from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem
from xmodule.x_module import ModuleSystem, XModule, XModuleDescriptor, DescriptorSystem, STUDENT_VIEW, STUDIO_VIEW
from xmodule.annotatable_module import AnnotatableDescriptor
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
......@@ -324,7 +324,7 @@ class TestStudentView(XBlockWrapperTestMixin, TestCase):
"""
self.assertEqual(
descriptor._xmodule.get_html(),
descriptor.render('student_view').content
descriptor.render(STUDENT_VIEW).content
)
......@@ -343,7 +343,7 @@ class TestStudioView(XBlockWrapperTestMixin, TestCase):
"""
Assert that studio_view and get_html render the same.
"""
self.assertEqual(descriptor.get_html(), descriptor.render('studio_view').content)
self.assertEqual(descriptor.get_html(), descriptor.render(STUDIO_VIEW).content)
class TestXModuleHandler(TestCase):
......
from xblock.fragment import Fragment
from xmodule.x_module import XModule
from xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.seq_module import SequenceDescriptor
from xmodule.progress import Progress
from xmodule.studio_editable import StudioEditableModule
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
from pkg_resources import resource_string
from copy import copy
......@@ -19,27 +19,6 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
''' Layout module for laying out submodules vertically.'''
def student_view(self, context):
# When rendering a Studio preview, use a different template to support drag and drop.
if context and context.get('runtime_type', None) == 'studio':
return self.studio_preview_view(context)
return self.render_view(context, 'vert_module.html')
def studio_preview_view(self, context):
"""
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
# For the container page we want the full drag-and-drop, but for unit pages we want
# a more concise version that appears alongside the "View =>" link.
if context.get('container_view'):
self.render_children(context, fragment, can_reorder=True, can_add=True)
return fragment
def render_view(self, context, template_name):
"""
Helper method for rendering student_view and the Studio version.
"""
fragment = Fragment()
contents = []
......@@ -47,7 +26,7 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
child_context['child_of_vertical'] = True
for child in self.get_display_items():
rendered_child = child.render('student_view', child_context)
rendered_child = child.render(STUDENT_VIEW, child_context)
fragment.add_frag_resources(rendered_child)
contents.append({
......@@ -55,12 +34,23 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
'content': rendered_child.content
})
fragment.add_content(self.system.render_template(template_name, {
fragment.add_content(self.system.render_template('vert_module.html', {
'items': contents,
'xblock_context': context,
}))
return fragment
def author_view(self, context):
"""
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
# For the container page we want the full drag-and-drop, but for unit pages we want
# a more concise version that appears alongside the "View =>" link.
if context.get('container_view'):
self.render_children(context, fragment, can_reorder=True, can_add=True)
return fragment
def get_progress(self):
# TODO: Cache progress or children array?
children = self.get_children()
......@@ -77,7 +67,10 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
return new_class
class VerticalDescriptor(VerticalFields, SequenceDescriptor):
class VerticalDescriptor(VerticalFields, SequenceDescriptor, StudioEditableDescriptor):
"""
Descriptor class for editing verticals.
"""
module_class = VerticalModule
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
......
......@@ -32,6 +32,25 @@ log = logging.getLogger(__name__)
XMODULE_METRIC_NAME = 'edxapp.xmodule'
# xblock view names
# This is the view that will be rendered to display the XBlock in the LMS.
# It will also be used to render the block in "preview" mode in Studio, unless
# the XBlock also implements author_view.
STUDENT_VIEW = 'student_view'
# An optional view of the XBlock similar to student_view, but with possible inline
# editing capabilities. This view differs from studio_view in that it should be as similar to student_view
# as possible. When previewing XBlocks within Studio, Studio will prefer author_view to student_view.
AUTHOR_VIEW = 'author_view'
# The view used to render an editor in Studio. The editor rendering can be completely different
# from the LMS student_view, and it is only shown when the author selects "Edit".
STUDIO_VIEW = 'studio_view'
# Views that present a "preview" view of an xblock (as opposed to an editing view).
PREVIEW_VIEWS = [STUDENT_VIEW, AUTHOR_VIEW]
class OpaqueKeyReader(IdReader):
"""
......@@ -934,7 +953,7 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
get_score = module_attr('get_score')
handle_ajax = module_attr('handle_ajax')
max_score = module_attr('max_score')
student_view = module_attr('student_view')
student_view = module_attr(STUDENT_VIEW)
get_child_descriptors = module_attr('get_child_descriptors')
xmodule_handler = module_attr('xmodule_handler')
......@@ -1138,7 +1157,7 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime): # p
return result
def render(self, block, view_name, context=None):
if view_name == 'student_view':
if view_name in PREVIEW_VIEWS:
assert block.xmodule_runtime is not None
if isinstance(block, (XModule, XModuleDescriptor)):
to_render = block._xmodule
......
......@@ -200,6 +200,7 @@ class CourseFixture(StudioApiFixture):
self._handouts = []
self._children = []
self._assets = []
self._advanced_settings = {}
def __str__(self):
"""
......@@ -236,6 +237,12 @@ class CourseFixture(StudioApiFixture):
"""
self._assets.extend(asset_name)
def add_advanced_settings(self, settings):
"""
Adds advanced settings to be set on the course when the install method is called.
"""
self._advanced_settings.update(settings)
def install(self):
"""
Create the course and XBlocks within the course.
......@@ -248,6 +255,7 @@ class CourseFixture(StudioApiFixture):
self._install_course_handouts()
self._configure_course()
self._upload_assets()
self._add_advanced_settings()
self._create_xblock_children(self._course_location, self._children)
return self
......@@ -415,6 +423,23 @@ class CourseFixture(StudioApiFixture):
raise CourseFixtureError('Could not upload {asset_name} with {url}. Status code: {code}'.format(
asset_name=asset_name, url=url, code=upload_response.status_code))
def _add_advanced_settings(self):
"""
Add advanced settings.
"""
url = STUDIO_BASE_URL + "/settings/advanced/" + self._course_key
# POST advanced settings to Studio
response = self.session.post(
url, data=self._encode_post_dict(self._advanced_settings),
headers=self.headers,
)
if not response.ok:
raise CourseFixtureError(
"Could not update advanced details to '{0}' with {1}: Status was {2}.".format(
self._advanced_settings, url, response.status_code))
def _create_xblock_children(self, parent_loc, xblock_descriptions):
"""
Recursively create XBlock children.
......@@ -489,6 +514,6 @@ class CourseFixture(StudioApiFixture):
Encode `post_dict` (a dictionary) as UTF-8 encoded JSON.
"""
return json.dumps({
k: v.encode('utf-8') if v is not None else v
k: v.encode('utf-8') if isinstance(v, basestring) else v
for k, v in post_dict.items()
})
......@@ -2,6 +2,7 @@ from bok_choy.page_object import PageObject
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from utils import click_css
from selenium.webdriver.support.ui import Select
class ComponentEditorView(PageObject):
......@@ -40,7 +41,7 @@ class ComponentEditorView(PageObject):
"""
return None
def get_setting_entry_index(self, label):
def get_setting_element(self, label):
"""
Returns the index of the setting entry with given label (display name) within the Settings modal.
"""
......@@ -48,15 +49,14 @@ class ComponentEditorView(PageObject):
setting_labels = self.q(css=self._bounded_selector('.metadata_edit .wrapper-comp-setting .setting-label'))
for index, setting in enumerate(setting_labels):
if setting.text == label:
return index
return self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting .setting-input'))[index]
return None
def set_field_value_and_save(self, label, value):
"""
Set the field with given label (display name) to the specified value, and presses Save.
Sets the text field with given label (display name) to the specified value, and presses Save.
"""
index = self.get_setting_entry_index(label)
elem = self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting input.setting-input'))[index]
elem = self.get_setting_element(label)
# Click in the field, delete the value there.
action = ActionChains(self.browser).click(elem)
for _x in range(0, len(elem.get_attribute('value'))):
......@@ -64,3 +64,12 @@ class ComponentEditorView(PageObject):
# Send the new text, then Tab to move to the next field (so change event is triggered).
action.send_keys(value).send_keys(Keys.TAB).perform()
click_css(self, 'a.action-save')
def set_select_value_and_save(self, label, value):
"""
Sets the select with given label (display name) to the specified value, and presses Save.
"""
elem = self.get_setting_element(label)
select = Select(elem)
select.select_by_value(value)
click_css(self, 'a.action-save')
......@@ -54,7 +54,24 @@ class ContainerPage(PageObject):
"""
Return a list of xblocks loaded on the container page.
"""
return self.q(css=XBlockWrapper.BODY_SELECTOR).map(
return self._get_xblocks()
@property
def inactive_xblocks(self):
"""
Return a list of inactive xblocks loaded on the container page.
"""
return self._get_xblocks(".is-inactive ")
@property
def active_xblocks(self):
"""
Return a list of active xblocks loaded on the container page.
"""
return self._get_xblocks(".is-active ")
def _get_xblocks(self, prefix=""):
return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map(
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results
def drag(self, source_index, target_index):
......@@ -77,15 +94,6 @@ class ContainerPage(PageObject):
).release().perform()
wait_for_notification(self)
def add_discussion(self, menu_index):
"""
Add a new instance of the discussion category.
menu_index specifies which instance of the menus should be used (based on vertical
placement within the page).
"""
click_css(self, 'a>span.large-discussion-icon', menu_index)
def duplicate(self, source_index):
"""
Duplicate the item with index source_index (based on vertical placement in page).
......@@ -101,6 +109,11 @@ class ContainerPage(PageObject):
click_css(self, 'a.button.action-primary', 0)
def edit(self):
"""
Clicks the "edit" button for the first component on the page.
Same as the implementation in unit.py, unit and component pages will be merging.
"""
self.q(css='.edit-button').first.click()
EmptyPromise(
lambda: self.q(css='.xblock-studio_view').present,
......@@ -109,6 +122,18 @@ class ContainerPage(PageObject):
return self
def add_missing_groups(self):
"""
Click the "add missing groups" link.
"""
click_css(self, '.add-missing-groups-button')
def missing_groups_button_present(self):
"""
Returns True if the "add missing groups" button is present.
"""
return self.q(css='.add-missing-groups-button').present
class XBlockWrapper(PageObject):
"""
......@@ -161,4 +186,4 @@ class XBlockWrapper(PageObject):
@property
def preview_selector(self):
return self._bounded_selector('.xblock-student_view')
return self._bounded_selector('.xblock-student_view,.xblock-author_view')
......@@ -27,7 +27,7 @@ class UnitPage(PageObject):
def _is_finished_loading():
# Wait until all components have been loaded
number_of_leaf_xblocks = len(self.q(css='{} .xblock-student_view'.format(Component.BODY_SELECTOR)).results)
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)
......@@ -99,9 +99,14 @@ class Component(PageObject):
@property
def preview_selector(self):
return self._bounded_selector('.xblock-student_view')
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,
......
......@@ -9,10 +9,12 @@ def click_css(page, css, source_index=0, require_notification=True):
"""
Click the button/link with the given css and index on the specified page (subclass of PageObject).
Will only consider buttons with a non-zero size.
If require_notification is False (default value is True), the method will return immediately.
Otherwise, it will wait for the "mini-notification" to appear and disappear.
"""
buttons = page.q(css=css)
buttons = page.q(css=css).filter(lambda el: el.size['width'] > 0)
target = buttons[source_index]
ActionChains(page.browser).click(target).release().perform()
if require_notification:
......@@ -31,5 +33,36 @@ def wait_for_notification(page):
num_notifications = len(page.q(css='.wrapper-notification-mini.is-hiding'))
return (num_notifications == 1, num_notifications)
Promise(_is_saving, 'Notification showing.').fulfill()
Promise(_is_saving_done, 'Notification hidden.').fulfill()
Promise(_is_saving, 'Notification should have been shown.').fulfill()
Promise(_is_saving_done, 'Notification should have been hidden.').fulfill()
def add_discussion(page, menu_index):
"""
Add a new instance of the discussion category.
menu_index specifies which instance of the menus should be used (based on vertical
placement within the page).
"""
click_css(page, 'a>span.large-discussion-icon', menu_index)
def add_advanced_component(page, menu_index, name):
"""
Adds an instance of the advanced component with the specified name.
menu_index specifies which instance of the menus should be used (based on vertical
placement within the page).
"""
click_css(page, 'a>span.large-advanced-icon', menu_index, require_notification=False)
# Sporadically, the advanced component was not getting created after the click_css call on the category (below).
# Try making sure that the menu of advanced components is visible before clicking (the HTML is always on the
# page, but will have display none until the large-advanced-icon is clicked).
def is_advanced_components_showing():
advanced_buttons = page.q(css=".new-component-advanced").filter(lambda el: el.size['width'] > 0)
return (len(advanced_buttons) == 1, len(advanced_buttons))
Promise(is_advanced_components_showing, "Advanced component menu not showing").fulfill()
click_css(page, 'a[data-category={}]'.format(name))
......@@ -8,6 +8,7 @@ from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
from ..pages.studio.component_editor import ComponentEditorView
from ..pages.studio.utils import add_discussion
from unittest import skip
......@@ -32,6 +33,87 @@ class ContainerBase(UniqueCourseTest):
self.course_info['run']
)
self.setup_fixtures()
self.auth_page = AutoAuthPage(
self.browser,
staff=False,
username=self.user.get('username'),
email=self.user.get('email'),
password=self.user.get('password')
)
self.auth_page.visit()
def setup_fixtures(self):
pass
def go_to_container_page(self, make_draft=False):
"""
Go to the test container page.
If make_draft is true, the unit page (accessed on way to container page) will be put into draft mode.
"""
unit = self.go_to_unit_page(make_draft)
container = unit.components[0].go_to_container()
return container
def go_to_unit_page(self, make_draft=False):
"""
Go to the test unit page.
If make_draft is true, the unit page will be put into draft mode.
"""
self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to()
if make_draft:
unit.edit_draft()
return unit
def verify_ordering(self, container, expected_orderings):
"""
Verifies the expected ordering of xblocks on the page.
"""
xblocks = container.xblocks
blocks_checked = set()
for expected_ordering in expected_orderings:
for xblock in xblocks:
parent = expected_ordering.keys()[0]
if xblock.name == parent:
blocks_checked.add(parent)
children = xblock.children
expected_length = len(expected_ordering.get(parent))
self.assertEqual(
expected_length, len(children),
"Number of children incorrect for group {0}. Expected {1} but got {2}.".format(parent, expected_length, len(children)))
for idx, expected in enumerate(expected_ordering.get(parent)):
self.assertEqual(expected, children[idx].name)
blocks_checked.add(expected)
break
self.assertEqual(len(blocks_checked), len(xblocks))
def do_action_and_verify(self, action, expected_ordering):
"""
Perform the supplied action and then verify the resulting ordering.
"""
container = self.go_to_container_page(make_draft=True)
action(container)
self.verify_ordering(container, expected_ordering)
# Reload the page to see that the change was persisted.
container = self.go_to_container_page()
self.verify_ordering(container, expected_ordering)
class NestedVerticalTest(ContainerBase):
__test__ = False
"""
Sets up a course structure with nested verticals.
"""
def setup_fixtures(self):
self.container_title = ""
self.group_a = "Expand or Collapse\nGroup A"
self.group_b = "Expand or Collapse\nGroup B"
......@@ -55,18 +137,6 @@ class ContainerBase(UniqueCourseTest):
self.duplicate_label = "Duplicate of '{0}'"
self.discussion_label = "Discussion"
self.setup_fixtures()
self.auth_page = AutoAuthPage(
self.browser,
staff=False,
username=self.user.get('username'),
email=self.user.get('email'),
password=self.user.get('password')
)
self.auth_page.visit()
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
......@@ -96,46 +166,8 @@ class ContainerBase(UniqueCourseTest):
self.user = course_fix.user
def go_to_container_page(self, make_draft=False):
unit = self.go_to_unit_page(make_draft)
container = unit.components[0].go_to_container()
return container
def go_to_unit_page(self, make_draft=False):
self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to()
if make_draft:
unit.edit_draft()
return unit
def verify_ordering(self, container, expected_orderings):
xblocks = container.xblocks
for expected_ordering in expected_orderings:
for xblock in xblocks:
parent = expected_ordering.keys()[0]
if xblock.name == parent:
children = xblock.children
expected_length = len(expected_ordering.get(parent))
self.assertEqual(
expected_length, len(children),
"Number of children incorrect for group {0}. Expected {1} but got {2}.".format(parent, expected_length, len(children)))
for idx, expected in enumerate(expected_ordering.get(parent)):
self.assertEqual(expected, children[idx].name)
break
def do_action_and_verify(self, action, expected_ordering):
container = self.go_to_container_page(make_draft=True)
action(container)
self.verify_ordering(container, expected_ordering)
# Reload the page to see that the change was persisted.
container = self.go_to_container_page()
self.verify_ordering(container, expected_ordering)
class DragAndDropTest(ContainerBase):
class DragAndDropTest(NestedVerticalTest):
"""
Tests of reordering within the container page.
"""
......@@ -196,7 +228,7 @@ class DragAndDropTest(ContainerBase):
def add_new_components_and_rearrange(container):
# Add a video component to Group 1
container.add_discussion(group_a_menu)
add_discussion(container, group_a_menu)
# Duplicate the first item in Group A
container.duplicate(self.group_a_item_1_action_index)
......@@ -216,7 +248,7 @@ class DragAndDropTest(ContainerBase):
self.do_action_and_verify(add_new_components_and_rearrange, expected_ordering)
class AddComponentTest(ContainerBase):
class AddComponentTest(NestedVerticalTest):
"""
Tests of adding a component to the container page.
"""
......@@ -224,7 +256,7 @@ class AddComponentTest(ContainerBase):
def add_and_verify(self, menu_index, expected_ordering):
self.do_action_and_verify(
lambda (container): container.add_discussion(menu_index),
lambda (container): add_discussion(container, menu_index),
expected_ordering
)
......@@ -256,7 +288,7 @@ class AddComponentTest(ContainerBase):
self.add_and_verify(container_menu, expected_ordering)
class DuplicateComponentTest(ContainerBase):
class DuplicateComponentTest(NestedVerticalTest):
"""
Tests of duplicating a component on the container page.
"""
......@@ -302,7 +334,7 @@ class DuplicateComponentTest(ContainerBase):
self.do_action_and_verify(duplicate_twice, expected_ordering)
class DeleteComponentTest(ContainerBase):
class DeleteComponentTest(NestedVerticalTest):
"""
Tests of deleting a component from the container page.
"""
......@@ -319,10 +351,13 @@ class DeleteComponentTest(ContainerBase):
{self.group_a: [self.group_a_item_2]},
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
{self.group_empty: []}]
self.delete_and_verify(self.group_a_item_1_action_index, expected_ordering)
# Group A itself has a delete icon now, so item_1 is index 1 instead of 0.
group_a_item_1_delete_index = 1
self.delete_and_verify(group_a_item_1_delete_index, expected_ordering)
class EditContainerTest(ContainerBase):
class EditContainerTest(NestedVerticalTest):
"""
Tests of editing a container.
"""
......
"""
Acceptance tests for Studio related to the split_test module.
"""
from unittest import skip
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from ..pages.studio.component_editor import ComponentEditorView
from test_studio_container import ContainerBase
from ..pages.studio.utils import add_advanced_component
from xmodule.partitions.partitions import Group, UserPartition
from bok_choy.promise import Promise
class SplitTest(ContainerBase):
"""
Tests for creating and editing split test instances in Studio.
"""
__test__ = True
def setup_fixtures(self):
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
course_fix.add_advanced_settings(
{
u"advanced_modules": ["split_test"],
u"user_partitions": [
UserPartition(0, 'Configuration alpha,beta', 'first', [Group("0", 'alpha'), Group("1", 'beta')]).to_json(),
UserPartition(1, 'Configuration 0,1,2', 'second', [Group("0", 'Group 0'), Group("1", 'Group 1'), Group("2", 'Group 2')]).to_json()
]
}
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit')
)
)
).install()
self.course_fix = course_fix
self.user = course_fix.user
def verify_groups(self, container, active_groups, inactive_groups, verify_missing_groups_not_present=True):
"""
Check that the groups appear and are correctly categorized as to active and inactive.
Also checks that the "add missing groups" button/link is not present unless a value of False is passed
for verify_missing_groups_not_present.
"""
def wait_for_xblocks_to_render():
# First xblock is the container for the page, subtract 1.
return (len(active_groups) + len(inactive_groups) == len(container.xblocks) - 1, len(active_groups))
Promise(wait_for_xblocks_to_render, "Number of xblocks on the page are incorrect").fulfill()
def check_xblock_names(expected_groups, actual_blocks):
self.assertEqual(len(expected_groups), len(actual_blocks))
for idx, expected in enumerate(expected_groups):
self.assertEqual('Expand or Collapse\n{}'.format(expected), actual_blocks[idx].name)
check_xblock_names(active_groups, container.active_xblocks)
check_xblock_names(inactive_groups, container.inactive_xblocks)
# Verify inactive xblocks appear after active xblocks
check_xblock_names(active_groups + inactive_groups, container.xblocks[1:])
if verify_missing_groups_not_present:
self.verify_add_missing_groups_button_not_present(container)
def verify_add_missing_groups_button_not_present(self, container):
"""
Checks that the "add missing gorups" button/link is not present.
"""
def missing_groups_button_not_present():
button_present = container.missing_groups_button_present()
return (not button_present, not button_present)
Promise(missing_groups_button_not_present, "Add missing groups button should not be showing.").fulfill()
def create_poorly_configured_split_instance(self):
"""
Creates a split test instance with a missing group and an inactive group.
Returns the container page.
"""
unit = self.go_to_unit_page(make_draft=True)
add_advanced_component(unit, 0, 'split_test')
container = self.go_to_container_page()
container.edit()
component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
self.course_fix.add_advanced_settings(
{
u"user_partitions": [
UserPartition(0, 'Configuration alpha,beta', 'first',
[Group("0", 'alpha'), Group("2", 'gamma')]).to_json()
]
}
)
self.course_fix._add_advanced_settings()
return self.go_to_container_page()
def test_create_and_select_group_configuration(self):
"""
Tests creating a split test instance on the unit page, and then
assigning the group configuration.
"""
unit = self.go_to_unit_page(make_draft=True)
add_advanced_component(unit, 0, 'split_test')
container = self.go_to_container_page()
container.edit()
component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
self.verify_groups(container, ['alpha', 'beta'], [])
# Switch to the other group configuration. Must navigate again to the container page so
# that there is only a single "editor" on the page.
container = self.go_to_container_page()
container.edit()
component_editor = ComponentEditorView(self.browser, container.locator)
component_editor.set_select_value_and_save('Group Configuration', 'Configuration 0,1,2')
self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
# Reload the page to make sure the groups were persisted.
container = self.go_to_container_page()
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")
def test_missing_group(self):
"""
The case of a split test with invalid configuration (missing group).
"""
container = self.create_poorly_configured_split_instance()
container.add_missing_groups()
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
# Reload the page to make sure the groups were persisted.
container = self.go_to_container_page()
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
def test_delete_inactive_group(self):
"""
Test deleting an inactive group.
"""
container = self.create_poorly_configured_split_instance()
container.delete(0)
self.verify_groups(container, ['alpha'], [], verify_missing_groups_not_present=False)
......@@ -40,8 +40,9 @@ Class Features
These are class attributes or functions that can be provided by an XBlock to customize behaviour
in the LMS.
* student_view (XBlock view): This is the view that will be rendered to display
the XBlock in the LMS.
* student_view (XBlock view): This is the view that will be rendered to display the XBlock
in the LMS. It will also be used to render the block in "preview" mode in Studio, unless
the XBlock also implements author_view.
* has_score (class property): True if this block should appear in the LMS progress page.
* get_progress (method): See documentation in x_module.py:XModuleMixin.get_progress.
* icon_class (class property): This can be one of (``other``, ``video``, or ``problem``), and
......@@ -77,7 +78,11 @@ Studio
Class Features
~~~~~~~~~~~~~~
* studio_view (XBlock.view): The view used to render an editor in Studio.
* studio_view (XBlock.view): The view used to render an editor in Studio. The editor rendering can
be completely different from the LMS student_view, and it is only shown when the author selects "Edit".
* author_view (XBlock.view): An optional view of the XBlock similar to student_view, but with possible inline
editing capabilities. This view differs from studio_view in that it should be as similar to student_view
as possible. When previewing XBlocks within Studio, Studio will prefer author_view to student_view.
* non_editable_metadata_fields (property): A list of :class:`~xblock.fields.Field` objects that
shouldn't be displayed in the default editing view for Studio.
......
......@@ -15,6 +15,7 @@ from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_static_urls
from xmodule.modulestore import MONGO_MODULESTORE_TYPE
from xmodule.x_module import STUDENT_VIEW
from courseware.access import has_access
from courseware.model_data import FieldDataCache
......@@ -196,7 +197,7 @@ def get_course_about_section(course, section_key):
if about_module is not None:
try:
html = about_module.render('student_view').content
html = about_module.render(STUDENT_VIEW).content
except Exception: # pylint: disable=broad-except
html = render_to_string('courseware/error-message.html', None)
log.exception(
......@@ -250,7 +251,7 @@ def get_course_info_section(request, course, section_key):
if info_module is not None:
try:
html = info_module.render('student_view').content
html = info_module.render(STUDENT_VIEW).content
except Exception: # pylint: disable=broad-except
html = render_to_string('courseware/error-message.html', None)
log.exception(
......
......@@ -12,6 +12,7 @@ from django.conf import settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.x_module import STUDENT_VIEW
from courseware.tests import BaseTestXmodule
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
......@@ -108,7 +109,7 @@ class TestLTI(BaseTestXmodule):
self.addCleanup(patcher.stop)
def test_lti_constructor(self):
generated_content = self.item_descriptor.render('student_view').content
generated_content = self.item_descriptor.render(STUDENT_VIEW).content
expected_content = self.runtime.render_template('lti.html', self.expected_context)
self.assertEqual(generated_content, expected_content)
......
......@@ -22,7 +22,7 @@ from xmodule.lti_module import LTIDescriptor
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import ItemFactory, CourseFactory
from xmodule.x_module import XModuleDescriptor
from xmodule.x_module import XModuleDescriptor, STUDENT_VIEW
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware import module_render as render
......@@ -94,7 +94,7 @@ class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase):
)
# get the rendered HTML output which should have the rewritten link
html = module.render('student_view').content
html = module.render(STUDENT_VIEW).content
# See if the url got rewritten to the target link
# note if the URL mapping changes then this assertion will break
......@@ -416,7 +416,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.course.id,
wrap_xmodule_display=True,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn('div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"', result_fragment.content)
......@@ -429,7 +429,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.course.id,
wrap_xmodule_display=False,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertNotIn('div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"', result_fragment.content)
......@@ -441,7 +441,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn(
'/c4x/{org}/{course}/asset/foo_content'.format(
......@@ -459,7 +459,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn(
'/c4x/{org}/{course}/asset/_file.jpg'.format(
......@@ -483,7 +483,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.course.id,
static_asset_path="toy_course_dir",
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn('href="/static/toy_course_dir', result_fragment.content)
def test_course_image(self):
......@@ -509,7 +509,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn(
'/courses/{course_id}/bar/content'.format(
......@@ -590,14 +590,14 @@ class MongoViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_studio_course(self):
"""Regular Studio courses should see 'View in Studio' links."""
self.setup_mongo_course()
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_only_in_top_level_vertical(self):
"""Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
self.setup_mongo_course()
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
# The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
parts = result_fragment.content.split('xmodule_VerticalModule')
self.assertEqual(3, len(parts), "Did not find two vertical modules")
......@@ -608,7 +608,7 @@ class MongoViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_xml_authored(self):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self.setup_mongo_course(course_edit_method='XML')
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertNotIn('View Unit in Studio', result_fragment.content)
......@@ -622,19 +622,19 @@ class MixedViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_mongo_backed(self):
"""Mixed mongo courses that are mongo backed should see 'View in Studio' links."""
self.setup_mongo_course()
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_xml_authored(self):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self.setup_mongo_course(course_edit_method='XML')
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertNotIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_xml_backed(self):
"""Course in XML only modulestore should not see 'View in Studio' links."""
self.setup_xml_course()
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertNotIn('View Unit in Studio', result_fragment.content)
......@@ -648,7 +648,7 @@ class XmlViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_xml_backed(self):
"""Course in XML only modulestore should not see 'View in Studio' links."""
self.setup_xml_course()
result_fragment = self.module.render('student_view')
result_fragment = self.module.render(STUDENT_VIEW)
self.assertNotIn('View Unit in Studio', result_fragment.content)
......@@ -694,7 +694,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertNotIn('Staff Debug', result_fragment.content)
def test_staff_debug_info_enabled(self):
......@@ -705,7 +705,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertIn('Staff Debug', result_fragment.content)
@patch.dict('django.conf.settings.FEATURES', {'DISPLAY_HISTOGRAMS_TO_STAFF': False})
......@@ -717,7 +717,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
result_fragment = module.render('student_view')
result_fragment = module.render(STUDENT_VIEW)
self.assertNotIn('histrogram', result_fragment.content)
def test_histogram_enabled_for_unscored_xmodules(self):
......@@ -741,7 +741,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
field_data_cache,
self.course.id,
)
module.render('student_view')
module.render(STUDENT_VIEW)
self.assertFalse(mock_grade_histogram.called)
def test_histogram_enabled_for_scored_xmodules(self):
......@@ -764,7 +764,7 @@ class TestStaffDebugInfo(ModuleStoreTestCase):
self.field_data_cache,
self.course.id,
)
module.render('student_view')
module.render(STUDENT_VIEW)
self.assertTrue(mock_grade_histogram.called)
......
......@@ -12,6 +12,7 @@ from webob import Request
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import editable_modulestore
from xmodule.x_module import STUDENT_VIEW
from . import BaseTestXmodule
from .test_video_xml import SOURCE_XML
from cache_toolbox.core import del_cached_content
......@@ -175,7 +176,7 @@ class TestTranscriptAvailableTranslationsDispatch(TestVideo):
def setUp(self):
super(TestTranscriptAvailableTranslationsDispatch, self).setUp()
self.item_descriptor.render('student_view')
self.item_descriptor.render(STUDENT_VIEW)
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
self.subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]}
......@@ -234,7 +235,7 @@ class TestTranscriptDownloadDispatch(TestVideo):
def setUp(self):
super(TestTranscriptDownloadDispatch, self).setUp()
self.item_descriptor.render('student_view')
self.item_descriptor.render(STUDENT_VIEW)
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
def test_download_transcript_not_exist(self):
......@@ -299,7 +300,7 @@ class TestTranscriptTranslationGetDispatch(TestVideo):
def setUp(self):
super(TestTranscriptTranslationGetDispatch, self).setUp()
self.item_descriptor.render('student_view')
self.item_descriptor.render(STUDENT_VIEW)
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
def test_translation_fails(self):
......@@ -609,7 +610,7 @@ class TestGetTranscript(TestVideo):
def setUp(self):
super(TestGetTranscript, self).setUp()
self.item_descriptor.render('student_view')
self.item_descriptor.render(STUDENT_VIEW)
self.item = self.item_descriptor.xmodule_runtime.xmodule_instance
def test_good_transcript(self):
......
......@@ -13,6 +13,7 @@ from xblock.field_data import DictFieldData
from xmodule.video_module import create_youtube_string
from xmodule.tests import get_test_descriptor_system
from xmodule.video_module import VideoDescriptor
from xmodule.x_module import STUDENT_VIEW
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from . import BaseTestXmodule
......@@ -25,7 +26,7 @@ class TestVideoYouTube(TestVideo):
def test_video_constructor(self):
"""Make sure that all parameters extracted correctly from xml"""
context = self.item_descriptor.render('student_view').content
context = self.item_descriptor.render(STUDENT_VIEW).content
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = {
......@@ -89,7 +90,7 @@ class TestVideoNonYouTube(TestVideo):
"""Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams.
"""
context = self.item_descriptor.render('student_view').content
context = self.item_descriptor.render(STUDENT_VIEW).content
sources = json.dumps([u'example.mp4', u'example.webm'])
expected_context = {
......@@ -231,7 +232,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
self.item_descriptor, 'transcript', 'download'
).rstrip('/?')
context = self.item_descriptor.render('student_view').content
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context.update({
'transcript_download_format': None if self.item_descriptor.track and self.item_descriptor.download_track else 'srt',
......@@ -344,7 +345,7 @@ class TestGetHtmlMethod(BaseTestXmodule):
sources=data['sources']
)
self.initialize_module(data=DATA)
context = self.item_descriptor.render('student_view').content
context = self.item_descriptor.render(STUDENT_VIEW).content
expected_context = dict(initial_context)
expected_context.update({
......
......@@ -5,6 +5,7 @@ import json
from operator import itemgetter
from . import BaseTestXmodule
from xmodule.x_module import STUDENT_VIEW
class TestWordCloud(BaseTestXmodule):
......@@ -242,7 +243,7 @@ class TestWordCloud(BaseTestXmodule):
def test_word_cloud_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
fragment = self.runtime.render(self.item_descriptor, 'student_view')
fragment = self.runtime.render(self.item_descriptor, STUDENT_VIEW)
expected_context = {
'ajax_url': self.item_descriptor.xmodule_runtime.ajax_url,
......
......@@ -42,6 +42,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from xmodule.modulestore.search import path_to_location
from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEndedGradingTab
from xmodule.x_module import STUDENT_VIEW
import shoppingcart
from opaque_keys import InvalidKeyError
......@@ -377,7 +378,7 @@ def index(request, course_id, chapter=None, section=None,
# Save where we are in the chapter
save_child_position(chapter_module, section)
context['fragment'] = section_module.render('student_view')
context['fragment'] = section_module.render(STUDENT_VIEW)
context['section_title'] = section_descriptor.display_name_with_default
else:
# section is none, so display a message
......@@ -865,7 +866,7 @@ def get_static_tab_contents(request, course, tab):
html = ''
if tab_module is not None:
try:
html = tab_module.render('student_view').content
html = tab_module.render(STUDENT_VIEW).content
except Exception: # pylint: disable=broad-except
html = render_to_string('courseware/error-message.html', None)
log.exception(
......
<%! from django.utils.translation import ugettext as _ %>
<%! from xmodule.split_test_module import ValidationMessageType %>
<%
split_test = context.get('split_test')
user_partition = split_test.descriptor.get_selected_partition()
messages = split_test.descriptor.validation_messages()
%>
% if is_root and not is_configured:
<div class="no-container-content">
% else:
<div class="wrapper-xblock-message">
% endif
% if user_partition:
<div class="xblock-message information">
<p>
<span class="message-text">
${_("This content experiment uses group configuration '{experiment_name}'.").format(experiment_name=user_partition.name)}
</span>
</p>
</div>
% endif
% if len(messages) > 0:
<%
def get_validation_icon(validation_type):
if validation_type == 'error':
return 'icon-exclamation-sign'
elif validation_type == 'warning':
return 'icon-warning-sign'
return None
error_messages = (message for message in messages if message.message_type==ValidationMessageType.error)
has_errors = next(error_messages, False)
aggregate_validation_class = 'has-errors' if has_errors else 'has-warnings'
aggregate_validation_type = 'error' if has_errors else 'warning'
%>
<div class="xblock-message validation ${aggregate_validation_class}">
% if is_configured:
<p class="${aggregate_validation_type}"><i class="${get_validation_icon(aggregate_validation_type)}"></i>
${_("This content experiment has issues that affect content visibility.")}
</p>
% endif
% if is_root or not is_configured:
<ul class="xblock-message-list">
% for message in messages:
<%
message_type = message.message_type
message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None
%>
<li class="xblock-message-item ${message_type}">
% if not is_configured:
<i class="${get_validation_icon(message_type)}"></i>
% endif
<span class="message-text">
% if message_type_display_name:
<span class="sr">${message_type_display_name}:</span>
% endif
${unicode(message)}
% if message.action_class:
<a href="#" class="button action-button ${message.action_class}">
<span class="action-button-text">${message.action_label}</span>
</a>
% endif
</span>
</li>
% endfor
</ul>
% endif
</div>
% endif
</div>
% if is_root:
<div class="wrapper-groups is-active">
<h3 class="sr">${_("Active Groups")}</h3>
${active_groups_preview}
</div>
% if inactive_groups_preview:
<div class="wrapper-groups is-inactive">
<h3 class="title">${_("Inactive Groups")}</h3>
${inactive_groups_preview}
</div>
% endif
% endif
<%! from django.utils.translation import ugettext as _ %>
<%! from xmodule.split_test_module import ValidationMessageType %>
<%
split_test = context.get('split_test')
(message, message_type) = split_test.descriptor.validation_message()
message_type_display_name = ValidationMessageType.display_name(message_type) if message_type else None
is_configured = split_test.user_partition_id >= 0
%>
% if message or not is_configured:
% if is_root and not is_configured:
<div class="no-container-content">
% else:
<div class="wrapper-xblock-message">
<div class="xblock-message ${message_type}">
% endif
% if not is_configured:
<p><i class="icon-warning-sign"></i> ${_("You must select a group configuration for this content experiment.")}
<a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i> <span class="action-button-text">${_("Select a Group Configuration")}</span>
</a>
</p>
% else:
<p>
% if message_type == 'warning':
<i class='icon-warning-sign'></i>
% elif message_type == 'error':
<i class='icon-exclamation-sign'></i>
% endif
<span class='message-text'>
% if message_type_display_name:
<span class='sr'>${message_type_display_name}:</span>
% endif
${message}
</span>
</p>
% endif
% if is_root and not is_configured:
</div>
% else:
</div>
</div>
% endif
% endif
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