Skip to content

Commit

Permalink
feat: [FC-0070] Create a new Studio view for rendering whole Unit in …
Browse files Browse the repository at this point in the history
…an iframe
  • Loading branch information
Sagirov Eugeniy committed Oct 9, 2024
1 parent b3759aa commit ea1ad59
Show file tree
Hide file tree
Showing 5 changed files with 460 additions and 75 deletions.
35 changes: 34 additions & 1 deletion cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@

__all__ = [
'container_handler',
'component_handler'
'component_handler',
'container_embed_handler',
]

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -141,6 +142,38 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st
return HttpResponseBadRequest("Only supports HTML requests")


@require_GET
@login_required
def container_embed_handler(request, usage_key_string): # pylint: disable=too-many-statements
"""
Returns an HttpResponse with HTML content for the container xBlock.
The returned HTML is a chromeless rendering of the xBlock.
GET
html: returns the HTML page for editing a container
json: not currently supported
"""

from ..utils import get_container_handler_context

if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):

try:
usage_key = UsageKey.from_string(usage_key_string)
except InvalidKeyError: # Raise Http404 on invalid 'usage_key_string'
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
with modulestore().bulk_operations(usage_key.course_key):
try:
course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key)
except ItemNotFoundError:
return HttpResponseBadRequest()

container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
return render_to_response('container_chromeless.html', container_handler_context)
else:
return HttpResponseBadRequest("Only supports HTML requests")


