Merge branch 'master' into jciasenza
@@ -1,8 +1,8 @@
|
||||
We don't maintain a detailed changelog. For details of changes, please see
|
||||
either the `edX Release Notes`_ or the `GitHub commit history`_.
|
||||
either the `Open edX Release Notes`_ or the `GitHub commit history`_.
|
||||
|
||||
|
||||
|
||||
|
||||
.. _edX Release Notes: https://edx.readthedocs.io/projects/open-edx-release-notes/en/latest/
|
||||
.. _Open edX Release Notes: https://docs.openedx.org/en/latest/community/release_notes/index.html
|
||||
.. _GitHub commit history: https://github.com/openedx/edx-platform/commits/master
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
CMS
|
||||
===
|
||||
|
||||
This directory contains code relating to the Open edX Content Management System ("CMS"). It allows learning content to be created, edited, versioned, and eventually published to the `Open edX Learning Mangement System <../lms>`_ ("LMS"). The main user-facing application that CMS powers is the `Open edX Studio <https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/getting_started/CA_get_started_Studio.html#>`_
|
||||
This directory contains code relating to the Open edX Content Management System ("CMS"). It allows learning content to be created, edited, versioned, and eventually published to the `Open edX Learning Mangement System <../lms>`_ ("LMS"). The main user-facing application that CMS powers is the `Open edX Studio <https://docs.openedx.org/en/latest/educators/concepts/open_edx_platform/what_is_studio.html>`_
|
||||
|
||||
See also
|
||||
--------
|
||||
|
||||
@@ -82,6 +82,22 @@ def is_unit(xblock, parent_xblock=None):
|
||||
return False
|
||||
|
||||
|
||||
def is_library_content(xblock):
|
||||
"""
|
||||
Returns true if the specified xblock is library content.
|
||||
"""
|
||||
return xblock.category == 'library_content'
|
||||
|
||||
|
||||
def get_parent_if_split_test(xblock):
|
||||
"""
|
||||
Returns the parent of the specified xblock if it is a split test, otherwise returns None.
|
||||
"""
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
if parent_xblock and parent_xblock.category == 'split_test':
|
||||
return parent_xblock
|
||||
|
||||
|
||||
def xblock_has_own_studio_page(xblock, parent_xblock=None):
|
||||
"""
|
||||
Returns true if the specified xblock has an associated Studio page. Most xblocks do
|
||||
|
||||
@@ -25,7 +25,11 @@ from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
from common.djangoapps.student.auth import has_course_author_access
|
||||
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
|
||||
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
|
||||
from cms.djangoapps.contentstore.helpers import is_unit
|
||||
from cms.djangoapps.contentstore.helpers import (
|
||||
get_parent_if_split_test,
|
||||
is_unit,
|
||||
is_library_content,
|
||||
)
|
||||
from cms.djangoapps.contentstore.toggles import (
|
||||
libraries_v1_enabled,
|
||||
libraries_v2_enabled,
|
||||
@@ -148,11 +152,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st
|
||||
except ItemNotFoundError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
is_unit_page = is_unit(xblock)
|
||||
unit = xblock if is_unit_page else None
|
||||
if use_new_unit_page(course.id):
|
||||
if is_unit(xblock) or is_library_content(xblock):
|
||||
return redirect(get_unit_url(course.id, xblock.location))
|
||||
|
||||
if is_unit_page and use_new_unit_page(course.id):
|
||||
return redirect(get_unit_url(course.id, unit.location))
|
||||
if split_xblock := get_parent_if_split_test(xblock):
|
||||
return redirect(get_unit_url(course.id, split_xblock.location))
|
||||
|
||||
container_handler_context = get_container_handler_context(request, usage_key, course, xblock)
|
||||
container_handler_context.update({
|
||||
|
||||
@@ -611,6 +611,7 @@ def _create_block(request):
|
||||
modulestore().update_item(created_block, request.user.id)
|
||||
response["upstreamRef"] = upstream_ref
|
||||
response["static_file_notices"] = asdict(static_file_notices)
|
||||
response["parent_locator"] = parent_locator
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
@@ -2779,16 +2779,16 @@ SHOW_ACCOUNT_ACTIVATION_CTA = False
|
||||
################# Documentation links for course apps #################
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
CALCULATOR_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/calculator.html"
|
||||
DISCUSSIONS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/create_discussion.html"
|
||||
EDXNOTES_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/notes.html"
|
||||
PROGRESS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html?highlight=progress#hiding-or-showing-the-wiki-or-progress-pages"
|
||||
TEAMS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_features/teams/teams_setup.html"
|
||||
TEXTBOOKS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/textbooks.html"
|
||||
WIKI_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/course_wiki.html"
|
||||
CUSTOM_PAGES_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html#adding-custom-pages"
|
||||
COURSE_LIVE_HELP_URL = "https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/course_assets/course_live.html"
|
||||
ORA_SETTINGS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html#configuring-course-level-open-response-assessment-settings"
|
||||
CALCULATOR_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_calculator.html"
|
||||
DISCUSSIONS_HELP_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_discussions.html"
|
||||
EDXNOTES_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/enable_notes.html"
|
||||
PROGRESS_HELP_URL = "https://docs.openedx.org/en/latest/educators/references/data/progress_page.html"
|
||||
TEAMS_HELP_URL = "https://docs.openedx.org/en/latest/educators/navigation/advanced_features.html#use-teams-in-your-course"
|
||||
TEXTBOOKS_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_textbooks.html"
|
||||
WIKI_HELP_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_wiki.html"
|
||||
CUSTOM_PAGES_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_custom_page.html"
|
||||
COURSE_LIVE_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/add_course_live.html"
|
||||
ORA_SETTINGS_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/Manage_ORA_Assignment.html"
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
# keys for big blue button live provider
|
||||
@@ -2813,7 +2813,7 @@ DISCUSSIONS_INCONTEXT_FEEDBACK_URL = ''
|
||||
|
||||
# Learn More link in upgraded discussion notification alert
|
||||
# pylint: disable=line-too-long
|
||||
DISCUSSIONS_INCONTEXT_LEARNMORE_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/manage_discussions/discussions.html"
|
||||
DISCUSSIONS_INCONTEXT_LEARNMORE_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_discussions.html"
|
||||
# pylint: enable=line-too-long
|
||||
|
||||
#### django-simple-history##
|
||||
@@ -2837,7 +2837,7 @@ def _should_send_learning_badge_events(settings):
|
||||
# Each topic configuration dictionary contains
|
||||
# * `enabled`: a toggle denoting whether the event will be published to the topic. These should be annotated
|
||||
# according to
|
||||
# https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html
|
||||
# https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html
|
||||
# * `event_key_field` which is a period-delimited string path to event data field to use as event key.
|
||||
# Note: The topic names should not include environment prefix as it will be dynamically added based on
|
||||
# EVENT_BUS_TOPIC_PREFIX setting.
|
||||
|
||||
6
cms/static/images/advanced-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path
|
||||
d="M19.8 18.4 14 10.67V6.5l1.35-1.69c.26-.33.03-.81-.39-.81H9.04c-.42 0-.65.48-.39.81L10 6.5v4.17L4.2 18.4c-.49.66-.02 1.6.8 1.6h14c.82 0 1.29-.94.8-1.6z"
|
||||
fill="currentColor">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 365 B |
6
cms/static/images/drag-and-drop-v2-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path
|
||||
d="M2.21 10.47 5 9.36 7.25 15H8V2h2.5v10h1V0H14v12h1V1.5h2.5V12h1V4.5H21V16c0 4.42-3.58 8-8 8-3.26 0-6.19-1.99-7.4-5.02l-3.39-8.51z"
|
||||
fill="currentColor">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 343 B |
3
cms/static/images/itembank-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path d="M21 3H3v18h18V3zM7.5 18c-.83 0-1.5-.67-1.5-1.5S6.67 15 7.5 15s1.5.67 1.5 1.5S8.33 18 7.5 18zm0-9C6.67 9 6 8.33 6 7.5S6.67 6 7.5 6 9 6.67 9 7.5 8.33 9 7.5 9zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm0-9c-.83 0-1.5-.67-1.5-1.5S15.67 6 16.5 6s1.5.67 1.5 1.5S17.33 9 16.5 9z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 581 B |
3
cms/static/images/library-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path d="M4 6H2v16h16v-2H4V6zm18-4H6v16h16V2zm-3 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
3
cms/static/images/library_v2-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 326 B |
6
cms/static/images/openassessment-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path
|
||||
d="M3 10h11v2H3v-2zm0-2h11V6H3v2zm0 8h7v-2H3v2zm15.01-3.13 1.41-1.41 2.12 2.12-1.41 1.41-2.12-2.12zm-.71.71-5.3 5.3V21h2.12l5.3-5.3-2.12-2.12z"
|
||||
fill="currentColor">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
6
cms/static/images/problem-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path
|
||||
d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"
|
||||
fill="currentColor">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 458 B |
3
cms/static/images/text-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" role="img" focusable="false" aria-hidden="true">
|
||||
<path d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
3
cms/static/images/video-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true">
|
||||
<path d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
@@ -42,10 +42,38 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add
|
||||
},
|
||||
|
||||
showComponentTemplates: function(event) {
|
||||
var type;
|
||||
var type, parentLocator, model, parentBlockType;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
type = $(event.currentTarget).data('type');
|
||||
parentLocator = $(event.currentTarget).closest('.xblock[data-usage-id]').data('usage-id');
|
||||
parentBlockType = $(event.currentTarget).parents('.xblock-author_view').last().data('block-type');
|
||||
model = this.collection.models.find(function(item) { return item.type === type; }) || {};
|
||||
|
||||
try {
|
||||
if (this.options.isIframeEmbed && parentBlockType !== 'split_test') {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'showComponentTemplates',
|
||||
payload: {
|
||||
type: type,
|
||||
parentLocator: parentLocator,
|
||||
model: {
|
||||
type: model.type,
|
||||
display_name: model.display_name,
|
||||
templates: model.templates,
|
||||
support_legend: model.support_legend,
|
||||
},
|
||||
}
|
||||
}, document.referrer
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
this.$('.new-component').slideUp(250);
|
||||
this.$('.new-component-' + type).slideDown(250);
|
||||
this.$('.new-component-' + type + ' div').focus();
|
||||
@@ -65,11 +93,25 @@ function($, _, gettext, BaseView, ViewUtils, AddXBlockButton, AddXBlockMenu, Add
|
||||
var self = this,
|
||||
$element = $(event.currentTarget),
|
||||
saveData = $element.data(),
|
||||
oldOffset = ViewUtils.getScrollOffset(this.$el);
|
||||
oldOffset = ViewUtils.getScrollOffset(this.$el),
|
||||
usageId = $element.closest('.xblock[data-usage-id]').data('usage-id');
|
||||
event.preventDefault();
|
||||
this.closeNewComponent(event);
|
||||
|
||||
if (saveData.type === 'library_v2') {
|
||||
try {
|
||||
if (this.options.isIframeEmbed) {
|
||||
return window.parent.postMessage(
|
||||
{
|
||||
type: 'showSingleComponentPicker',
|
||||
payload: { usageId },
|
||||
}, document.referrer
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
var modal = new AddLibraryContent();
|
||||
modal.showComponentPicker(
|
||||
this.options.libraryContentPickerUrl,
|
||||
|
||||
@@ -40,6 +40,7 @@ function($, _, Backbone, gettext, BasePage,
|
||||
'change .header-library-checkbox': 'toggleLibraryComponent',
|
||||
'click .collapse-button': 'collapseXBlock',
|
||||
'click .xblock-view-action-button': 'viewXBlockContent',
|
||||
'click .xblock-view-group-link': 'viewXBlockContent',
|
||||
},
|
||||
|
||||
options: {
|
||||
@@ -60,8 +61,9 @@ function($, _, Backbone, gettext, BasePage,
|
||||
initialize: function(options) {
|
||||
BasePage.prototype.initialize.call(this, options);
|
||||
this.viewClass = options.viewClass || this.defaultViewClass;
|
||||
this.isLibraryPage = (this.model.attributes.category === 'library');
|
||||
this.isLibraryContentPage = (this.model.attributes.category === 'library_content');
|
||||
this.isLibraryPage = this.model.attributes.category === 'library';
|
||||
this.isLibraryContentPage = this.model.attributes.category === 'library_content';
|
||||
this.isSplitTestContentPage = this.model.attributes.category === 'split_test';
|
||||
this.nameEditor = new XBlockStringFieldEditor({
|
||||
el: this.$('.wrapper-xblock-field'),
|
||||
model: this.model
|
||||
@@ -131,13 +133,16 @@ function($, _, Backbone, gettext, BasePage,
|
||||
|
||||
if (this.options.isIframeEmbed) {
|
||||
window.addEventListener('message', (event) => {
|
||||
const { data } = event;
|
||||
const { data: initialData } = event;
|
||||
|
||||
if (!data) return;
|
||||
if (!initialData) return;
|
||||
|
||||
let xblockElement;
|
||||
let xblockWrapper;
|
||||
|
||||
const data = { ...initialData };
|
||||
data.payload = { ...data?.payload, ...data?.payload?.data };
|
||||
|
||||
if (data.payload && data.payload.locator) {
|
||||
xblockElement = $(`[data-locator="${data.payload.locator}"]`);
|
||||
xblockWrapper = $("li.studio-xblock-wrapper[data-locator='" + data.payload.locator + "']");
|
||||
@@ -173,6 +178,25 @@ function($, _, Backbone, gettext, BasePage,
|
||||
this.listenTo(Backbone, 'move:onXBlockMoved', this.onXBlockMoved);
|
||||
},
|
||||
|
||||
postMessageToParent: function(body, callbackFn = null) {
|
||||
try {
|
||||
window.parent.postMessage(body, document.referrer);
|
||||
if (callbackFn) {
|
||||
callbackFn();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to post message:', e);
|
||||
}
|
||||
},
|
||||
|
||||
postMessageForHideProcessingNotification: function () {
|
||||
this.postMessageToParent({
|
||||
type: 'hideProcessingNotification',
|
||||
message: 'Hide processing notification',
|
||||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
getViewParameters: function() {
|
||||
return {
|
||||
el: this.$('.wrapper-xblock'),
|
||||
@@ -237,18 +261,14 @@ function($, _, Backbone, gettext, BasePage,
|
||||
const scrollOffset = scrollOffsetString ? parseInt(scrollOffsetString, 10) : 0;
|
||||
|
||||
if (scrollOffset) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'scrollToXBlock',
|
||||
message: 'Scroll to XBlock',
|
||||
payload: { scrollOffset }
|
||||
}, document.referrer
|
||||
);
|
||||
localStorage.removeItem('modalEditLastYPosition');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
self.postMessageToParent(
|
||||
{
|
||||
type: 'scrollToXBlock',
|
||||
message: 'Scroll to XBlock',
|
||||
payload: { scrollOffset }
|
||||
},
|
||||
() => localStorage.removeItem('modalEditLastYPosition')
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -272,13 +292,14 @@ function($, _, Backbone, gettext, BasePage,
|
||||
|
||||
renderAddXBlockComponents: function() {
|
||||
var self = this;
|
||||
if (self.options.canEdit && !self.options.isIframeEmbed) {
|
||||
if (self.options.canEdit && (!self.options.isIframeEmbed || self.isSplitTestContentPage)) {
|
||||
this.$('.add-xblock-component').each(function(index, element) {
|
||||
var component = new AddXBlockComponent({
|
||||
el: element,
|
||||
createComponent: _.bind(self.createComponent, self),
|
||||
collection: self.options.templates,
|
||||
libraryContentPickerUrl: self.options.libraryContentPickerUrl,
|
||||
isIframeEmbed: self.options.isIframeEmbed,
|
||||
});
|
||||
component.render();
|
||||
});
|
||||
@@ -288,7 +309,7 @@ function($, _, Backbone, gettext, BasePage,
|
||||
},
|
||||
|
||||
initializePasteButton() {
|
||||
if (this.options.canEdit && !this.options.isIframeEmbed) {
|
||||
if (this.options.canEdit && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) {
|
||||
// We should have the user's clipboard status.
|
||||
const data = this.options.clipboardData;
|
||||
this.refreshPasteButton(data);
|
||||
@@ -305,7 +326,8 @@ function($, _, Backbone, gettext, BasePage,
|
||||
refreshPasteButton(data) {
|
||||
// Do not perform any changes on paste button since they are not
|
||||
// rendered on Library or LibraryContent pages
|
||||
if (!this.isLibraryPage && !this.isLibraryContentPage && !this.options.isIframeEmbed) {
|
||||
if (!this.isLibraryPage && !this.isLibraryContentPage && (!this.options.isIframeEmbed || this.isSplitTestContentPage)) {
|
||||
this.postMessageForHideProcessingNotification();
|
||||
// 'data' is the same data returned by the "get clipboard status" API endpoint
|
||||
// i.e. /api/content-staging/v1/clipboard/
|
||||
if (this.options.canEdit && data.content) {
|
||||
@@ -340,17 +362,11 @@ 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);
|
||||
if (this.options.isIframeEmbed) {
|
||||
this.postMessageToParent({
|
||||
type: 'pasteComponent',
|
||||
payload: {},
|
||||
});
|
||||
}
|
||||
// Get the ID of the container (usually a unit/vertical) that we're pasting into:
|
||||
const parentElement = this.findXBlockElement(event.target);
|
||||
@@ -375,6 +391,9 @@ function($, _, Backbone, gettext, BasePage,
|
||||
placeholderElement.remove();
|
||||
});
|
||||
}).done((data) => {
|
||||
if (this.options.isIframeEmbed) {
|
||||
this.postMessageForHideProcessingNotification();
|
||||
}
|
||||
const {
|
||||
conflicting_files: conflictingFiles,
|
||||
error_files: errorFiles,
|
||||
@@ -646,17 +665,11 @@ function($, _, Backbone, gettext, BasePage,
|
||||
subMenu.classList.toggle('is-shown');
|
||||
|
||||
if (!subMenu.classList.contains('is-shown') && this.options.isIframeEmbed) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: { courseXBlockDropdownHeight: 0 }
|
||||
}, document.referrer
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
this.postMessageToParent({
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: { courseXBlockDropdownHeight: 0 }
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the viewport height and the dropdown menu height.
|
||||
@@ -668,33 +681,21 @@ function($, _, Backbone, gettext, BasePage,
|
||||
|
||||
if (courseUnitXBlockIframeHeight < courseXBlockDropdownHeight) {
|
||||
// If the dropdown menu is taller than the iframe, adjust the height of the dropdown menu.
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: { courseXBlockDropdownHeight },
|
||||
}, document.referrer
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
this.postMessageToParent({
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: { courseXBlockDropdownHeight },
|
||||
});
|
||||
} else if ((courseXBlockDropdownHeight + clickYPosition) > courseUnitXBlockIframeHeight) {
|
||||
if (courseXBlockDropdownHeight > courseUnitXBlockIframeHeight / 2) {
|
||||
// If the dropdown menu is taller than half the iframe, send a message to adjust its height.
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: {
|
||||
courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2,
|
||||
},
|
||||
}, document.referrer
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
this.postMessageToParent({
|
||||
type: 'toggleCourseXBlockDropdown',
|
||||
message: 'Adjust the height of the dropdown menu',
|
||||
payload: {
|
||||
courseXBlockDropdownHeight: courseXBlockDropdownHeight / 2,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Move the dropdown menu upward to prevent it from overflowing out of the viewport.
|
||||
if (this.options.isIframeEmbed) {
|
||||
@@ -719,18 +720,12 @@ function($, _, Backbone, gettext, BasePage,
|
||||
},
|
||||
|
||||
openManageTags: function(event) {
|
||||
const contentId = this.findXBlockElement(event.target).data('locator');
|
||||
try {
|
||||
if (this.options.isIframeEmbed) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'openManageTags',
|
||||
payload: { contentId }
|
||||
}, document.referrer
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const contentId = this.findXBlockElement(event.target).data('locator');
|
||||
if (this.options.isIframeEmbed) {
|
||||
this.postMessageToParent({
|
||||
type: 'openManageTags',
|
||||
payload: { contentId },
|
||||
});
|
||||
}
|
||||
const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
|
||||
|
||||
@@ -747,13 +742,17 @@ function($, _, Backbone, gettext, BasePage,
|
||||
const usageId = encodeURI(primaryHeader.attr('data-usage-id'));
|
||||
try {
|
||||
if (this.options.isIframeEmbed) {
|
||||
return window.parent.postMessage(
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'copyXBlock',
|
||||
type: this.isSplitTestContentPage ? 'copyXBlockLegacy' : 'copyXBlock',
|
||||
message: 'Copy the XBlock',
|
||||
payload: { usageId }
|
||||
}, document.referrer
|
||||
);
|
||||
|
||||
if (!this.isSplitTestContentPage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -795,6 +794,7 @@ function($, _, Backbone, gettext, BasePage,
|
||||
setTimeout(checkStatus, 1_000);
|
||||
return deferred;
|
||||
} else {
|
||||
this.postMessageForHideProcessingNotification();
|
||||
throw new Error(`Unexpected clipboard status "${status}" in successful API response.`);
|
||||
}
|
||||
});
|
||||
@@ -909,15 +909,12 @@ function($, _, Backbone, gettext, BasePage,
|
||||
this.deleteComponent(this.findXBlockElement(event.target));
|
||||
},
|
||||
|
||||
createPlaceholderElement: function() {
|
||||
return $('<div/>', {class: 'studio-xblock-wrapper'});
|
||||
},
|
||||
|
||||
createComponent: function(template, target, iframeMessageData) {
|
||||
// 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),
|
||||
self = this,
|
||||
parentLocator = parentElement.data('locator'),
|
||||
buttonPanel = target?.closest('.add-xblock-component'),
|
||||
listPanel = buttonPanel?.prev(),
|
||||
@@ -929,28 +926,55 @@ function($, _, Backbone, gettext, BasePage,
|
||||
placeholderElement,
|
||||
$container;
|
||||
|
||||
if (this.options.isIframeEmbed) {
|
||||
if (this.options.isIframeEmbed && !this.isSplitTestContentPage) {
|
||||
$container = $('ol.reorderable-container.ui-sortable');
|
||||
scrollOffset = 0;
|
||||
} else {
|
||||
$container = listPanel;
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel);
|
||||
if (!target.length && iframeMessageData.payload.parent_locator) {
|
||||
$container = $('.xblock[data-usage-id="' + iframeMessageData.payload.parent_locator + '"]')
|
||||
.find('ol.reorderable-container.ui-sortable');
|
||||
}
|
||||
if (!iframeMessageData) {
|
||||
scrollOffset = ViewUtils.getScrollOffset(buttonPanel);
|
||||
}
|
||||
}
|
||||
|
||||
placeholderElement = $placeholderEl.appendTo($container);
|
||||
|
||||
if (this.options.isIframeEmbed) {
|
||||
if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') {
|
||||
return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data);
|
||||
if (this.options.isIframeEmbed && iframeMessageData) {
|
||||
if (iframeMessageData.payload.data && iframeMessageData.type === 'addXBlock') {
|
||||
return this.onNewXBlock(placeholderElement, scrollOffset, false, iframeMessageData.payload.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.isIframeEmbed && this.isSplitTestContentPage) {
|
||||
this.postMessageToParent({
|
||||
type: 'addNewComponent',
|
||||
message: 'Add new XBlock',
|
||||
payload: {},
|
||||
});
|
||||
if (iframeMessageData) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return $.postJSON(this.getURLRoot() + '/', requestData,
|
||||
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
|
||||
.always(function () {
|
||||
if (self.options.isIframeEmbed && self.isSplitTestContentPage) {
|
||||
self.postMessageToParent({
|
||||
type: 'hideProcessingNotification',
|
||||
message: 'Hide processing notification',
|
||||
payload: {}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
// Remove the placeholder if the update failed
|
||||
placeholderElement.remove();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
duplicateComponent: function(xblockElement) {
|
||||
@@ -966,17 +990,11 @@ function($, _, Backbone, gettext, BasePage,
|
||||
placeholderElement = $placeholderEl.insertAfter(xblockElement);
|
||||
|
||||
if (this.options.isIframeEmbed) {
|
||||
try {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'scrollToXBlock',
|
||||
message: 'Scroll to XBlock',
|
||||
payload: { scrollOffset: xblockElement.height() }
|
||||
}, document.referrer
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.postMessageToParent({
|
||||
type: 'scrollToXBlock',
|
||||
message: 'Scroll to XBlock',
|
||||
payload: { scrollOffset: xblockElement.height() }
|
||||
});
|
||||
|
||||
const messageHandler = ({ data }) => {
|
||||
if (data && data.type === 'completeXBlockDuplicating') {
|
||||
@@ -1028,7 +1046,6 @@ function($, _, Backbone, gettext, BasePage,
|
||||
getSelectedLibraryComponents: function() {
|
||||
var self = this;
|
||||
var locator = this.$el.find('.studio-xblock-wrapper').data('locator');
|
||||
console.log(ModuleUtils);
|
||||
$.getJSON(
|
||||
ModuleUtils.getUpdateUrl(locator) + '/handler/get_block_ids',
|
||||
function(data) {
|
||||
@@ -1065,19 +1082,16 @@ function($, _, Backbone, gettext, BasePage,
|
||||
},
|
||||
|
||||
viewXBlockContent: function(event) {
|
||||
try {
|
||||
if (this.options.isIframeEmbed) {
|
||||
event.preventDefault();
|
||||
var usageId = event.currentTarget.href.split('/').pop() || '';
|
||||
window.parent.postMessage({
|
||||
type: 'handleViewXBlockContent',
|
||||
message: 'View the content of the XBlock',
|
||||
payload: { usageId },
|
||||
}, document.referrer);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (this.options.isIframeEmbed) {
|
||||
event.preventDefault();
|
||||
const usageId = event.currentTarget.href.split('/').pop() || '';
|
||||
const isViewGroupLink = event.currentTarget.classList.contains('xblock-view-group-link');
|
||||
this.postMessageToParent({
|
||||
type: isViewGroupLink ? 'handleViewGroupConfigurations' : 'handleViewXBlockContent',
|
||||
message: isViewGroupLink ? 'View the group configurations page' : 'View the content of the XBlock',
|
||||
payload: { usageId },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1142,6 +1156,17 @@ function($, _, Backbone, gettext, BasePage,
|
||||
destinationUrl = this.$('.xblock-header-primary').attr("authoring_MFE_base_url") + '/' + blockType[1] + '/' + encodeURI(data.locator);
|
||||
}
|
||||
|
||||
if (this.options.isIframeEmbed && this.isSplitTestContentPage) {
|
||||
return this.postMessageToParent({
|
||||
type: 'handleRedirectToXBlockEditPage',
|
||||
message: 'Redirect to xBlock edit page',
|
||||
payload: {
|
||||
type: blockType[1],
|
||||
locator: encodeURI(data.locator),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
window.location.href = destinationUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
body,
|
||||
#main {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[class*="view-"] .wrapper {
|
||||
.inner-wrapper {
|
||||
max-width: 100%;
|
||||
@@ -39,67 +44,105 @@ html {
|
||||
|
||||
.actions-list .action-item .action-button {
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: ($baseline * .3);
|
||||
padding: ($baseline * .15) ($baseline / 2);
|
||||
|
||||
&:hover {
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.level-page .xblock-message {
|
||||
padding: ($baseline * .75) ($baseline * 1.2);
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
&.information {
|
||||
color: $text-color;
|
||||
background-color: $xblock-message-info-bg;
|
||||
border-color: $xblock-message-info-border-color;
|
||||
}
|
||||
|
||||
&.validation.has-warnings {
|
||||
color: $black;
|
||||
background-color: $xblock-message-warning-bg;
|
||||
border-color: $xblock-message-warning-border-color;
|
||||
border-top-width: 1px;
|
||||
|
||||
.icon {
|
||||
color: $xblock-message-warning-border-color;
|
||||
.action-button-text {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.xblock-author_view-library_content > .wrapper-xblock-message .xblock-message {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
border-radius: 4px;
|
||||
padding: ($baseline * 1.2);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15);
|
||||
margin-bottom: ($baseline * 1.4);
|
||||
&.level-page {
|
||||
.xblock-message {
|
||||
padding: ($baseline * .75) ($baseline * 1.2);
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
.xblock-message-list {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&.information,
|
||||
&.validation.has-warnings,
|
||||
&.validation.has-errors {
|
||||
color: $black;
|
||||
border-width: 0;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
padding: ($baseline * 1.2);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
&.information {
|
||||
background-color: $xblock-message-info-bg;
|
||||
|
||||
.icon {
|
||||
color: $xblock-message-info-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.validation.has-warnings {
|
||||
background-color: $xblock-message-warning-bg;
|
||||
|
||||
.icon {
|
||||
color: $xblock-message-warning-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.validation.has-errors {
|
||||
background-color: $xblock-message-error-bg;
|
||||
|
||||
.icon {
|
||||
color: $xblock-message-error-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.studio-xblock-wrapper > .wrapper-xblock-message .xblock-message,
|
||||
.xblock > .wrapper-xblock-message .xblock-message {
|
||||
border-radius: 4px;
|
||||
margin-bottom: ($baseline * 1.4);
|
||||
}
|
||||
}
|
||||
|
||||
.xblock-author_view-split_test .wrapper-xblock {
|
||||
background: $white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
&.level-element {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15);
|
||||
margin: 0 0 ($baseline * 1.4) 0;
|
||||
}
|
||||
|
||||
&.level-element .xblock-header-primary {
|
||||
background-color: $white;
|
||||
}
|
||||
.xblock-header-primary {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&.level-element .xblock-render {
|
||||
background: $white;
|
||||
margin: 0;
|
||||
padding: $baseline;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
.xblock-render {
|
||||
background: $white;
|
||||
margin: 0;
|
||||
padding: $baseline;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-xblock .header-actions .actions-list {
|
||||
.wrapper-nav-sub {
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.action-actions-menu:last-of-type .nav-sub {
|
||||
right: 120px;
|
||||
}
|
||||
@@ -176,6 +219,13 @@ html {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-groups.is-inactive {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .15), 0 2px 8px rgba(0, 0, 0, .15);
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
margin: ($baseline * 1.5) ($baseline / 2) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-xblock-modal select {
|
||||
@@ -443,8 +493,8 @@ html {
|
||||
}
|
||||
|
||||
&.xmodule_DoneXBlock {
|
||||
margin-top: 60px;
|
||||
padding: 0 20px;
|
||||
margin-top: ($baseline * 3);
|
||||
padding: 0 $baseline;
|
||||
}
|
||||
|
||||
.xblock-actions {
|
||||
@@ -578,7 +628,7 @@ html {
|
||||
}
|
||||
|
||||
body [class*="view-"] .openassessment_editor_buttons.xblock-actions {
|
||||
padding: 15px 2% 3px 2%;
|
||||
padding: ($baseline * .75) 2% ($baseline * .15) 2%;
|
||||
}
|
||||
|
||||
[class*="view-"] {
|
||||
@@ -634,7 +684,7 @@ body [class*="view-"] .openassessment_editor_buttons.xblock-actions {
|
||||
.list-input.settings-list {
|
||||
.field.comp-setting-entry.is-set .setting-input {
|
||||
color: $text-color;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: ($baseline * .25);
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -733,7 +783,7 @@ select {
|
||||
#openassessment_editor_header .editor_tabs .oa_editor_tab {
|
||||
@extend %light-button;
|
||||
|
||||
padding: 0 10px;
|
||||
padding: 0 ($baseline / 2);
|
||||
}
|
||||
|
||||
#openassessment_editor_header,
|
||||
@@ -762,7 +812,7 @@ select {
|
||||
#oa_rubric_editor_wrapper .openassessment_criterion_option
|
||||
.openassessment_criterion_option_point_wrapper label input {
|
||||
min-width: 70px;
|
||||
font-size: 18px;
|
||||
font-size: $base-font-size;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
@@ -835,7 +885,7 @@ select {
|
||||
width: 100%;
|
||||
|
||||
&.background-url {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: ($baseline / 2);
|
||||
}
|
||||
|
||||
&.autozone-layout {
|
||||
@@ -858,3 +908,104 @@ select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.xblock-render {
|
||||
.add-xblock-component {
|
||||
background: transparent;
|
||||
padding: $baseline;
|
||||
|
||||
.new-component {
|
||||
h5 {
|
||||
margin-bottom: ($baseline * 1.2);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ($baseline * .6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.add-xblock-component-button {
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15);
|
||||
width: 176px;
|
||||
height: 110px;
|
||||
color: $primary;
|
||||
border-color: $primary;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: ($baseline * .4);
|
||||
|
||||
&:hover {
|
||||
color: darken($primary, 10%);
|
||||
background-color: lighten($primary, 80%);
|
||||
border-color: darken($primary, 15%);
|
||||
}
|
||||
|
||||
.large-template-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: $primary;
|
||||
|
||||
@each $name, $file in $template-icon-map {
|
||||
&.large-#{$name}-icon {
|
||||
mask: url("#{$static-path}/images/#{$file}.svg") center no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
color: inherit;
|
||||
font-size: 15.75px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.beta {
|
||||
color: $white;
|
||||
background-color: $primary;
|
||||
padding: ($baseline * .1) ($baseline * .4) ($baseline * .2);
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin: -($baseline * .3) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-templates {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .15), 0 1px 4px rgba(0, 0, 0, .15);
|
||||
margin: $baseline;
|
||||
overflow: hidden;
|
||||
|
||||
.button-component:hover {
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend %primary-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.paste-component {
|
||||
margin: ($baseline * 1.2) ($baseline / 2) 0;
|
||||
|
||||
.paste-component-whats-in-clipboard .clipboard-details-popup {
|
||||
right: ($baseline / 2 * -1);
|
||||
}
|
||||
|
||||
.paste-component-button.button {
|
||||
@extend %button-primary-outline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
display: block;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background: darken($primary, 5%);
|
||||
@@ -46,6 +48,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
%button-primary-outline {
|
||||
@extend %modal-actions-button;
|
||||
|
||||
color: $primary;
|
||||
border-color: $primary;
|
||||
text-shadow: none;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
|
||||
&:focus {
|
||||
color: $primary;
|
||||
background: transparent;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -5px;
|
||||
border: 2px solid $primary;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: darken($primary, 10%);
|
||||
background-color: lighten($primary, 80%);
|
||||
border-color: darken($primary, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
%light-button {
|
||||
@extend %modal-actions-button;
|
||||
|
||||
|
||||
@@ -317,6 +317,23 @@ $dark: #212529;
|
||||
$zindex-dropdown: 100;
|
||||
|
||||
$xblock-message-info-bg: #eff8fa;
|
||||
$xblock-message-info-border-color: #9cd2e6;
|
||||
$xblock-message-info-icon-color: #9cd2e6;
|
||||
|
||||
$xblock-message-warning-bg: #fffdf0;
|
||||
$xblock-message-warning-border-color: #fff6bf;
|
||||
$xblock-message-warning-icon-color: #ffd900;
|
||||
|
||||
$xblock-message-error-bg: #fbf2f3;
|
||||
$xblock-message-error-icon-color: #c32d3a;
|
||||
|
||||
$template-icon-map: (
|
||||
"library": "library-icon",
|
||||
"library_v2": "library_v2-icon",
|
||||
"itembank": "itembank-icon",
|
||||
"advanced": "advanced-icon",
|
||||
"html": "text-icon",
|
||||
"openassessment": "openassessment-icon",
|
||||
"problem": "problem-icon",
|
||||
"video": "video-icon",
|
||||
"drag-and-drop-v2": "drag-and-drop-v2-icon",
|
||||
"text": "text-icon"
|
||||
);
|
||||
|
||||
@@ -201,6 +201,7 @@ from openedx.core.release import RELEASE_LINE
|
||||
outlineURL: "${outline_url | n, js_escaped_string}",
|
||||
clipboardData: ${user_clipboard | n, dump_js_escaped_json},
|
||||
isIframeEmbed: true,
|
||||
libraryContentPickerUrl: "${library_content_picker_url | n, js_escaped_string}",
|
||||
}
|
||||
);
|
||||
</%static:webpack>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% if (support_legend.show_legend) { %>
|
||||
<span class="support-documentation">
|
||||
<a class="support-documentation-link"
|
||||
href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/create_exercises_and_tools.html#levels-of-support-for-tools" rel="noopener" target="_blank">
|
||||
href="https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_problem_types.html" rel="noopener" target="_blank">
|
||||
<%- support_legend.documentation_label %>
|
||||
</a>
|
||||
<span class="support-documentation-level">
|
||||
|
||||
@@ -8,4 +8,4 @@ We make use of the `social-auth-app-django`_ as our backend library for this dja
|
||||
To enable this feature, check out the `third party authentication documentation`.
|
||||
|
||||
.. _social-auth-app-django: https://github.com/python-social-auth/social-app-django
|
||||
.. _third party authentication documentation: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html
|
||||
.. _third party authentication documentation: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html
|
||||
|
||||
@@ -12,4 +12,4 @@ Glossary
|
||||
|
||||
More Documentation
|
||||
==================
|
||||
`Events in the Tracking Logs <https://edx.readthedocs.io/projects/devdata/en/stable/internal_data_formats/tracking_logs.html>`_
|
||||
`Events in the Tracking Logs <https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/index.html>`_
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<article class="response">
|
||||
<h3>What web browser should I use?</h3>
|
||||
<p>The Open edX platform works best with current versions of Chrome, Firefox or Safari, or with Internet Explorer version 9 and above.</p>
|
||||
<p>See our <a href="https://edx.readthedocs.org/projects/open-edx-learner-guide/en/latest/front_matter/browsers.html">list of supported browsers</a> for the most up-to-date information.</p>
|
||||
<p>See our <a href="https://docs.openedx.org/en/latest/developers/references/developer_guide/testing/browsers.html">list of supported browsers</a> for the most up-to-date information.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
|
||||
@@ -69,12 +69,12 @@ If you want to provide learners with new content experiences within courses, opt
|
||||
|
||||
For a more detailed comparison of content integration options, see `Options for Extending the edX Platform`_ in the *Open edX Developer's Guide*.
|
||||
|
||||
.. _XBlock tutorial: https://edx.readthedocs.io/projects/xblock-tutorial/en/latest/
|
||||
.. _as a consumer: https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/lti_component.html
|
||||
.. _as a provider: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/lti/
|
||||
.. _Options for Extending the edX Platform: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/extending_platform/extending.html
|
||||
.. _custom JavaScript application: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/extending_platform/javascript.html
|
||||
.. _external grader documentation: https://edx.readthedocs.io/projects/open-edx-ca/en/latest/exercises_tools/external_graders.html
|
||||
.. _XBlock tutorial: https://docs.openedx.org/projects/xblock/en/latest/xblock-tutorial/index.html
|
||||
.. _as a consumer: https://docs.openedx.org/en/latest/educators/navigation/components_activities.html#lti-component
|
||||
.. _as a provider: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/lti/index.html
|
||||
.. _Options for Extending the edX Platform: https://docs.openedx.org/en/latest/developers/references/developer_guide/extending_platform/extending.html
|
||||
.. _custom JavaScript application: https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/custom_javascript.html
|
||||
.. _external grader documentation: https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_external_graders.html
|
||||
.. _You can follow this guide to install and enable custom TinyMCE plugins: extensions/tinymce_plugins.rst
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ Here are the different integration points that python plugins can use:
|
||||
.. _course tabs documentation: https://openedx.atlassian.net/wiki/spaces/AC/pages/30965919/Adding+a+new+course+tab
|
||||
.. |course_tools.py| replace:: ``course_tools.py``
|
||||
.. _course_tools.py: https://github.com/openedx/edx-platform/blob/master/openedx/features/course_experience/course_tools.py
|
||||
.. _Adding Custom Fields to the Registration Page: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/customize_registration_page.html
|
||||
.. _Adding Custom Fields to the Registration Page: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/customize_registration_page.html
|
||||
.. |learning_context.py| replace:: ``learning_context.py``
|
||||
.. _learning_context.py: https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/xblock/learning_context/learning_context.py
|
||||
.. |UserPartition docstring| replace:: ``UserPartition`` docstring
|
||||
@@ -189,8 +189,8 @@ In addition, Open edX operators will be able to replace entire MFEs with complet
|
||||
|
||||
.. |example edx theme| replace:: example ``edx`` theme
|
||||
.. _example edx theme: https://github.com/openedx/paragon/tree/master/scss/edx
|
||||
.. _Changing Themes for an Open edX Site: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/changing_appearance/theming/
|
||||
.. _Overriding Brand Specific Elements: https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#overriding-brand-specific-elements
|
||||
.. _Changing Themes for an Open edX Site: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/changing_appearance/theming/index.html
|
||||
.. _Overriding Brand Specific Elements: https://github.com/openedx/brand-openedx
|
||||
|
||||
Custom frontends
|
||||
****************
|
||||
|
||||
@@ -21,8 +21,8 @@ In the LMS, the following technologies can be used to access course content and
|
||||
|
||||
.. _edX DDD Ubiquitous Language: https://openedx.atlassian.net/wiki/spaces/AC/pages/188032048/edX+DDD+Ubiquitous+Language
|
||||
.. _Course Overviews: https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/content/course_overviews/__init__.py
|
||||
.. _Course Blocks: https://openedx.atlassian.net/wiki/display/EDUCATOR/Course+Blocks
|
||||
.. _Modulestore: https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/modulestores/index.html
|
||||
.. _Course Blocks: https://openedx.atlassian.net/wiki/spaces/AC/pages/158321366/Course+Blocks+aka+xblocks+components
|
||||
.. _Modulestore: https://docs.openedx.org/projects/edx-platform/en/latest/references/docs/xmodule/modulestore/docs/overview.html
|
||||
|
||||
Decisions
|
||||
=========
|
||||
|
||||
@@ -880,7 +880,14 @@ def display_date_for_certificate(course, certificate):
|
||||
|
||||
if _course_uses_available_date(course) and course.certificate_available_date < datetime.now(UTC):
|
||||
return course.certificate_available_date
|
||||
elif course.certificates_display_behavior == CertificatesDisplayBehaviors.END and course.end:
|
||||
# It is possible for a self-paced course run to end up configured with a display behavior of "END" even though it
|
||||
# shouldn't be a valid option. We must check if the course is instructor-paced here to ensure that we are selecting
|
||||
# the correct date to display.
|
||||
elif (
|
||||
not course.self_paced
|
||||
and course.certificates_display_behavior == CertificatesDisplayBehaviors.END
|
||||
and course.end
|
||||
):
|
||||
return course.end
|
||||
else:
|
||||
return certificate.modified_date
|
||||
|
||||
@@ -117,9 +117,9 @@ Related DEPR (edX deprecation process) tickets:
|
||||
* `Remove PDF generation code`_
|
||||
* `Remove PDF view code`_
|
||||
|
||||
.. _Enable Course Certificates: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/enable_certificates.html
|
||||
.. _Enable Course Certificates: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/enable_certificates.html
|
||||
.. _Deprecate web certificate setting: https://github.com/openedx/edx-platform/pull/17285
|
||||
.. _Disable PDF certificate generation: https://github.com/openedx/edx-platform/pull/19833
|
||||
.. _Set Up Certificates in Studio: https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/set_up_course/studio_add_course_information/studio_creating_certificates.html
|
||||
.. _Set Up Certificates in Studio: https://docs.openedx.org/en/latest/educators/how-tos/set_up_course/manage_certificates.html
|
||||
.. _Remove PDF generation code: https://openedx.atlassian.net/browse/DEPR-155
|
||||
.. _Remove PDF view code: https://openedx.atlassian.net/browse/DEPR-157
|
||||
|
||||
@@ -1141,6 +1141,7 @@ class CertificatesApiTestCase(TestCase):
|
||||
certificate instance when the display behavior is set to EARLY_NO_INFO.
|
||||
"""
|
||||
with configure_waffle_namespace(True):
|
||||
self.course.self_paced = False
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.certificate.modified_date
|
||||
|
||||
@@ -1150,6 +1151,7 @@ class CertificatesApiTestCase(TestCase):
|
||||
associated with the course when the display behavior is set to END_WITH_DATE.
|
||||
"""
|
||||
with configure_waffle_namespace(True):
|
||||
self.course.self_paced = False
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.END_WITH_DATE
|
||||
self.course.certificate_available_date = datetime(2017, 2, 1, tzinfo=pytz.UTC)
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.course.certificate_available_date
|
||||
@@ -1160,6 +1162,7 @@ class CertificatesApiTestCase(TestCase):
|
||||
when the display behavior is set to END.
|
||||
"""
|
||||
with configure_waffle_namespace(True):
|
||||
self.course.self_paced = False
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.END
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.course.end
|
||||
|
||||
@@ -1172,6 +1175,27 @@ class CertificatesApiTestCase(TestCase):
|
||||
self.certificate.date_override = datetime(2016, 1, 1, tzinfo=pytz.UTC)
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.certificate.date_override.date
|
||||
|
||||
def test_display_date_for_self_paced_course_run(self):
|
||||
"""
|
||||
Test to verify that the "earned date" displayed on a course certificate is the last modified date of a
|
||||
certificate instance when the display behavior is set to EARLY_NO_INFO and the course run is self-paced.
|
||||
"""
|
||||
with configure_waffle_namespace(True):
|
||||
self.course.self_paced = True
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.EARLY_NO_INFO
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.certificate.modified_date
|
||||
|
||||
def test_display_date_for_self_paced_course_run_with_cdb_end(self):
|
||||
"""
|
||||
Test for a bug fix and some defensive coding. It is possible for self-paced course runs to end up with a display
|
||||
behavior of END. This test ensures that we select the correct issue date even when the course run's
|
||||
configuration is unexpected.
|
||||
"""
|
||||
with configure_waffle_namespace(True):
|
||||
self.course.self_paced = True
|
||||
self.course.certificates_display_behavior = CertificatesDisplayBehaviors.END
|
||||
assert display_date_for_certificate(self.course, self.certificate) == self.certificate.modified_date
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CertificatesMessagingTestCase(ModuleStoreTestCase):
|
||||
|
||||
@@ -79,13 +79,13 @@ Overall Course Grade
|
||||
|
||||
- A learner's overall numerical grade in the course can range anywhere between 0% to 100%.
|
||||
|
||||
- Course teams set the `grade range <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/grade_range.html>`_ and specify the Pass / Fail threshold (for example, a minimum of 50/100 is required to Pass).
|
||||
- Course teams set the `grade range <https://docs.openedx.org/en/latest/educators/how-tos/grading/set_grade_range.html>`_ and specify the Pass / Fail threshold (for example, a minimum of 50/100 is required to Pass).
|
||||
|
||||
- The Passing grade range can be further divided into letter grades, such as A, B, etc.
|
||||
|
||||
Assignment Weights
|
||||
|
||||
* Course teams set the `assignment types <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/grading/configure_assignment_type.html>`_ used in the course, along with their weights and the number of allowed drops (number of assignments with the lowest grades that can be discarded in the final grade computation).
|
||||
* Course teams set the `assignment types <https://docs.openedx.org/en/latest/educators/references/grading/gradebook_assignment_types.html>`_ used in the course, along with their weights and the number of allowed drops (number of assignments with the lowest grades that can be discarded in the final grade computation).
|
||||
|
||||
Computation
|
||||
|
||||
@@ -118,9 +118,9 @@ Problem Scores
|
||||
|
||||
- **automatically scored, synchronously** at the time of submission, such as for most Capa-based problems
|
||||
|
||||
- **automatically scored, asynchronously** via an `external grader service <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/external_graders.html>`_
|
||||
- **automatically scored, asynchronously** via an `external grader service <https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_external_graders.html>`_
|
||||
|
||||
- **manually scored**, such as for `Open Response Assessments <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/OpenResponseAssessments.html>`_, where the calculation requires human input from either
|
||||
- **manually scored**, such as for `Open Response Assessments <https://docs.openedx.org/en/latest/educators/navigation/components_activities.html#open-response-assessments>`_, where the calculation requires human input from either
|
||||
|
||||
- a single course staff (staff assessment)
|
||||
|
||||
@@ -153,19 +153,19 @@ As described above in the Grade Computation section, the grading policy is distr
|
||||
|
||||
- A problem's external grader configuration
|
||||
|
||||
- A problem's individual grading policy - as currently supported by `ORA's assessment configuration <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/OpenResponseAssessments.html#how-scores-for-open-response-assessments-are-calculated>`_
|
||||
- A problem's individual grading policy - as currently supported by `ORA's assessment configuration <https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_OpenResponseAssessments.html#how-scores-for-open-response-assessments-are-calculated>`_
|
||||
|
||||
|
||||
Grade Overrides/Exceptions
|
||||
--------------------------
|
||||
|
||||
Today, we support the following features to `adjust grades <https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-koa.master/student_progress/course_grades.html#adjust-grades-for-one-or-all-learners>`_, but don't have a general feature to override a grade for any xBlock:
|
||||
Today, we support the following features to `adjust grades <https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#adjust-grades-for-one-or-all-learners>`_, but don't have a general feature to override a grade for any xBlock:
|
||||
|
||||
* In `ORA Studio settings <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/open_response_assessments/Manage_ORA_Assignment.html#override-a-learner-s-assessment-grade>`_:
|
||||
* In `ORA Studio settings <https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/Manage_ORA_Assignment.html#override-a-learner-assessment-grade>`_:
|
||||
|
||||
- override a learner's grade for an ORA2 block
|
||||
|
||||
* In LMS Instructor Dashboard or `Staff Debug Info <http://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/manage_live_course/staff_debug_info.html>`_:
|
||||
* In LMS Instructor Dashboard or `Staff Debug Info <https://docs.openedx.org/en/latest/educators/references/data/staff_debug_info.html>`_:
|
||||
|
||||
- reset the number of attempts a learner has made for a problem back to 0
|
||||
|
||||
@@ -173,7 +173,7 @@ Today, we support the following features to `adjust grades <https://edx.readthed
|
||||
|
||||
- delete a student state for a problem
|
||||
|
||||
* In `Gradebook <https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-koa.master/student_progress/course_grades.html#adjust-grades-for-one-or-all-learners>`_:
|
||||
* In `Gradebook <https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#adjust-grades-for-one-or-all-learners>`_:
|
||||
|
||||
- override a subsection grade for a learner
|
||||
- override subsection grades in bulk (master's track only)
|
||||
|
||||
@@ -105,7 +105,7 @@ class InitializeView(StaffGraderBaseView):
|
||||
# This toggle is documented on the edx-ora2 repo in openassessment/xblock/config_mixin.py
|
||||
# Note: Do not copy this practice of directly using a toggle from a library.
|
||||
# Instead, see docs for exposing a wrapper api:
|
||||
# https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/implement_the_right_toggle_type.html#using-other-toggles pylint: disable=line-too-long
|
||||
# https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/implement_the_right_toggle_type.html#using-other-toggles pylint: disable=line-too-long
|
||||
# pylint: disable=toggle-missing-annotation
|
||||
enhanced_staff_grader_flag = CourseWaffleFlag(
|
||||
f"{WAFFLE_NAMESPACE}.{ENHANCED_STAFF_GRADER}",
|
||||
|
||||
@@ -320,7 +320,7 @@ FEATURES = {
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Set to True to enable Custom Courses for edX, a feature that is more commonly known as
|
||||
# CCX. Documentation for configuring and using this feature is available at
|
||||
# https://edx.readthedocs.io/projects/open-edx-ca/en/latest/set_up_course/custom_courses.html
|
||||
# https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/enable_ccx.html
|
||||
# .. toggle_warning: When set to true, 'lms.djangoapps.ccx.overrides.CustomCoursesForEdxOverrideProvider' will
|
||||
# be added to MODULESTORE_FIELD_OVERRIDE_PROVIDERS
|
||||
# .. toggle_use_cases: opt_in, circuit_breaker
|
||||
@@ -633,7 +633,7 @@ FEATURES = {
|
||||
# .. toggle_description: Set to True to enable course certificates on your instance of Open edX.
|
||||
# .. toggle_warning: You must enable this feature flag in both Studio and the LMS and complete the configuration tasks
|
||||
# described here:
|
||||
# https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/enable_certificates.html pylint: disable=line-too-long,useless-suppression
|
||||
# https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/enable_certificates.html pylint: disable=line-too-long,useless-suppression
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2015-03-13
|
||||
# .. toggle_target_removal_date: None
|
||||
@@ -706,7 +706,7 @@ FEATURES = {
|
||||
# and applications.
|
||||
# .. toggle_warning: After enabling this feature flag there are multiple steps involved to configure edX
|
||||
# as LTI provider. Full guide is available here:
|
||||
# https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/lti/index.html
|
||||
# https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/lti/index.html
|
||||
# .. toggle_use_cases: open_edx
|
||||
# .. toggle_creation_date: 2015-04-24
|
||||
# .. toggle_tickets: https://github.com/openedx/edx-platform/pull/7689
|
||||
@@ -2055,8 +2055,7 @@ LOCALE_PATHS = Derived(_make_locale_paths)
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
# Guidelines for translators
|
||||
TRANSLATORS_GUIDE = 'https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/' \
|
||||
'conventions/internationalization/i18n_translators_guide.html'
|
||||
TRANSLATORS_GUIDE = 'https://docs.openedx.org/en/latest/translators/index.html'
|
||||
|
||||
#################################### AWS #######################################
|
||||
# The number of seconds that a generated URL is valid for.
|
||||
@@ -5257,16 +5256,16 @@ SHOW_ACCOUNT_ACTIVATION_CTA = False
|
||||
################# Documentation links for course apps #################
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
CALCULATOR_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/calculator.html"
|
||||
DISCUSSIONS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/create_discussion.html"
|
||||
EDXNOTES_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/notes.html"
|
||||
PROGRESS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html?highlight=progress#hiding-or-showing-the-wiki-or-progress-pages"
|
||||
TEAMS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_features/teams/teams_setup.html"
|
||||
TEXTBOOKS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/textbooks.html"
|
||||
WIKI_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/course_wiki.html"
|
||||
CUSTOM_PAGES_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html#adding-custom-pages"
|
||||
COURSE_BULK_EMAIL_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/manage_live_course/bulk_email.html"
|
||||
ORA_SETTINGS_HELP_URL = "https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_assets/pages.html#configuring-course-level-open-response-assessment-settings"
|
||||
CALCULATOR_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_calculator.html"
|
||||
DISCUSSIONS_HELP_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_discussions.html"
|
||||
EDXNOTES_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/enable_notes.html"
|
||||
PROGRESS_HELP_URL = "https://docs.openedx.org/en/latest/educators/references/data/progress_page.html"
|
||||
TEAMS_HELP_URL = "https://docs.openedx.org/en/latest/educators/navigation/advanced_features.html#use-teams-in-your-course"
|
||||
TEXTBOOKS_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_textbooks.html"
|
||||
WIKI_HELP_URL = "https://docs.openedx.org/en/latest/educators/concepts/communication/about_course_wiki.html"
|
||||
CUSTOM_PAGES_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_custom_page.html"
|
||||
COURSE_BULK_EMAIL_HELP_URL = "https://docs.openedx.org/en/latest/educators/references/communication/bulk_email.html"
|
||||
ORA_SETTINGS_HELP_URL = "https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/Manage_ORA_Assignment.html"
|
||||
|
||||
################# Bulk Course Email Settings #################
|
||||
# If set, recipients of bulk course email messages will be filtered based on the last_login date of their User account.
|
||||
@@ -5425,7 +5424,7 @@ def _should_send_learning_badge_events(settings):
|
||||
# Each topic configuration dictionary contains
|
||||
# * `enabled`: a toggle denoting whether the event will be published to the topic. These should be annotated
|
||||
# according to
|
||||
# https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html
|
||||
# https://docs.openedx.org/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html
|
||||
# * `event_key_field` which is a period-delimited string path to event data field to use as event key.
|
||||
# Note: The topic names should not include environment prefix as it will be dynamically added based on
|
||||
# EVENT_BUS_TOPIC_PREFIX setting.
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<li class="hint-item" id="hint-moreinfo" tabindex="-1">
|
||||
<p>
|
||||
<span class="bold">For detailed information, see
|
||||
<a id="hint-link-first" href="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/completing_assignments/SFD_mathformatting.html#math-formatting" target="_blank" >Entering Mathematical and Scientific Expressions</a> in the
|
||||
<a id="hint-link-second" href="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/index.html" target="_blank" >EdX Learner's Guide</a>.
|
||||
<a id="hint-link-first" href="https://docs.openedx.org/en/latest/learners/completing_assignments/SFD_mathformatting.html#entering-math-expressions-in-assignments-or-the-calculator" target="_blank" >Entering Mathematical and Scientific Expressions</a> in the
|
||||
<a id="hint-link-second" href="https://docs.openedx.org/en/latest/learners/index.html" target="_blank" >Open edX Learner's Guide</a>.
|
||||
</span>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
@@ -24,10 +24,10 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<p class="sr">${_('Use the arrow keys to navigate the tips or use the tab key to return to the calculator')}</p>
|
||||
<p>
|
||||
<span class="bold">
|
||||
${Text(_("For detailed information, see {math_link_start}Entering Mathematical and Scientific Expressions{math_link_end} in the {guide_link_start}edX Guide for Students{guide_link_end}.")).format(
|
||||
math_link_start=HTML('<a href="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/completing_assignments/SFD_mathformatting.html" target="_blank" >'),
|
||||
${Text(_("For detailed information, see {math_link_start}Entering Mathematical and Scientific Expressions{math_link_end} in the {guide_link_start}Open edX Guide for Students{guide_link_end}.")).format(
|
||||
math_link_start=HTML('<a href="https://docs.openedx.org/en/latest/learners/completing_assignments/SFD_mathformatting.html#entering-math-expressions-in-assignments-or-the-calculator" target="_blank" >'),
|
||||
math_link_end=HTML('</a>'),
|
||||
guide_link_start=HTML('<a href="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/index.html" target="_blank">'),
|
||||
guide_link_start=HTML('<a href="https://docs.openedx.org/en/latest/learners/index.html" target="_blank">'),
|
||||
guide_link_end=HTML('</a>'),
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -8,7 +8,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<div class="accomplishment-support-print">
|
||||
<p class="accomplishment-metadata-copy">
|
||||
${Text(_("For tips and tricks on printing your certificate, view the {link_start}Web Certificates help documentation{link_end}.")).format(
|
||||
link_start=HTML('<a target="_blank" href="https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/OpenSFD_certificates.html#print-a-web-certificate">'),
|
||||
link_start=HTML('<a target="_blank" href="https://docs.openedx.org/en/latest/learners/OpenSFD_certificates.html#print-a-web-certificate">'),
|
||||
link_end=HTML('</a>'),
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ show_link = group_configuration_url is not None
|
||||
<p>
|
||||
<span class="message-text">
|
||||
${Text(_("This content experiment uses group configuration '{group_configuration_name}'.")).format(
|
||||
group_configuration_name=Text(HTML("<a href='{}'>{}</a>")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name
|
||||
group_configuration_name=Text(HTML("<a href='{}' class='xblock-view-group-link'>{}</a>")).format(group_configuration_url, user_partition.name) if show_link else user_partition.name
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -6,11 +6,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
from hashlib import blake2b
|
||||
|
||||
from django.utils.text import slugify
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.text import slugify
|
||||
from opaque_keys.edx.keys import LearningContextKey, UsageKey
|
||||
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryContainerLocator
|
||||
from openedx_learning.api.authoring_models import Collection
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
from openedx.core.djangoapps.content.search.models import SearchAccess
|
||||
@@ -19,7 +20,6 @@ from openedx.core.djangoapps.content_libraries import api as lib_api
|
||||
from openedx.core.djangoapps.content_tagging import api as tagging_api
|
||||
from openedx.core.djangoapps.xblock import api as xblock_api
|
||||
from openedx.core.djangoapps.xblock.data import LatestVersion
|
||||
from openedx_learning.api.authoring_models import Collection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -554,7 +554,7 @@ def searchable_doc_for_container(
|
||||
) -> dict:
|
||||
"""
|
||||
Generate a dictionary document suitable for ingestion into a search engine
|
||||
like Meilisearch or Elasticsearch, so that the given collection can be
|
||||
like Meilisearch or Elasticsearch, so that the given container can be
|
||||
found using faceted search.
|
||||
|
||||
If no container is found for the given container key, the returned document
|
||||
@@ -572,33 +572,38 @@ def searchable_doc_for_container(
|
||||
Fields.usage_key: str(container_key), # Field name isn't exact but this is the closest match
|
||||
Fields.block_id: container_key.container_id, # Field name isn't exact but this is the closest match
|
||||
Fields.access_id: _meili_access_id_from_context_key(container_key.library_key),
|
||||
Fields.publish_status: PublishStatus.never,
|
||||
}
|
||||
|
||||
try:
|
||||
container = lib_api.get_container(container_key)
|
||||
except lib_api.ContentLibraryCollectionNotFound:
|
||||
except lib_api.ContentLibraryContainerNotFound:
|
||||
# Container not found, so we can only return the base doc
|
||||
pass
|
||||
return doc
|
||||
|
||||
if container:
|
||||
# TODO: check if there's a more efficient way to load these num_children counts?
|
||||
draft_num_children = len(lib_api.get_container_children(container_key, published=False))
|
||||
draft_num_children = lib_api.get_container_children_count(container_key, published=False)
|
||||
publish_status = PublishStatus.published
|
||||
if container.last_published is None:
|
||||
publish_status = PublishStatus.never
|
||||
elif container.has_unpublished_changes:
|
||||
publish_status = PublishStatus.modified
|
||||
|
||||
doc.update({
|
||||
Fields.display_name: container.display_name,
|
||||
Fields.created: container.created.timestamp(),
|
||||
Fields.modified: container.modified.timestamp(),
|
||||
Fields.num_children: draft_num_children,
|
||||
})
|
||||
library = lib_api.get_library(container_key.library_key)
|
||||
if library:
|
||||
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
|
||||
doc.update({
|
||||
Fields.display_name: container.display_name,
|
||||
Fields.created: container.created.timestamp(),
|
||||
Fields.modified: container.modified.timestamp(),
|
||||
Fields.num_children: draft_num_children,
|
||||
Fields.publish_status: publish_status,
|
||||
})
|
||||
library = lib_api.get_library(container_key.library_key)
|
||||
if library:
|
||||
doc[Fields.breadcrumbs] = [{"display_name": library.title}]
|
||||
|
||||
if container.published_version_num is not None:
|
||||
published_num_children = len(lib_api.get_container_children(container_key, published=True))
|
||||
doc[Fields.published] = {
|
||||
# Fields.published_display_name: container_published.title, TODO: set the published title
|
||||
Fields.published_num_children: published_num_children,
|
||||
}
|
||||
if container.published_version_num is not None:
|
||||
published_num_children = lib_api.get_container_children_count(container_key, published=True)
|
||||
doc[Fields.published] = {
|
||||
# Fields.published_display_name: container_published.title, TODO: set the published title
|
||||
Fields.published_num_children: published_num_children,
|
||||
}
|
||||
|
||||
return doc
|
||||
|
||||
@@ -227,6 +227,7 @@ class TestSearchApi(ModuleStoreTestCase):
|
||||
"display_name": "Unit 1",
|
||||
# description is not set for containers
|
||||
"num_children": 0,
|
||||
"publish_status": "never",
|
||||
"context_key": "lib:org1:lib",
|
||||
"org": "org1",
|
||||
"created": created_date.timestamp(),
|
||||
|
||||
@@ -3,12 +3,13 @@ Tests for the Studio content search documents (what gets stored in the index)
|
||||
"""
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timezone
|
||||
from organizations.models import Organization
|
||||
|
||||
from freezegun import freeze_time
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
from organizations.models import Organization
|
||||
|
||||
from openedx.core.djangoapps.content_tagging import api as tagging_api
|
||||
from openedx.core.djangoapps.content_libraries import api as library_api
|
||||
from openedx.core.djangoapps.content_tagging import api as tagging_api
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -17,13 +18,13 @@ from xmodule.modulestore.tests.factories import BlockFactory, ToyCourseFactory
|
||||
try:
|
||||
# This import errors in the lms because content.search is not an installed app there.
|
||||
from ..documents import (
|
||||
searchable_doc_for_course_block,
|
||||
searchable_doc_tags,
|
||||
searchable_doc_tags_for_collection,
|
||||
searchable_doc_collections,
|
||||
searchable_doc_for_collection,
|
||||
searchable_doc_for_container,
|
||||
searchable_doc_for_course_block,
|
||||
searchable_doc_for_library_block,
|
||||
searchable_doc_tags,
|
||||
searchable_doc_tags_for_collection,
|
||||
)
|
||||
from ..models import SearchAccess
|
||||
except RuntimeError:
|
||||
@@ -522,6 +523,7 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
|
||||
"display_name": "A Unit in the Search Index",
|
||||
# description is not set for containers
|
||||
"num_children": 0,
|
||||
"publish_status": "never",
|
||||
"context_key": "lib:edX:2012_Fall",
|
||||
"access_id": self.library_access_id,
|
||||
"breadcrumbs": [{"display_name": "some content_library"}],
|
||||
@@ -531,6 +533,106 @@ class StudioDocumentsTest(SharedModuleStoreTestCase):
|
||||
# "published" is not set since we haven't published it yet
|
||||
}
|
||||
|
||||
def test_published_container(self):
|
||||
"""
|
||||
Test creating a search document for a published container
|
||||
"""
|
||||
created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
|
||||
with freeze_time(created_date):
|
||||
container_meta = library_api.create_container(
|
||||
self.library.key,
|
||||
container_type=library_api.ContainerType.Unit,
|
||||
slug="unit1",
|
||||
title="A Unit in the Search Index",
|
||||
user_id=None,
|
||||
)
|
||||
library_api.update_container_children(
|
||||
container_meta.container_key,
|
||||
[self.library_block.usage_key],
|
||||
user_id=None,
|
||||
)
|
||||
library_api.publish_changes(self.library.key)
|
||||
|
||||
doc = searchable_doc_for_container(container_meta.container_key)
|
||||
|
||||
assert doc == {
|
||||
"id": "lctedx2012_fallunitunit1-edd13a0c",
|
||||
"block_id": "unit1",
|
||||
"block_type": "unit",
|
||||
"usage_key": "lct:edX:2012_Fall:unit:unit1",
|
||||
"type": "library_container",
|
||||
"org": "edX",
|
||||
"display_name": "A Unit in the Search Index",
|
||||
# description is not set for containers
|
||||
"num_children": 1,
|
||||
"publish_status": "published",
|
||||
"context_key": "lib:edX:2012_Fall",
|
||||
"access_id": self.library_access_id,
|
||||
"breadcrumbs": [{"display_name": "some content_library"}],
|
||||
"created": 1680674828.0,
|
||||
"modified": 1680674828.0,
|
||||
"published": {"num_children": 1},
|
||||
# "tags" should be here but we haven't implemented them yet
|
||||
# "published" is not set since we haven't published it yet
|
||||
}
|
||||
|
||||
def test_published_container_with_changes(self):
|
||||
"""
|
||||
Test creating a search document for a published container
|
||||
"""
|
||||
created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc)
|
||||
with freeze_time(created_date):
|
||||
container_meta = library_api.create_container(
|
||||
self.library.key,
|
||||
container_type=library_api.ContainerType.Unit,
|
||||
slug="unit1",
|
||||
title="A Unit in the Search Index",
|
||||
user_id=None,
|
||||
)
|
||||
library_api.update_container_children(
|
||||
container_meta.container_key,
|
||||
[self.library_block.usage_key],
|
||||
user_id=None,
|
||||
)
|
||||
library_api.publish_changes(self.library.key)
|
||||
block_2 = library_api.create_library_block(
|
||||
self.library.key,
|
||||
"html",
|
||||
"text3",
|
||||
)
|
||||
|
||||
# Add another component after publish
|
||||
with freeze_time(created_date):
|
||||
library_api.update_container_children(
|
||||
container_meta.container_key,
|
||||
[block_2.usage_key],
|
||||
user_id=None,
|
||||
entities_action=authoring_api.ChildrenEntitiesAction.APPEND,
|
||||
)
|
||||
|
||||
doc = searchable_doc_for_container(container_meta.container_key)
|
||||
|
||||
assert doc == {
|
||||
"id": "lctedx2012_fallunitunit1-edd13a0c",
|
||||
"block_id": "unit1",
|
||||
"block_type": "unit",
|
||||
"usage_key": "lct:edX:2012_Fall:unit:unit1",
|
||||
"type": "library_container",
|
||||
"org": "edX",
|
||||
"display_name": "A Unit in the Search Index",
|
||||
# description is not set for containers
|
||||
"num_children": 2,
|
||||
"publish_status": "modified",
|
||||
"context_key": "lib:edX:2012_Fall",
|
||||
"access_id": self.library_access_id,
|
||||
"breadcrumbs": [{"display_name": "some content_library"}],
|
||||
"created": 1680674828.0,
|
||||
"modified": 1680674828.0,
|
||||
"published": {"num_children": 1},
|
||||
# "tags" should be here but we haven't implemented them yet
|
||||
# "published" is not set since we haven't published it yet
|
||||
}
|
||||
|
||||
def test_mathjax_plain_text_conversion_for_search(self):
|
||||
"""
|
||||
Test how an HTML block with mathjax equations gets converted to plain text in search description.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
API for containers (Sections, Subsections, Units) in Content Libraries
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
@@ -9,10 +10,11 @@ from uuid import uuid4
|
||||
|
||||
from django.utils.text import slugify
|
||||
from opaque_keys.edx.locator import (
|
||||
LibraryLocatorV2,
|
||||
LibraryContainerLocator,
|
||||
LibraryLocatorV2,
|
||||
UsageKeyV2,
|
||||
LibraryUsageLocatorV2,
|
||||
)
|
||||
|
||||
from openedx_events.content_authoring.data import LibraryContainerData
|
||||
from openedx_events.content_authoring.signals import (
|
||||
LIBRARY_CONTAINER_CREATED,
|
||||
@@ -22,8 +24,10 @@ from openedx_events.content_authoring.signals import (
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
from openedx_learning.api.authoring_models import Container
|
||||
|
||||
from openedx.core.djangoapps.xblock.api import get_component_from_usage_key
|
||||
|
||||
from ..models import ContentLibrary
|
||||
from .libraries import PublishableItem
|
||||
from .libraries import LibraryXBlockMetadata, PublishableItem
|
||||
|
||||
|
||||
# The public API is only the following symbols:
|
||||
@@ -34,9 +38,12 @@ __all__ = [
|
||||
"get_container",
|
||||
"create_container",
|
||||
"get_container_children",
|
||||
"get_container_children_count",
|
||||
"library_container_locator",
|
||||
"update_container",
|
||||
"delete_container",
|
||||
"update_container_children",
|
||||
"get_containers_contains_component",
|
||||
]
|
||||
|
||||
|
||||
@@ -252,14 +259,79 @@ def get_container_children(
|
||||
"""
|
||||
Get the entities contained in the given container (e.g. the components/xblocks in a unit)
|
||||
"""
|
||||
assert isinstance(container_key, LibraryContainerLocator)
|
||||
content_library = ContentLibrary.objects.get_by_key(container_key.library_key)
|
||||
learning_package = content_library.learning_package
|
||||
assert learning_package is not None
|
||||
container = authoring_api.get_container_by_key(
|
||||
learning_package.id,
|
||||
key=container_key.container_id,
|
||||
container = _get_container(container_key)
|
||||
if container_key.container_type == ContainerType.Unit.value:
|
||||
child_components = authoring_api.get_components_in_unit(container.unit, published=published)
|
||||
return [LibraryXBlockMetadata.from_component(
|
||||
container_key.library_key,
|
||||
entry.component
|
||||
) for entry in child_components]
|
||||
else:
|
||||
child_entities = authoring_api.get_entities_in_container(container, published=published)
|
||||
return [ContainerMetadata.from_container(
|
||||
container_key.library_key,
|
||||
entry.entity
|
||||
) for entry in child_entities]
|
||||
|
||||
|
||||
def get_container_children_count(
|
||||
container_key: LibraryContainerLocator,
|
||||
published=False,
|
||||
) -> int:
|
||||
"""
|
||||
Get the count of entities contained in the given container (e.g. the components/xblocks in a unit)
|
||||
"""
|
||||
container = _get_container(container_key)
|
||||
return authoring_api.get_container_children_count(container, published=published)
|
||||
|
||||
|
||||
def update_container_children(
|
||||
container_key: LibraryContainerLocator,
|
||||
children_ids: list[UsageKeyV2] | list[LibraryContainerLocator],
|
||||
user_id: int | None,
|
||||
entities_action: authoring_api.ChildrenEntitiesAction = authoring_api.ChildrenEntitiesAction.REPLACE,
|
||||
):
|
||||
"""
|
||||
Adds children components or containers to given container.
|
||||
"""
|
||||
library_key = container_key.library_key
|
||||
container_type = container_key.container_type
|
||||
container = _get_container(container_key)
|
||||
match container_type:
|
||||
case ContainerType.Unit.value:
|
||||
components = [get_component_from_usage_key(key) for key in children_ids] # type: ignore[arg-type]
|
||||
new_version = authoring_api.create_next_unit_version(
|
||||
container.unit,
|
||||
components=components, # type: ignore[arg-type]
|
||||
created=datetime.now(),
|
||||
created_by=user_id,
|
||||
entities_action=entities_action,
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Invalid container type: {container_type}")
|
||||
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=library_key,
|
||||
container_key=str(container_key),
|
||||
)
|
||||
)
|
||||
child_entities = authoring_api.get_entities_in_container(container, published=published)
|
||||
# TODO: convert the return type to list[ContainerMetadata | LibraryXBlockMetadata] ?
|
||||
return child_entities
|
||||
|
||||
return ContainerMetadata.from_container(library_key, new_version.container)
|
||||
|
||||
|
||||
def get_containers_contains_component(
|
||||
usage_key: LibraryUsageLocatorV2
|
||||
) -> list[ContainerMetadata]:
|
||||
"""
|
||||
Get containers that contains the component.
|
||||
"""
|
||||
assert isinstance(usage_key, LibraryUsageLocatorV2)
|
||||
component = get_component_from_usage_key(usage_key)
|
||||
containers = authoring_api.get_containers_with_entity(
|
||||
component.publishable_entity.pk,
|
||||
)
|
||||
return [
|
||||
ContainerMetadata.from_container(usage_key.context_key, container)
|
||||
for container in containers
|
||||
]
|
||||
|
||||
@@ -82,6 +82,7 @@ from openedx_events.content_authoring.data import (
|
||||
ContentLibraryData,
|
||||
LibraryBlockData,
|
||||
LibraryCollectionData,
|
||||
LibraryContainerData,
|
||||
ContentObjectChangedData,
|
||||
)
|
||||
from openedx_events.content_authoring.signals import (
|
||||
@@ -92,6 +93,7 @@ from openedx_events.content_authoring.signals import (
|
||||
LIBRARY_BLOCK_DELETED,
|
||||
LIBRARY_BLOCK_UPDATED,
|
||||
LIBRARY_COLLECTION_UPDATED,
|
||||
LIBRARY_CONTAINER_UPDATED,
|
||||
CONTENT_OBJECT_ASSOCIATIONS_CHANGED,
|
||||
)
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -113,6 +115,7 @@ from openedx.core.djangoapps.xblock.api import (
|
||||
xblock_type_display_name,
|
||||
)
|
||||
from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_learning_core
|
||||
from openedx.core.djangoapps.content_libraries import api as lib_api
|
||||
from openedx.core.types import User as UserType
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -302,6 +305,7 @@ class PublishableItem(LibraryItem):
|
||||
last_draft_created_by: str = ""
|
||||
has_unpublished_changes: bool = False
|
||||
collections: list[CollectionMetadata] = field(default_factory=list)
|
||||
can_stand_alone: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -343,6 +347,7 @@ class LibraryXBlockMetadata(PublishableItem):
|
||||
last_draft_created_by=last_draft_created_by,
|
||||
has_unpublished_changes=component.versioning.has_unpublished_changes,
|
||||
collections=associated_collections or [],
|
||||
can_stand_alone=component.publishable_entity.can_stand_alone,
|
||||
)
|
||||
|
||||
|
||||
@@ -899,6 +904,18 @@ def set_library_block_olx(usage_key: LibraryUsageLocatorV2, new_olx_str: str) ->
|
||||
)
|
||||
)
|
||||
|
||||
# For each container, trigger LIBRARY_CONTAINER_UPDATED signal and set background=True to trigger
|
||||
# container indexing asynchronously.
|
||||
affected_containers = lib_api.get_containers_contains_component(usage_key)
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=usage_key.lib_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
return new_component_version
|
||||
|
||||
|
||||
@@ -958,9 +975,17 @@ def validate_can_add_block_to_library(
|
||||
return content_library, usage_key
|
||||
|
||||
|
||||
def create_library_block(library_key, block_type, definition_id, user_id=None):
|
||||
def create_library_block(
|
||||
library_key: LibraryLocatorV2,
|
||||
block_type: str,
|
||||
definition_id: str,
|
||||
user_id: int | None = None,
|
||||
can_stand_alone: bool = True,
|
||||
):
|
||||
"""
|
||||
Create a new XBlock in this library of the specified type (e.g. "html").
|
||||
|
||||
Set can_stand_alone = False when a component is created under a container, like unit.
|
||||
"""
|
||||
# It's in the serializer as ``definition_id``, but for our purposes, it's
|
||||
# the block_id. See the comments in ``LibraryXBlockCreationSerializer`` for
|
||||
@@ -969,7 +994,7 @@ def create_library_block(library_key, block_type, definition_id, user_id=None):
|
||||
|
||||
content_library, usage_key = validate_can_add_block_to_library(library_key, block_type, block_id)
|
||||
|
||||
_create_component_for_block(content_library, usage_key, user_id)
|
||||
_create_component_for_block(content_library, usage_key, user_id, can_stand_alone)
|
||||
|
||||
# Now return the metadata about the new block:
|
||||
LIBRARY_BLOCK_CREATED.send_event(
|
||||
@@ -1135,6 +1160,7 @@ def _create_component_for_block(
|
||||
content_lib: ContentLibrary,
|
||||
usage_key: LibraryUsageLocatorV2,
|
||||
user_id: int | None = None,
|
||||
can_stand_alone: bool = True,
|
||||
):
|
||||
"""
|
||||
Create a Component for an XBlock type, initialize it, and return the ComponentVersion.
|
||||
@@ -1144,6 +1170,8 @@ def _create_component_for_block(
|
||||
will be set as the current draft. This function does not publish the
|
||||
Component.
|
||||
|
||||
Set can_stand_alone = False when a component is created under a container, like unit.
|
||||
|
||||
TODO: We should probably shift this to openedx.core.djangoapps.xblock.api
|
||||
(along with its caller) since it gives runtime storage specifics. The
|
||||
Library-specific logic stays in this module, so "create a block for my lib"
|
||||
@@ -1168,6 +1196,7 @@ def _create_component_for_block(
|
||||
title=display_name,
|
||||
created=now,
|
||||
created_by=user_id,
|
||||
can_stand_alone=can_stand_alone,
|
||||
)
|
||||
content = authoring_api.get_or_create_text_content(
|
||||
learning_package.id,
|
||||
@@ -1191,6 +1220,7 @@ def delete_library_block(usage_key: LibraryUsageLocatorV2, remove_from_parent=Tr
|
||||
component = get_component_from_usage_key(usage_key)
|
||||
library_key = usage_key.context_key
|
||||
affected_collections = authoring_api.get_entity_collections(component.learning_package_id, component.key)
|
||||
affected_containers = lib_api.get_containers_contains_component(usage_key)
|
||||
|
||||
authoring_api.soft_delete_draft(component.pk)
|
||||
|
||||
@@ -1214,6 +1244,19 @@ def delete_library_block(usage_key: LibraryUsageLocatorV2, remove_from_parent=Tr
|
||||
)
|
||||
)
|
||||
|
||||
# For each container, trigger LIBRARY_CONTAINER_UPDATED signal and set background=True to trigger
|
||||
# container indexing asynchronously.
|
||||
#
|
||||
# To update the components count in containers
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=library_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def restore_library_block(usage_key: LibraryUsageLocatorV2) -> None:
|
||||
"""
|
||||
|
||||
@@ -6,8 +6,8 @@ import logging
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
from openedx_events.content_authoring.data import LibraryBlockData
|
||||
from openedx_events.content_authoring.signals import LIBRARY_BLOCK_UPDATED
|
||||
from openedx_events.content_authoring.data import LibraryBlockData, LibraryContainerData
|
||||
from openedx_events.content_authoring.signals import LIBRARY_BLOCK_UPDATED, LIBRARY_CONTAINER_UPDATED
|
||||
from opaque_keys.edx.keys import UsageKeyV2
|
||||
from opaque_keys.edx.locator import LibraryUsageLocatorV2, LibraryLocatorV2
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -114,3 +114,19 @@ class LibraryContextImpl(LearningContext):
|
||||
usage_key=usage_key,
|
||||
)
|
||||
)
|
||||
|
||||
def send_container_updated_events(self, usage_key: UsageKeyV2):
|
||||
"""
|
||||
Send "container updated" events for containers that contains the library block
|
||||
with the given usage_key.
|
||||
"""
|
||||
assert isinstance(usage_key, LibraryUsageLocatorV2)
|
||||
affected_containers = api.get_containers_contains_component(usage_key)
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=usage_key.lib_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -21,8 +21,8 @@ from ..models import ContentLibrary
|
||||
from .utils import convert_exceptions
|
||||
from .serializers import (
|
||||
ContentLibraryCollectionSerializer,
|
||||
ContentLibraryCollectionComponentsUpdateSerializer,
|
||||
ContentLibraryCollectionUpdateSerializer,
|
||||
ContentLibraryComponentKeysSerializer,
|
||||
)
|
||||
from openedx.core.types.http import RestRequest
|
||||
|
||||
@@ -200,7 +200,7 @@ class LibraryCollectionsView(ModelViewSet):
|
||||
content_library = self.get_content_library()
|
||||
collection_key = kwargs["key"]
|
||||
|
||||
serializer = ContentLibraryCollectionComponentsUpdateSerializer(data=request.data)
|
||||
serializer = ContentLibraryComponentKeysSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
usage_keys = serializer.validated_data["usage_keys"]
|
||||
|
||||
@@ -8,10 +8,10 @@ import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.transaction import non_atomic_requests
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryContainerLocator
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_204_NO_CONTENT
|
||||
@@ -124,3 +124,152 @@ class LibraryContainerView(GenericAPIView):
|
||||
)
|
||||
|
||||
return Response({}, status=HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@method_decorator(non_atomic_requests, name="dispatch")
|
||||
@view_auth_classes()
|
||||
class LibraryContainerChildrenView(GenericAPIView):
|
||||
"""
|
||||
View to get or update children of specific container (a section, subsection, or unit)
|
||||
"""
|
||||
serializer_class = serializers.LibraryXBlockMetadataSerializer
|
||||
|
||||
@convert_exceptions
|
||||
@swagger_auto_schema(
|
||||
responses={200: list[serializers.LibraryXBlockMetadataSerializer]}
|
||||
)
|
||||
def get(self, request, container_key: LibraryContainerLocator):
|
||||
"""
|
||||
Get children components of given container
|
||||
Example:
|
||||
GET /api/libraries/v2/containers/<container_key>/children/
|
||||
Result:
|
||||
[
|
||||
{
|
||||
'block_type': 'problem',
|
||||
'can_stand_alone': True,
|
||||
'collections': [],
|
||||
'created': '2025-03-21T13:53:55Z',
|
||||
'def_key': None,
|
||||
'display_name': 'Blank Problem',
|
||||
'has_unpublished_changes': True,
|
||||
'id': 'lb:CL-TEST:containers:problem:Problem1',
|
||||
'last_draft_created': '2025-03-21T13:53:55Z',
|
||||
'last_draft_created_by': 'Bob',
|
||||
'last_published': None,
|
||||
'modified': '2025-03-21T13:53:55Z',
|
||||
'published_by': None,
|
||||
},
|
||||
{
|
||||
'block_type': 'html',
|
||||
'can_stand_alone': False,
|
||||
'collections': [],
|
||||
'created': '2025-03-21T13:53:55Z',
|
||||
'def_key': None,
|
||||
'display_name': 'Text',
|
||||
'has_unpublished_changes': True,
|
||||
'id': 'lb:CL-TEST:containers:html:Html1',
|
||||
'last_draft_created': '2025-03-21T13:53:55Z',
|
||||
'last_draft_created_by': 'Bob',
|
||||
'last_published': None,
|
||||
'modified': '2025-03-21T13:53:55Z',
|
||||
'published_by': None,
|
||||
}
|
||||
]
|
||||
"""
|
||||
published = request.GET.get('published', False)
|
||||
api.require_permission_for_library_key(
|
||||
container_key.library_key,
|
||||
request.user,
|
||||
permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
|
||||
)
|
||||
child_entities = api.get_container_children(container_key, published)
|
||||
if container_key.container_type == api.ContainerType.Unit.value:
|
||||
data = serializers.LibraryXBlockMetadataSerializer(child_entities, many=True).data
|
||||
else:
|
||||
data = serializers.LibraryContainerMetadataSerializer(child_entities, many=True).data
|
||||
return Response(data)
|
||||
|
||||
def _update_component_children(
|
||||
self,
|
||||
request,
|
||||
container_key: LibraryContainerLocator,
|
||||
action: authoring_api.ChildrenEntitiesAction,
|
||||
):
|
||||
"""
|
||||
Helper function to update children in container.
|
||||
"""
|
||||
api.require_permission_for_library_key(
|
||||
container_key.library_key,
|
||||
request.user,
|
||||
permissions.CAN_EDIT_THIS_CONTENT_LIBRARY,
|
||||
)
|
||||
serializer = serializers.ContentLibraryComponentKeysSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
# Only components under units are supported for now.
|
||||
assert container_key.container_type == api.ContainerType.Unit.value
|
||||
|
||||
container = api.update_container_children(
|
||||
container_key,
|
||||
children_ids=serializer.validated_data["usage_keys"],
|
||||
user_id=request.user.id,
|
||||
entities_action=action,
|
||||
)
|
||||
return Response(serializers.LibraryContainerMetadataSerializer(container).data)
|
||||
|
||||
@convert_exceptions
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.ContentLibraryComponentKeysSerializer,
|
||||
responses={200: serializers.LibraryContainerMetadataSerializer}
|
||||
)
|
||||
def post(self, request, container_key: LibraryContainerLocator):
|
||||
"""
|
||||
Add components to unit
|
||||
Example:
|
||||
POST /api/libraries/v2/containers/<container_key>/children/
|
||||
Request body:
|
||||
{"usage_keys": ['lb:CL-TEST:containers:problem:Problem1', 'lb:CL-TEST:containers:html:Html1']}
|
||||
"""
|
||||
return self._update_component_children(
|
||||
request,
|
||||
container_key,
|
||||
action=authoring_api.ChildrenEntitiesAction.APPEND,
|
||||
)
|
||||
|
||||
@convert_exceptions
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.ContentLibraryComponentKeysSerializer,
|
||||
responses={200: serializers.LibraryContainerMetadataSerializer}
|
||||
)
|
||||
def delete(self, request, container_key: LibraryContainerLocator):
|
||||
"""
|
||||
Remove components from unit
|
||||
Example:
|
||||
DELETE /api/libraries/v2/containers/<container_key>/children/
|
||||
Request body:
|
||||
{"usage_keys": ['lb:CL-TEST:containers:problem:Problem1', 'lb:CL-TEST:containers:html:Html1']}
|
||||
"""
|
||||
return self._update_component_children(
|
||||
request,
|
||||
container_key,
|
||||
action=authoring_api.ChildrenEntitiesAction.REMOVE,
|
||||
)
|
||||
|
||||
@convert_exceptions
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.ContentLibraryComponentKeysSerializer,
|
||||
responses={200: serializers.LibraryContainerMetadataSerializer}
|
||||
)
|
||||
def patch(self, request, container_key: LibraryContainerLocator):
|
||||
"""
|
||||
Replace components in unit, can be used to reorder components as well.
|
||||
Example:
|
||||
PATCH /api/libraries/v2/containers/<container_key>/children/
|
||||
Request body:
|
||||
{"usage_keys": ['lb:CL-TEST:containers:problem:Problem1', 'lb:CL-TEST:containers:html:Html1']}
|
||||
"""
|
||||
return self._update_component_children(
|
||||
request,
|
||||
container_key,
|
||||
action=authoring_api.ChildrenEntitiesAction.REPLACE,
|
||||
)
|
||||
|
||||
@@ -159,6 +159,7 @@ class LibraryXBlockMetadataSerializer(serializers.Serializer):
|
||||
tags_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
collections = CollectionMetadataSerializer(many=True, required=False)
|
||||
can_stand_alone = serializers.BooleanField(read_only=True)
|
||||
|
||||
|
||||
class LibraryXBlockTypeSerializer(serializers.Serializer):
|
||||
@@ -193,6 +194,9 @@ class LibraryXBlockCreationSerializer(serializers.Serializer):
|
||||
# creating new block from scratch
|
||||
staged_content = serializers.CharField(required=False)
|
||||
|
||||
# Optional param defaults to True, set to False if block is being created under a container.
|
||||
can_stand_alone = serializers.BooleanField(required=False, default=True)
|
||||
|
||||
|
||||
class LibraryPasteClipboardSerializer(serializers.Serializer):
|
||||
"""
|
||||
@@ -345,7 +349,7 @@ class UsageKeyV2Serializer(serializers.BaseSerializer):
|
||||
raise ValidationError from err
|
||||
|
||||
|
||||
class ContentLibraryCollectionComponentsUpdateSerializer(serializers.Serializer):
|
||||
class ContentLibraryComponentKeysSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for adding/removing Components to/from a Collection.
|
||||
"""
|
||||
|
||||
@@ -33,6 +33,7 @@ URL_LIB_BLOCK_OLX = URL_LIB_BLOCK + 'olx/' # Get or set the OLX of the specifie
|
||||
URL_LIB_BLOCK_ASSETS = URL_LIB_BLOCK + 'assets/' # List the static asset files of the specified XBlock
|
||||
URL_LIB_BLOCK_ASSET_FILE = URL_LIB_BLOCK + 'assets/{file_name}' # Get, delete, or upload a specific static asset file
|
||||
URL_LIB_CONTAINER = URL_PREFIX + 'containers/{container_key}/' # Get a container in this library
|
||||
URL_LIB_CONTAINER_COMPONENTS = URL_LIB_CONTAINER + 'children/' # Get, add or delete a component in this container
|
||||
|
||||
URL_LIB_LTI_PREFIX = URL_PREFIX + 'lti/1.3/'
|
||||
URL_LIB_LTI_JWKS = URL_LIB_LTI_PREFIX + 'pub/jwks/'
|
||||
@@ -229,9 +230,21 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):
|
||||
expect_response
|
||||
)
|
||||
|
||||
def _add_block_to_library(self, lib_key, block_type, slug, parent_block=None, expect_response=200):
|
||||
def _add_block_to_library(
|
||||
self,
|
||||
lib_key,
|
||||
block_type,
|
||||
slug,
|
||||
parent_block=None,
|
||||
can_stand_alone=True,
|
||||
expect_response=200,
|
||||
):
|
||||
""" Add a new XBlock to the library """
|
||||
data = {"block_type": block_type, "definition_id": slug}
|
||||
data = {
|
||||
"block_type": block_type,
|
||||
"definition_id": slug,
|
||||
"can_stand_alone": can_stand_alone,
|
||||
}
|
||||
if parent_block:
|
||||
data["parent_block"] = parent_block
|
||||
return self._api('post', URL_LIB_BLOCKS.format(lib_key=lib_key), data, expect_response)
|
||||
@@ -372,3 +385,54 @@ class ContentLibrariesRestApiTest(APITransactionTestCase):
|
||||
def _delete_container(self, container_key: str, expect_response=204):
|
||||
""" Delete a container (unit etc.) """
|
||||
return self._api('delete', URL_LIB_CONTAINER.format(container_key=container_key), None, expect_response)
|
||||
|
||||
def _get_container_components(self, container_key: str, expect_response=200):
|
||||
""" Get container components"""
|
||||
return self._api(
|
||||
'get',
|
||||
URL_LIB_CONTAINER_COMPONENTS.format(container_key=container_key),
|
||||
None,
|
||||
expect_response
|
||||
)
|
||||
|
||||
def _add_container_components(
|
||||
self,
|
||||
container_key: str,
|
||||
children_ids: list[str],
|
||||
expect_response=200,
|
||||
):
|
||||
""" Add container components"""
|
||||
return self._api(
|
||||
'post',
|
||||
URL_LIB_CONTAINER_COMPONENTS.format(container_key=container_key),
|
||||
{'usage_keys': children_ids},
|
||||
expect_response
|
||||
)
|
||||
|
||||
def _remove_container_components(
|
||||
self,
|
||||
container_key: str,
|
||||
children_ids: list[str],
|
||||
expect_response=200,
|
||||
):
|
||||
""" Remove container components"""
|
||||
return self._api(
|
||||
'delete',
|
||||
URL_LIB_CONTAINER_COMPONENTS.format(container_key=container_key),
|
||||
{'usage_keys': children_ids},
|
||||
expect_response
|
||||
)
|
||||
|
||||
def _patch_container_components(
|
||||
self,
|
||||
container_key: str,
|
||||
children_ids: list[str],
|
||||
expect_response=200,
|
||||
):
|
||||
""" Update container components"""
|
||||
return self._api(
|
||||
'patch',
|
||||
URL_LIB_CONTAINER_COMPONENTS.format(container_key=container_key),
|
||||
{'usage_keys': children_ids},
|
||||
expect_response
|
||||
)
|
||||
|
||||
@@ -16,12 +16,14 @@ from opaque_keys.edx.locator import LibraryLocatorV2
|
||||
from openedx_events.content_authoring.data import (
|
||||
ContentObjectChangedData,
|
||||
LibraryCollectionData,
|
||||
LibraryContainerData,
|
||||
)
|
||||
from openedx_events.content_authoring.signals import (
|
||||
CONTENT_OBJECT_ASSOCIATIONS_CHANGED,
|
||||
LIBRARY_COLLECTION_CREATED,
|
||||
LIBRARY_COLLECTION_DELETED,
|
||||
LIBRARY_COLLECTION_UPDATED,
|
||||
LIBRARY_CONTAINER_UPDATED,
|
||||
)
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -742,3 +744,109 @@ class ContentLibraryCollectionsTest(ContentLibrariesRestApiTest, OpenEdxEventsTe
|
||||
},
|
||||
event_receiver.call_args_list[1].kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ContentLibraryContainersTest(ContentLibrariesRestApiTest, TestCase):
|
||||
"""
|
||||
Tests for Content Library API containers methods.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create Content Libraries
|
||||
self._create_library("test-lib-cont-1", "Test Library 1")
|
||||
|
||||
# Fetch the created ContentLibrare objects so we can access their learning_package.id
|
||||
self.lib1 = ContentLibrary.objects.get(slug="test-lib-cont-1")
|
||||
|
||||
# Create Units
|
||||
self.unit1 = api.create_container(self.lib1.library_key, api.ContainerType.Unit, 'unit-1', 'Unit 1', None)
|
||||
self.unit2 = api.create_container(self.lib1.library_key, api.ContainerType.Unit, 'unit-2', 'Unit 2', None)
|
||||
|
||||
# Create XBlocks
|
||||
# Create some library blocks in lib1
|
||||
self.problem_block = self._add_block_to_library(
|
||||
self.lib1.library_key, "problem", "problem1",
|
||||
)
|
||||
self.problem_block_usage_key = UsageKey.from_string(self.problem_block["id"])
|
||||
self.html_block = self._add_block_to_library(
|
||||
self.lib1.library_key, "html", "html1",
|
||||
)
|
||||
self.html_block_usage_key = UsageKey.from_string(self.html_block["id"])
|
||||
|
||||
# Add content to units
|
||||
api.update_container_children(
|
||||
self.unit1.container_key,
|
||||
[self.problem_block_usage_key, self.html_block_usage_key],
|
||||
None,
|
||||
)
|
||||
api.update_container_children(
|
||||
self.unit2.container_key,
|
||||
[self.html_block_usage_key],
|
||||
None,
|
||||
)
|
||||
|
||||
def test_get_containers_contains_component(self):
|
||||
problem_block_containers = api.get_containers_contains_component(self.problem_block_usage_key)
|
||||
html_block_containers = api.get_containers_contains_component(self.html_block_usage_key)
|
||||
|
||||
assert len(problem_block_containers) == 1
|
||||
assert problem_block_containers[0].container_key == self.unit1.container_key
|
||||
|
||||
assert len(html_block_containers) == 2
|
||||
assert html_block_containers[0].container_key == self.unit1.container_key
|
||||
assert html_block_containers[1].container_key == self.unit2.container_key
|
||||
|
||||
def _validate_calls_of_html_block(self, event_mock):
|
||||
"""
|
||||
Validate that the `event_mock` has been called twice
|
||||
using the `LIBRARY_CONTAINER_UPDATED` signal.
|
||||
"""
|
||||
assert event_mock.call_count == 2
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
library_key=self.lib1.library_key,
|
||||
container_key=str(self.unit1.container_key),
|
||||
background=True,
|
||||
)
|
||||
},
|
||||
event_mock.call_args_list[0].kwargs,
|
||||
)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
library_key=self.lib1.library_key,
|
||||
container_key=str(self.unit2.container_key),
|
||||
background=True,
|
||||
)
|
||||
},
|
||||
event_mock.call_args_list[1].kwargs,
|
||||
)
|
||||
|
||||
def test_call_container_update_signal_when_delete_component(self):
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
api.delete_library_block(self.html_block_usage_key)
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
def test_call_container_update_signal_when_update_olx(self):
|
||||
block_olx = "<html><b>Hello world!</b></html>"
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
self._set_library_block_olx(self.html_block_usage_key, block_olx)
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
def test_call_container_update_signal_when_update_component(self):
|
||||
block_olx = "<html><b>Hello world!</b></html>"
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
self._set_library_block_fields(self.html_block_usage_key, {"data": block_olx, "metadata": {}})
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
Tests for Learning-Core-based Content Libraries
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from freezegun import freeze_time
|
||||
from unittest import mock
|
||||
|
||||
from opaque_keys.edx.locator import LibraryLocatorV2
|
||||
from openedx_events.content_authoring.data import LibraryContainerData
|
||||
@@ -178,3 +178,155 @@ class ContainersTestCase(OpenEdxEventsTestMixin, ContentLibrariesRestApiTest):
|
||||
assert container1_data["container_key"].startswith("lct:CL-TEST:containers:unit:alpha-bravo-")
|
||||
assert container2_data["container_key"].startswith("lct:CL-TEST:containers:unit:alpha-bravo-")
|
||||
assert container1_data["container_key"] != container2_data["container_key"]
|
||||
|
||||
def test_unit_add_children(self):
|
||||
"""
|
||||
Test that we can add and get unit children components
|
||||
"""
|
||||
update_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(update_receiver)
|
||||
lib = self._create_library(slug="containers", title="Container Test Library", description="Units and more")
|
||||
lib_key = LibraryLocatorV2.from_string(lib["id"])
|
||||
|
||||
# Create container and add some components
|
||||
container_data = self._create_container(lib["id"], "unit", display_name="Alpha Bravo", slug=None)
|
||||
problem_block = self._add_block_to_library(lib["id"], "problem", "Problem1", can_stand_alone=False)
|
||||
html_block = self._add_block_to_library(lib["id"], "html", "Html1", can_stand_alone=False)
|
||||
self._add_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block["id"], html_block["id"]]
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 2
|
||||
assert data[0]['id'] == problem_block['id']
|
||||
assert not data[0]['can_stand_alone']
|
||||
assert data[1]['id'] == html_block['id']
|
||||
assert not data[1]['can_stand_alone']
|
||||
problem_block_2 = self._add_block_to_library(lib["id"], "problem", "Problem2", can_stand_alone=False)
|
||||
html_block_2 = self._add_block_to_library(lib["id"], "html", "Html2")
|
||||
# Add two more components
|
||||
self._add_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block_2["id"], html_block_2["id"]]
|
||||
)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
lib_key,
|
||||
container_key=container_data["container_key"],
|
||||
),
|
||||
},
|
||||
update_receiver.call_args_list[0].kwargs,
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
# Verify total number of components to be 2 + 2 = 4
|
||||
assert len(data) == 4
|
||||
assert data[2]['id'] == problem_block_2['id']
|
||||
assert not data[2]['can_stand_alone']
|
||||
assert data[3]['id'] == html_block_2['id']
|
||||
assert data[3]['can_stand_alone']
|
||||
|
||||
def test_unit_remove_children(self):
|
||||
"""
|
||||
Test that we can remove unit children components
|
||||
"""
|
||||
update_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(update_receiver)
|
||||
lib = self._create_library(slug="containers", title="Container Test Library", description="Units and more")
|
||||
lib_key = LibraryLocatorV2.from_string(lib["id"])
|
||||
|
||||
# Create container and add some components
|
||||
container_data = self._create_container(lib["id"], "unit", display_name="Alpha Bravo", slug=None)
|
||||
problem_block = self._add_block_to_library(lib["id"], "problem", "Problem1", can_stand_alone=False)
|
||||
html_block = self._add_block_to_library(lib["id"], "html", "Html1", can_stand_alone=False)
|
||||
problem_block_2 = self._add_block_to_library(lib["id"], "problem", "Problem2", can_stand_alone=False)
|
||||
html_block_2 = self._add_block_to_library(lib["id"], "html", "Html2")
|
||||
self._add_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block["id"], html_block["id"], problem_block_2["id"], html_block_2["id"]]
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 4
|
||||
# Remove both problem blocks.
|
||||
self._remove_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block_2["id"], problem_block["id"]]
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 2
|
||||
assert data[0]['id'] == html_block['id']
|
||||
assert data[1]['id'] == html_block_2['id']
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
lib_key,
|
||||
container_key=container_data["container_key"],
|
||||
),
|
||||
},
|
||||
update_receiver.call_args_list[0].kwargs,
|
||||
)
|
||||
|
||||
def test_unit_replace_children(self):
|
||||
"""
|
||||
Test that we can completely replace/reorder unit children components.
|
||||
"""
|
||||
update_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(update_receiver)
|
||||
lib = self._create_library(slug="containers", title="Container Test Library", description="Units and more")
|
||||
lib_key = LibraryLocatorV2.from_string(lib["id"])
|
||||
|
||||
# Create container and add some components
|
||||
container_data = self._create_container(lib["id"], "unit", display_name="Alpha Bravo", slug=None)
|
||||
problem_block = self._add_block_to_library(lib["id"], "problem", "Problem1", can_stand_alone=False)
|
||||
html_block = self._add_block_to_library(lib["id"], "html", "Html1", can_stand_alone=False)
|
||||
problem_block_2 = self._add_block_to_library(lib["id"], "problem", "Problem2", can_stand_alone=False)
|
||||
html_block_2 = self._add_block_to_library(lib["id"], "html", "Html2")
|
||||
self._add_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block["id"], html_block["id"], problem_block_2["id"], html_block_2["id"]]
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 4
|
||||
assert data[0]['id'] == problem_block['id']
|
||||
assert data[1]['id'] == html_block['id']
|
||||
assert data[2]['id'] == problem_block_2['id']
|
||||
assert data[3]['id'] == html_block_2['id']
|
||||
|
||||
# Reorder the components
|
||||
self._patch_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[problem_block["id"], problem_block_2["id"], html_block["id"], html_block_2["id"]]
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 4
|
||||
assert data[0]['id'] == problem_block['id']
|
||||
assert data[1]['id'] == problem_block_2['id']
|
||||
assert data[2]['id'] == html_block['id']
|
||||
assert data[3]['id'] == html_block_2['id']
|
||||
|
||||
# Replace with new components
|
||||
new_problem_block = self._add_block_to_library(lib["id"], "problem", "New_Problem", can_stand_alone=False)
|
||||
new_html_block = self._add_block_to_library(lib["id"], "html", "New_Html", can_stand_alone=False)
|
||||
self._patch_container_components(
|
||||
container_data["container_key"],
|
||||
children_ids=[new_problem_block["id"], new_html_block["id"]],
|
||||
)
|
||||
data = self._get_container_components(container_data["container_key"])
|
||||
assert len(data) == 2
|
||||
assert data[0]['id'] == new_problem_block['id']
|
||||
assert data[1]['id'] == new_html_block['id']
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
lib_key,
|
||||
container_key=container_data["container_key"],
|
||||
),
|
||||
},
|
||||
update_receiver.call_args_list[0].kwargs,
|
||||
)
|
||||
|
||||
@@ -80,6 +80,8 @@ urlpatterns = [
|
||||
path('containers/<lib_container_key:container_key>/', include([
|
||||
# Get metadata about a specific container in this library, update or delete the container:
|
||||
path('', containers.LibraryContainerView.as_view()),
|
||||
# update components under container
|
||||
path('children/', containers.LibraryContainerChildrenView.as_view()),
|
||||
# Update collections for a given container
|
||||
# path('collections/', views.LibraryContainerCollectionsView.as_view(), name='update-collections-ct'),
|
||||
# path('publish/', views.LibraryContainerPublishView.as_view()),
|
||||
|
||||
@@ -66,7 +66,7 @@ Glossary
|
||||
plan on removing this term from this app's code to avoid confusion.
|
||||
|
||||
- **Section**: From our
|
||||
`documentation <https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/developing_course/course_sections.html#what-is-a-section>`__,
|
||||
`documentation <https://docs.openedx.org/en/latest/educators/concepts/open_edx_platform/about_course_sections.html>`__,
|
||||
“A section is the topmost category in your course. A section can
|
||||
represent a time period in your course, a chapter, or another
|
||||
organizing principle. A section contains one or more subsections.”
|
||||
@@ -155,7 +155,7 @@ Configuring A.C.E.
|
||||
|
||||
These instructions assume you have already setup an Open edX instance or
|
||||
are running devstack. See the `Open edX Developer’s
|
||||
Guide <https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/>`__
|
||||
Guide <https://docs.openedx.org/en/latest/developers/references/developer_guide/index.html>`__
|
||||
for information on setting them up.
|
||||
|
||||
The Schedule app relies on ACE. When live, ACE sends emails to users
|
||||
|
||||
@@ -9,7 +9,7 @@ To enable this feature, set in a settings.py:
|
||||
This was taken from StackOverflow (http://stackoverflow.com/questions/14830669/how-to-expire-django-session-in-5minutes)
|
||||
|
||||
If left unset, session expiration will be handled by Django's SESSION_COOKIE_AGE,
|
||||
which defauts to 1209600 (2 weeks, in seconds).
|
||||
which defaults to 1209600 (2 weeks, in seconds).
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -51,4 +51,4 @@ An example request using *curl*, storing information in a field named ``occupati
|
||||
|
||||
It is important to note that this data will not be returned as part of the User API until the system's Site Configuration has been updated. Details on how to update the Site Configuration can be found `here`_.
|
||||
|
||||
.. _here: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/retrieve_extended_profile_metadata.html
|
||||
.. _here: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/retrieve_extended_profile_metadata.html
|
||||
|
||||
@@ -76,3 +76,11 @@ class LearningContext:
|
||||
|
||||
usage_key: the UsageKeyV2 subclass used for this learning context
|
||||
"""
|
||||
|
||||
def send_container_updated_events(self, usage_key):
|
||||
"""
|
||||
Send "container updated" events for containers that contains the block with
|
||||
the given usage_key in this context.
|
||||
|
||||
usage_key: the UsageKeyV2 subclass used for this learning context
|
||||
"""
|
||||
|
||||
@@ -317,6 +317,7 @@ class LearningCoreXBlockRuntime(XBlockRuntime):
|
||||
# Signal that we've modified this block
|
||||
learning_context = get_learning_context_impl(usage_key)
|
||||
learning_context.send_block_updated_event(usage_key)
|
||||
learning_context.send_container_updated_events(usage_key)
|
||||
|
||||
def _get_component_from_usage_key(self, usage_key):
|
||||
"""
|
||||
|
||||
@@ -20,7 +20,7 @@ Additionally, a subset of ignored errors that are configured as ignored will als
|
||||
* Using New Relic terminology, this extra error class and message data will live on the Transaction and not the TransactionError, because ignored errors won't have a TransactionError.
|
||||
* Use these additional custom attributes to help diagnose unexpected issues with ignored errors.
|
||||
|
||||
.. _IGNORED_ERRORS settings and toggles on Readthedocs: https://edx.readthedocs.io/projects/edx-platform-technical/en/latest/search.html?q=IGNORED_ERRORS&check_keywords=yes&area=default
|
||||
.. _IGNORED_ERRORS settings and toggles on Readthedocs: https://docs.openedx.org/projects/edx-platform/en/latest/search.html?q=IGNORED_ERRORS
|
||||
|
||||
Logging ignored errors
|
||||
-----------------------
|
||||
|
||||
@@ -6,4 +6,4 @@ for a user to see all of their course bookmarks. It also registers
|
||||
a course tool called "Bookmarks" that provides a link to this page.
|
||||
For more information about the feature, see `Bookmarking Course Content`_.
|
||||
|
||||
.. _Bookmarking Course Content: https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/SFD_bookmarks.html
|
||||
.. _Bookmarking Course Content: https://docs.openedx.org/en/latest/learners/SFD_bookmarks.html
|
||||
|
||||
@@ -4,4 +4,4 @@ Course Search
|
||||
This directory contains a Django application that allows a learner to search
|
||||
the content of their course. To learn more, see `Enabling Open edX Search`_.
|
||||
|
||||
.. _Enabling Open edX Search: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/edx_search.html
|
||||
.. _Enabling Open edX Search: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/edx_search.html
|
||||
|
||||
@@ -112,7 +112,7 @@ numpy<2.0.0
|
||||
# Date: 2023-09-18
|
||||
# pinning this version to avoid updates while the library is being developed
|
||||
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269
|
||||
openedx-learning==0.19.1
|
||||
openedx-learning==0.19.2
|
||||
|
||||
# Date: 2023-11-29
|
||||
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
|
||||
|
||||
@@ -820,7 +820,7 @@ openedx-filters==2.0.1
|
||||
# ora2
|
||||
openedx-forum==0.1.9
|
||||
# via -r requirements/edx/kernel.in
|
||||
openedx-learning==0.19.1
|
||||
openedx-learning==0.19.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/kernel.in
|
||||
|
||||
@@ -1383,7 +1383,7 @@ openedx-forum==0.1.9
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
openedx-learning==0.19.1
|
||||
openedx-learning==0.19.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/doc.txt
|
||||
|
||||
@@ -992,7 +992,7 @@ openedx-filters==2.0.1
|
||||
# ora2
|
||||
openedx-forum==0.1.9
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-learning==0.19.1
|
||||
openedx-learning==0.19.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
|
||||
@@ -1050,7 +1050,7 @@ openedx-filters==2.0.1
|
||||
# ora2
|
||||
openedx-forum==0.1.9
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-learning==0.19.1
|
||||
openedx-learning==0.19.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
|
||||
@@ -258,7 +258,7 @@ def main():
|
||||
epilog += "understand and fix any violations, read the docs here:\n"
|
||||
epilog += "\n"
|
||||
# pylint: disable=line-too-long
|
||||
epilog += " https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/conventions/preventing_xss.html#xss-linter\n"
|
||||
epilog += " https://docs.openedx.org/en/latest/developers/references/developer_guide/preventing_xss/preventing_xss.html#xss-linter\n"
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
|
||||
@@ -5,7 +5,7 @@ rules:
|
||||
# https://github.com/returntocorp/semgrep/issues/8608
|
||||
#
|
||||
# Here's the intended URL, for reference:
|
||||
# https://edx.readthedocs.io/projects/edx-django-utils/en/latest/monitoring/how_tos/add_code_owner_custom_attribute_to_an_ida.html#handling-celery-tasks
|
||||
# https://docs.openedx.org/projects/edx-django-utils/en/latest/monitoring/how_tos/add_code_owner_custom_attribute_to_an_ida.html#handling-celery-tasks
|
||||
message: |
|
||||
Celery tasks need to be decorated with `@set_code_owner_attribute`
|
||||
(from the `edx_django_utils.monitoring` module) in order for us
|
||||
@@ -13,7 +13,7 @@ rules:
|
||||
|
||||
For more information, see the Celery section of "Add Code_Owner
|
||||
Custom Attributes to an IDA" in the Monitoring How-Tos of
|
||||
<https://edx.readthedocs.io/projects/edx-django-utils>.
|
||||
<https://docs.openedx.org/projects/edx-django-utils/en/latest/>.
|
||||
languages:
|
||||
- python
|
||||
patterns:
|
||||
@@ -68,7 +68,7 @@ rules:
|
||||
|
||||
For more information, see the Celery section of "Add Code_Owner
|
||||
Custom Attributes to an IDA" in the Monitoring How-Tos of
|
||||
<https://edx.readthedocs.io/projects/edx-django-utils>.
|
||||
<https://docs.openedx.org/projects/edx-django-utils/en/latest/>.
|
||||
languages:
|
||||
- python
|
||||
patterns:
|
||||
|
||||
@@ -65,7 +65,7 @@ There are two example themes provided within edx-platform's themes directory:
|
||||
|
||||
For more details, see `Changing Themes for an Open edX Site`_.
|
||||
|
||||
.. _Changing Themes for an Open edX Site: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/changing_appearance/theming/index.html
|
||||
.. _Changing Themes for an Open edX Site: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/changing_appearance/index.html
|
||||
|
||||
HTML Templates
|
||||
--------------
|
||||
|
||||
@@ -7,8 +7,10 @@ provide any overrides, which means that it adopts the built-in themes,
|
||||
templates etc.
|
||||
|
||||
The `Red Theme`_ is provided as an example of building a simple new theme.
|
||||
Another is `Tutor Indigo`_.
|
||||
|
||||
For more information on building your own theme, see `Changing Themes for an Open edX Site`_.
|
||||
|
||||
.. _Changing Themes for an Open edX Site: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/changing_appearance/theming/index.html
|
||||
.. _Changing Themes for an Open edX Site: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/changing_appearance/index.html
|
||||
.. _Red Theme: https://github.com/openedx/edx-platform/tree/master/themes/red-theme
|
||||
.. _Tutor Indigo: https://github.com/overhangio/tutor-indigo
|
||||
@@ -7,8 +7,10 @@ provide any overrides, which means that it adopts the built-in themes,
|
||||
templates etc.
|
||||
|
||||
The `Red Theme`_ is provided as an example of building a simple new theme.
|
||||
Another is `Tutor Indigo`_.
|
||||
|
||||
For more information on building your own theme, see `Changing Themes for an Open edX Site`_.
|
||||
|
||||
.. _Changing Themes for an Open edX Site: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/changing_appearance/theming/index.html
|
||||
.. _Changing Themes for an Open edX Site: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/changing_appearance/index.html
|
||||
.. _Red Theme: https://github.com/openedx/edx-platform/tree/master/themes/red-theme
|
||||
.. _Tutor Indigo: https://github.com/overhangio/tutor-indigo
|
||||
@@ -7,8 +7,10 @@ provide any overrides, which means that it adopts the built-in themes,
|
||||
templates etc.
|
||||
|
||||
The `Red Theme`_ is provided as an example of building a simple new theme.
|
||||
Another is `Tutor Indigo`_.
|
||||
|
||||
For more information on building your own theme, see `Changing Themes for an Open edX Site`_.
|
||||
|
||||
.. _Changing Themes for an Open edX Site: https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/changing_appearance/theming/index.html
|
||||
.. _Changing Themes for an Open edX Site: https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/changing_appearance/index.html
|
||||
.. _Red Theme: https://github.com/openedx/edx-platform/tree/master/themes/red-theme
|
||||
.. _Tutor Indigo: https://github.com/overhangio/tutor-indigo
|
||||
|
||||
@@ -841,8 +841,8 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
# Translators: please don't translate "id".
|
||||
help=_(
|
||||
'Configure team sets, limit team sizes, and set visibility settings using JSON. See '
|
||||
'<a target="_blank" href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/'
|
||||
'course_features/teams/teams_setup.html#enable-and-configure-teams">teams '
|
||||
'<a target="_blank" href="https://docs.openedx.org/en/latest/educators/references/'
|
||||
'advanced_features/teams_configuration_options.html>teams '
|
||||
'configuration documentation</a> for help and examples.'
|
||||
),
|
||||
scope=Scope.settings,
|
||||
|
||||
@@ -98,7 +98,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
DOCS_ANCHOR_TAG_OPEN = (
|
||||
"<a rel='noopener' target='_blank' "
|
||||
"href='https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/lti_component.html'>"
|
||||
"href='https://docs.openedx.org/en/latest/educators/navigation/components_activities.html#lti-component'>"
|
||||
)
|
||||
BREAK_TAG = '<br />'
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ data: |
|
||||
<article class="response">
|
||||
<h3>What web browser should I use?</h3>
|
||||
<p>The Open edX platform works best with current versions of Chrome, Edge, Firefox, or Safari.</p>
|
||||
<p>See our <a href="https://edx.readthedocs.org/projects/open-edx-learner-guide/en/latest/front_matter/browsers.html">list of supported browsers</a> for the most up-to-date information.</p>
|
||||
<p>See our <a href="https://docs.openedx.org/en/latest/developers/references/developer_guide/testing/browsers.html">list of supported browsers</a> for the most up-to-date information.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
|
||||
@@ -12,8 +12,8 @@ data: |
|
||||
</p>
|
||||
<p>
|
||||
For more information, see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/circuit_schematic_builder.html" target="_blank">
|
||||
Circuit Schematic Builder Problem</a> in <i>Building and Running an edX Course</i>.
|
||||
<a href="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/add_circuit_schematic_builder.html" target="_blank">
|
||||
Circuit Schematic Builder Problem</a> in <i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>
|
||||
When you add the problem, be sure to select <strong>Settings</strong>
|
||||
|
||||
@@ -23,8 +23,8 @@ data: |
|
||||
click the "Show Answer" button.
|
||||
</p>
|
||||
<p>
|
||||
For more information, see <a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/custom_python.html" target="_blank">
|
||||
Write-Your-Own-Grader Problem</a> in <i>Building and Running an edX Course</i>.
|
||||
For more information, see <a href="https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_custom_python_problem.html" target="_blank">
|
||||
Write-Your-Own-Grader Problem</a> in <i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>
|
||||
When you add the problem, be sure to select <strong>Settings</strong>
|
||||
|
||||
@@ -8,8 +8,8 @@ data: |
|
||||
<p>In drag and drop problems, students respond to a question by dragging text or objects to a specific location on an image.</p>
|
||||
<p>
|
||||
For more information, see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/drag_and_drop_deprecated.html" target="_blank">
|
||||
Drag and Drop Problem (Deprecated)</a> in <i>Building and Running an edX Course</i>.
|
||||
<a href="https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/guide_drag_and_drop.html" target="_blank">
|
||||
Drag and Drop Problem (Deprecated)</a> in <i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>
|
||||
When you add the problem, be sure to select <strong>Settings</strong>
|
||||
|
||||
@@ -7,9 +7,9 @@ data: |
|
||||
<p>
|
||||
In an image mapped input problem, also known as a "pointing on a picture" problem, students click inside a defined region in an image. You define this region by including coordinates in the body of the problem. You can define one rectangular region,
|
||||
multiple rectangular regions, or one non-rectangular region. For more information, see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/image_mapped_input.html" target="_blank">Image Mapped Input Problem</a>
|
||||
<a href="https://docs.openedx.org/en/latest/educators/concepts/exercise_tools/about_image_mapped_input.html" target="_blank">Image Mapped Input Problem</a>
|
||||
in
|
||||
<i>Building and Running an edx Course</i>.
|
||||
<i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>When you add the problem, be sure to select
|
||||
<strong>Settings</strong>
|
||||
|
||||
@@ -20,13 +20,8 @@ data: |
|
||||
</p>
|
||||
<p>
|
||||
For more information, see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/custom_javascript.html" target="_blank">
|
||||
Custom JavaScript Problem</a> in <i>Building and Running an edX Course</i>.
|
||||
</p>
|
||||
<p>
|
||||
JavaScript developers can also see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-developer-guide/en/latest/extending_platform/javascript.html" target="_blank">
|
||||
Custom JavaScript Applications</a> in the <i>EdX Developer's Guide</i>.
|
||||
<a href="https://docs.openedx.org/en/latest/educators/references/course_development/exercise_tools/custom_javascript.html" target="_blank">
|
||||
Custom JavaScript Problem</a> in <i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>
|
||||
When you add the problem, be sure to select <strong>Settings</strong>
|
||||
|
||||
@@ -96,8 +96,8 @@ data: |
|
||||
</p>
|
||||
<p>
|
||||
For more information, see
|
||||
<a href="https://edx.readthedocs.io/projects/edx-partner-course-staff/en/latest/exercises_tools/problem_in_latex.html" target="_blank">
|
||||
Problem Written in LaTeX</a> in <i>Building and Running an edX Course</i>.
|
||||
<a href="https://docs.openedx.org/en/latest/educators/how-tos/course_development/exercise_tools/create_problem_in_latex.html" target="_blank">
|
||||
Problem Written in LaTeX</a> in <i>Building and Running an Open edX Course</i>.
|
||||
</p>
|
||||
<p>You can use the following example problems as models.</p>
|
||||
<p><strong>Example Option Problem</strong></p>
|
||||
|
||||