Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Refactored XBlockAside rendering and added support for student view
Browse files Browse the repository at this point in the history
  • Loading branch information
gsidebo authored and tasawernawaz committed Dec 11, 2018
1 parent 353fe07 commit 3dad2be
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 39 deletions.
24 changes: 12 additions & 12 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET
from opaque_keys import InvalidKeyError
from opaque_keys.edx.asides import AsideUsageKeyV1, AsideUsageKeyV2
from opaque_keys.edx.keys import UsageKey
from xblock.core import XBlock
from xblock.django.request import django_to_webob_request, webob_to_django_response
from xblock.exceptions import NoSuchHandlerError
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist

from contentstore.utils import get_lms_link_for_item, get_xblock_aside_instance, reverse_course_url
from contentstore.utils import get_lms_link_for_item, reverse_course_url
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
from contentstore.views.item import StudioEditModuleRuntime, add_container_page_publishing_info, create_xblock_info
from edxmako.shortcuts import render_to_response
from openedx.core.lib.xblock_utils import is_xblock_aside, get_aside_from_xblock
from student.auth import has_course_author_access
from xblock_django.api import authorable_xblocks, disabled_xblocks
from xblock_django.models import XBlockStudioConfigurationFlag
Expand Down Expand Up @@ -441,25 +441,25 @@ def component_handler(request, usage_key_string, handler, suffix=''):
:class:`django.http.HttpResponse`: The response from the handler, converted to a
django response
"""

usage_key = UsageKey.from_string(usage_key_string)

# Let the module handle the AJAX
req = django_to_webob_request(request)

asides = []

try:
if isinstance(usage_key, (AsideUsageKeyV1, AsideUsageKeyV2)):
if is_xblock_aside(usage_key):
# Get the descriptor for the block being wrapped by the aside (not the aside itself)
descriptor = modulestore().get_item(usage_key.usage_key)
aside_instance = get_xblock_aside_instance(usage_key)
asides = [aside_instance] if aside_instance else []
resp = aside_instance.handle(handler, req, suffix)
handler_descriptor = get_aside_from_xblock(descriptor, usage_key.aside_type)
asides = [handler_descriptor]
else:
descriptor = modulestore().get_item(usage_key)
descriptor.xmodule_runtime = StudioEditModuleRuntime(request.user)
resp = descriptor.handle(handler, req, suffix)
handler_descriptor = descriptor
asides = []
handler_descriptor.xmodule_runtime = StudioEditModuleRuntime(request.user)
resp = handler_descriptor.handle(handler, req, suffix)
except NoSuchHandlerError:
log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True)
log.info("XBlock %s attempted to access missing handler %r", handler_descriptor, handler, exc_info=True)
raise Http404

# unintentional update to handle any side effects of handle call
Expand Down
10 changes: 9 additions & 1 deletion cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
from openedx.core.djangoapps.waffle_utils import WaffleSwitch
from openedx.core.lib.gating import api as gating_api
from openedx.core.lib.xblock_utils import request_token, wrap_xblock
from openedx.core.lib.xblock_utils import request_token, wrap_xblock, wrap_xblock_aside
from static_replace import replace_static_urls
from student.auth import has_studio_read_access, has_studio_write_access
from util.date_utils import get_default_time_display
Expand Down Expand Up @@ -326,6 +326,14 @@ def xblock_view_handler(request, usage_key_string, view_name):
request_token=request_token(request),
))

xblock.runtime.wrappers_asides.append(partial(
wrap_xblock_aside,
'StudioRuntime',
usage_id_serializer=unicode,
request_token=request_token(request),
extra_classes=['wrapper-comp-plugins']
))

if view_name in (STUDIO_VIEW, VISIBILITY_VIEW):
if view_name == STUDIO_VIEW and xblock.xmodule_runtime is None:
xblock.xmodule_runtime = StudioEditModuleRuntime(request.user)
Expand Down
45 changes: 43 additions & 2 deletions cms/djangoapps/contentstore/views/tests/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
from django.test.client import RequestFactory
from mock import Mock, PropertyMock, patch
from opaque_keys import InvalidKeyError
from opaque_keys.edx.asides import AsideUsageKeyV2
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from pyquery import PyQuery
from pytz import UTC
from six import text_type
from web_fragments.fragment import Fragment
from webob import Response
from xblock.core import XBlockAside
Expand Down Expand Up @@ -2135,6 +2137,7 @@ def test_add_groups(self):

@ddt.ddt
class TestComponentHandler(TestCase):
"""Tests for component handler api"""
shard = 1

def setUp(self):
Expand All @@ -2151,9 +2154,10 @@ def setUp(self):
# of the xBlock descriptor.
self.descriptor = self.modulestore.return_value.get_item.return_value

self.usage_key_string = unicode(
BlockUsageLocator(CourseLocator('dummy_org', 'dummy_course', 'dummy_run'), 'dummy_category', 'dummy_name')
self.usage_key = BlockUsageLocator(
CourseLocator('dummy_org', 'dummy_course', 'dummy_run'), 'dummy_category', 'dummy_name'
)
self.usage_key_string = text_type(self.usage_key)

self.user = UserFactory()

Expand Down Expand Up @@ -2192,6 +2196,43 @@ def create_response(handler, request, suffix):
self.assertEquals(component_handler(self.request, self.usage_key_string, 'dummy_handler').status_code,
status_code)

@ddt.data((True, True), (False, False),)
@ddt.unpack
def test_aside(self, is_xblock_aside, is_get_aside_called):
"""
test get_aside_from_xblock called
"""
def create_response(handler, request, suffix): # pylint: disable=unused-argument
"""create dummy response"""
return Response(status_code=200)

def get_usage_key():
"""return usage key"""
return (
text_type(AsideUsageKeyV2(self.usage_key, "aside"))
if is_xblock_aside
else self.usage_key_string
)

self.descriptor.handle = create_response

with patch(
'contentstore.views.component.is_xblock_aside',
return_value=is_xblock_aside
), patch(
'contentstore.views.component.get_aside_from_xblock'
) as mocked_get_aside_from_xblock, patch(
"contentstore.views.component.webob_to_django_response"
) as mocked_webob_to_django_response:
component_handler(
self.request,
get_usage_key(),
'dummy_handler'
)
assert mocked_webob_to_django_response.called is True

assert mocked_get_aside_from_xblock.called is is_get_aside_called


class TestComponentTemplates(CourseTestCase):
"""
Expand Down
13 changes: 11 additions & 2 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
view: 'studio_view',
viewSpecificClasses: 'modal-editor confirm',
// Translators: "title" is the name of the current component being edited.
titleFormat: gettext('Editing: %(title)s'),
titleFormat: gettext('Editing: {title}'),
addPrimaryActionButton: true
}),