def get_component_templates(courselike, library=False): # lint-amnesty, pylint: disable=too-many-statements
"""
Returns the applicable component templates that can be used by the specified course or library.
Expand Down
58 changes: 58 additions & 0 deletions cms/djangoapps/contentstore/views/tests/test_container_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,61 @@ def test_container_page_with_valid_and_invalid_usage_key_string(self):
usage_key_string=str(self.vertical.location)
)
self.assertEqual(response.status_code, 200)


class ContainerEmbedPageTestCase(ContainerPageTestCase): # lint-amnesty, pylint: disable=test-inherits-tests
"""
Unit tests for the container embed page.
"""

def test_container_html(self):
assets_url = reverse(
'assets_handler', kwargs={'course_key_string': str(self.child_container.location.course_key)}
)
self._test_html_content(
self.child_container,
expected_section_tag=(
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
'data-locator="{0}" data-course-key="{0.course_key}" data-course-assets="{1}">'.format(
self.child_container.location, assets_url
)
),
)

def test_container_on_container_html(self):
"""
Create the scenario of an xblock with children (non-vertical) on the container page.
This should create a container page that is a child of another container page.
"""
draft_container = self._create_block(self.child_container, "wrapper", "Wrapper")
self._create_block(draft_container, "html", "Child HTML")

def test_container_html(xblock):
assets_url = reverse(
'assets_handler', kwargs={'course_key_string': str(draft_container.location.course_key)}
)
self._test_html_content(
xblock,
expected_section_tag=(
'<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" '
'data-locator="{0}" data-course-key="{0.course_key}" data-course-assets="{1}">'.format(
draft_container.location, assets_url
)
),
)

# Test the draft version of the container
test_container_html(draft_container)

# Now publish the unit and validate again
self.store.publish(self.vertical.location, self.user.id)
draft_container = self.store.get_item(draft_container.location)
test_container_html(draft_container)

def _test_html_content(self, xblock, expected_section_tag): # lint-amnesty, pylint: disable=arguments-differ
"""
Get the HTML for a container page and verify the section tag is correct
and the breadcrumbs trail is correct.
"""
html = self.get_page_html(xblock)
self.assertIn(expected_section_tag, html)
164 changes: 90 additions & 74 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ function($, _, Backbone, gettext, BasePage,

renderAddXBlockComponents: function() {
var self = this;
if (self.options.canEdit) {
if (self.options.canEdit && !self.options.isIframeEmbed) {
this.$('.add-xblock-component').each(function(index, element) {
var component = new AddXBlockComponent({
el: element,
Expand Down Expand Up @@ -273,6 +273,18 @@ function($, _, Backbone, gettext, BasePage,
/** The user has clicked on the "Paste Component button" */
pasteComponent(event) {
event.preventDefault();
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'pasteComponent',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
// Get the ID of the container (usually a unit/vertical) that we're pasting into:
const parentElement = this.findXBlockElement(event.target);
const parentLocator = parentElement.data('locator');
Expand Down Expand Up @@ -365,6 +377,18 @@ function($, _, Backbone, gettext, BasePage,

editXBlock: function(event, options) {
event.preventDefault();
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'editXBlock',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}

if (!options || options.view !== 'visibility_view') {
const primaryHeader = $(event.target).closest('.xblock-header-primary, .nav-actions');
Expand Down Expand Up @@ -432,66 +456,43 @@ function($, _, Backbone, gettext, BasePage,
});
},

duplicateXBlock: function(event) {
event.preventDefault();
this.duplicateComponent(this.findXBlockElement(event.target));
},

openManageTags: function(event) {
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'openManageTags',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
const contentId = this.findXBlockElement(event.target).data('locator');

TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
},

showMoveXBlockModal: function(event) {
var xblockElement = this.findXBlockElement(event.target),
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
modal = new MoveXBlockModal({
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
XBlockURLRoot: this.getURLRoot(),
outlineURL: this.options.outlineURL
});

event.preventDefault();
modal.show();
},

deleteXBlock: function(event) {
event.preventDefault();
this.deleteComponent(this.findXBlockElement(event.target));
},

createPlaceholderElement: function() {
return $('<div/>', {class: 'studio-xblock-wrapper'});
},

createComponent: function(template, target) {
// A placeholder element is created in the correct location for the new xblock
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
var parentElement = this.findXBlockElement(target),
parentLocator = parentElement.data('locator'),
buttonPanel = target.closest('.add-xblock-component'),
listPanel = buttonPanel.prev(),
scrollOffset = ViewUtils.getScrollOffset(buttonPanel),
$placeholderEl = $(this.createPlaceholderElement()),
requestData = _.extend(template, {
parent_locator: parentLocator
}),
placeholderElement;
placeholderElement = $placeholderEl.appendTo(listPanel);
return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
});
},

copyXBlock: function(event) {
event.preventDefault();
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'copyXBlock',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
const clipboardEndpoint = "/api/content-staging/v1/clipboard/";
const element = this.findXBlockElement(event.target);
const usageKeyToCopy = element.data('locator');
Expand Down Expand Up @@ -535,48 +536,63 @@ function($, _, Backbone, gettext, BasePage,
});
},

duplicateComponent: function(xblockElement) {
// A placeholder element is created in the correct location for the duplicate xblock
// and then onNewXBlock will replace it with a rendering of the xblock. Note that
// for xblocks that can't be replaced inline, the entire parent will be refreshed.
var self = this,
parentElement = self.findXBlockElement(xblockElement.parent()),
scrollOffset = ViewUtils.getScrollOffset(xblockElement),
$placeholderEl = $(self.createPlaceholderElement()),
placeholderElement;

placeholderElement = $placeholderEl.insertAfter(xblockElement);
XBlockUtils.duplicateXBlock(xblockElement, parentElement)
.done(function(data) {
self.onNewXBlock(placeholderElement, scrollOffset, true, data);
})
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
});
},

duplicateXBlock: function(event) {
event.preventDefault();
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'duplicateXBlock',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
this.duplicateComponent(this.findXBlockElement(event.target));
},

showMoveXBlockModal: function(event) {
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'showMoveXBlockModal',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
var xblockElement = this.findXBlockElement(event.target),
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
modal = new MoveXBlockModal({
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
XBlockURLRoot: this.getURLRoot(),
outlineURL: this.options.outlineURL
});
sourceXBlockInfo: XBlockUtils.findXBlockInfo(xblockElement, this.model),
sourceParentXBlockInfo: XBlockUtils.findXBlockInfo(parentXBlockElement, this.model),
XBlockURLRoot: this.getURLRoot(),
outlineURL: this.options.outlineURL
});

event.preventDefault();
modal.show();
},

deleteXBlock: function(event) {
event.preventDefault();
try {
if (this.options.isIframeEmbed) {
window.parent.postMessage(
{
type: 'deleteXBlock',
payload: {}
}, document.referrer
);
}
} catch (e) {
console.error(e);
}
this.deleteComponent(this.findXBlockElement(event.target));
},

Expand Down
Loading

0 comments on commit ea1ad59

Please sign in to comment.