Expand Down Expand Up @@ -87,6 +87,10 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
this.$('.modal-window-title').text(title);
if (editorView.getDataEditor() && editorView.getMetadataEditor()) {
this.addDefaultModes();
// If the plugins content element exists, add a button to reveal it.
if (this.$('.wrapper-comp-plugins').length > 0) {
this.addModeButton('plugins', gettext('Plugins'));
}
this.selectMode(editorView.mode);
}
}
Expand Down Expand Up @@ -125,7 +129,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod
displayName = gettext('Component');
}
}
return interpolate(this.options.titleFormat, {title: displayName}, true);
return edx.StringUtils.interpolate(
this.options.titleFormat, {
title: displayName
}
);
},

addDefaultModes: function() {
Expand Down Expand Up @@ -192,6 +200,7 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/modals/base_mod

addModeButton: function(mode, displayName) {
var buttonPanel = this.$('.editor-modes');
// xss-lint: disable=javascript-jquery-append
buttonPanel.append(this.editorModeButtonTemplate({
mode: mode,
displayName: displayName
Expand Down
28 changes: 21 additions & 7 deletions cms/static/js/views/xblock_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between
* the available modes.
*/
define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata', 'js/collections/metadata',
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'js/views/xblock', 'js/views/metadata', 'js/collections/metadata',
'jquery.inputnumber'],
function($, _, gettext, XBlockView, MetadataView, MetadataCollection) {
function($, _, gettext, BaseView, XBlockView, MetadataView, MetadataCollection) {
var XBlockEditorView = XBlockView.extend({
// takes XBlockInfo as a model

Expand All @@ -24,6 +24,7 @@ define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata

initializeEditors: function() {
var metadataEditor,
pluginEl,
defaultMode = 'editor';
metadataEditor = this.createMetadataEditor();
this.metadataEditor = metadataEditor;
Expand All @@ -35,6 +36,12 @@ define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata
}
this.selectMode(defaultMode);
}
pluginEl = this.$('.wrapper-comp-plugins');
if (pluginEl.length > 0) {
this.pluginEditor = new BaseView({
el: pluginEl
});
}
},

getDefaultModes: function() {
Expand Down Expand Up @@ -87,6 +94,10 @@ define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata
return this.metadataEditor;
},

getPluginEditor: function() {
return this.pluginEditor;
},

/**
* Returns the updated field data for the xblock. Note that this works for all
* XModules as well as for XBlocks that provide a 'collectFieldData' API.
Expand Down Expand Up @@ -144,14 +155,17 @@ define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata
},

selectMode: function(mode) {
var showEditor = mode === 'editor',
dataEditor = this.getDataEditor(),
metadataEditor = this.getMetadataEditor();
var dataEditor = this.getDataEditor(),
metadataEditor = this.getMetadataEditor(),
pluginEditor = this.getPluginEditor();
if (dataEditor) {
this.setEditorActivation(dataEditor, showEditor);
this.setEditorActivation(dataEditor, mode === 'editor');
}
if (metadataEditor) {
this.setEditorActivation(metadataEditor.$el, !showEditor);
this.setEditorActivation(metadataEditor.$el, mode === 'settings');
}
if (pluginEditor) {
this.setEditorActivation(pluginEditor.$el, mode === 'plugins');
}
this.mode = mode;
},
Expand Down
3 changes: 2 additions & 1 deletion cms/static/sass/elements/_modal-window.scss
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@
margin-left: ($baseline/2);

.editor-button,
.settings-button {
.settings-button,
.plugins-button {
@extend %btn-secondary-gray;
@extend %t-copy-sub1;

Expand Down
8 changes: 8 additions & 0 deletions cms/static/sass/elements/_xblocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,14 @@
}
}

.wrapper-comp-plugins {
display: none;

&.is-active {
display: block;
}
}


// +Case - Special Xblock Type Overrides
// ====================
Expand Down
4 changes: 4 additions & 0 deletions common/static/common/js/xblock/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
} else {
selector = '.' + blockClass;
}
// After an element is initialized, a class is added to it. To avoid repeat initialization, no
// elements with that class should be selected.
selector += ':not(.xblock-initialized)';
return $(element).immediateDescendents(selector).map(function(idx, elem) {
return initializer(elem, requestToken);
}).toArray();
Expand Down Expand Up @@ -112,6 +115,7 @@
initializeAside: function(element) {
var blockUsageId = $(element).data('block-id');
var blockElement = $(element).siblings('[data-usage-id="' + blockUsageId + '"]')[0];

return constructBlock(element, [blockElement, initArgs(element)]);
},

Expand Down
26 changes: 23 additions & 3 deletions lms/djangoapps/courseware/module_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@
replace_course_urls,
replace_jump_to_id_urls,
replace_static_urls,
wrap_xblock
wrap_xblock,
is_xblock_aside,
get_aside_from_xblock,
)
from student.models import anonymous_id_for_user, user_by_anonymous_id
from student.roles import CourseBetaTesterRole
Expand Down Expand Up @@ -1116,7 +1118,18 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
set_custom_metrics_for_course_key(course_key)

with modulestore().bulk_operations(course_key):
instance, tracking_context = get_module_by_usage_id(request, course_id, usage_id, course=course)
try:
usage_key = UsageKey.from_string(unquote_slashes(usage_id))
except InvalidKeyError:
raise Http404
if is_xblock_aside(usage_key):
# Get the usage key for the block being wrapped by the aside (not the aside itself)
block_usage_key = usage_key.usage_key
else:
block_usage_key = usage_key
instance, tracking_context = get_module_by_usage_id(
request, course_id, unicode(block_usage_key), course=course
)

# Name the transaction so that we can view XBlock handlers separately in
# New Relic. The suffix is necessary for XModule handlers because the
Expand All @@ -1129,7 +1142,14 @@ def _invoke_xblock_handler(request, course_id, usage_id, handler, suffix, course
req = django_to_webob_request(request)
try:
with tracker.get_tracker().context(tracking_context_name, tracking_context):
resp = instance.handle(handler, req, suffix)
if is_xblock_aside(usage_key):
# In this case, 'instance' is the XBlock being wrapped by the aside, so
# the actual aside instance needs to be retrieved in order to invoke its
# handler method.
handler_instance = get_aside_from_xblock(instance, usage_key.aside_type)
else:
handler_instance = instance
resp = handler_instance.handle(handler, req, suffix)
if suffix == 'problem_check' \
and course \
and getattr(course, 'entrance_exam_enabled', False) \
Expand Down
12 changes: 11 additions & 1 deletion lms/djangoapps/courseware/student_field_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json

from courseware.models import StudentFieldOverride
from openedx.core.lib.xblock_utils import is_xblock_aside

from .field_overrides import FieldOverrideProvider

Expand Down Expand Up @@ -44,9 +45,18 @@ def _get_overrides_for_user(user, block):
Gets all of the individual student overrides for given user and block.
Returns a dictionary of field override values keyed by field name.
"""
if (
hasattr(block, "scope_ids") and
hasattr(block.scope_ids, "usage_id") and
is_xblock_aside(block.scope_ids.usage_id)
):
location = block.scope_ids.usage_id.usage_key
else:
location = block.location

query = StudentFieldOverride.objects.filter(
course_id=block.runtime.course_id,
location=block.location,
location=location,
student_id=user.id,
)
overrides = {}
Expand Down
Loading

0 comments on commit 3dad2be

Please sign in to comment.