Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
646c2ac
fix: move links with status None to forbidden link
Mar 27, 2025
f32b08b
feat: add new param in url (#36458)
mubbsharanwar Mar 28, 2025
1303965
feat: Also set `MPLCONFIGDIR` to make matplotlib complain less (#36456)
timmc-edx Mar 28, 2025
79f33a6
fix: notification count only for web (#36459)
AhtishamShahid Mar 28, 2025
1ca57ec
Basic CRUD REST Endpoints for units in content libraries [FC-0083] (#…
bradenmacdonald Mar 28, 2025
3834f20
fix: sort sections in course-optimizer before returning result (#36441)
hinakhadim Mar 28, 2025
896ca99
chore: calling other djangoapps from API instead of model (#36448)
deborahgu Mar 28, 2025
bcaa79c
feat: api for adding, removing and updating components in container (…
navinkarkera Mar 31, 2025
e5cafb6
docs: fix minor typo (#36216)
robrap Mar 31, 2025
f5c17bb
feat: [FC-0070] add events and style for rendering Split xblock in ch…
ihor-romaniuk Mar 31, 2025
c934166
chore: geoip2: update maxmind geolite country database
feanil Apr 1, 2025
3bda03f
Merge pull request #36463 from openedx/feanil/geoip2-bot-update-count…
feanil Apr 1, 2025
e548585
feat: Added get_containers_contains_component in containers api with …
ChrisChV Mar 20, 2025
5f01638
feat: Add publish_status to containers search document
ChrisChV Mar 22, 2025
3b49af3
feat: Add LIBRARY_CONTAINER_UPDATED whend deleted a component inside …
ChrisChV Mar 22, 2025
1d94788
feat: Send LIBRARY_CONTAINER_UPDATED signal when updating component o…
ChrisChV Mar 24, 2025
da9f510
fix: Bugs sending LIBRARY_CONTAINER_UPDATED signal
ChrisChV Mar 26, 2025
6c12709
feat: Add publish_status of container as PublishStatus.Never by default
ChrisChV Mar 26, 2025
a5bcc11
refactor: ContentLibraryContainersTest to use update_container_childr…
ChrisChV Apr 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions cms/djangoapps/contentstore/core/course_optimizer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from cms.djangoapps.contentstore.tasks import CourseLinkCheckTask, LinkState
from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import get_xblock
from cms.djangoapps.contentstore.xblock_storage_handlers.xblock_helpers import usage_key_with_run
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore


# Restricts status in the REST API to only those which the requesting user has permission to view.
Expand Down Expand Up @@ -285,3 +287,26 @@ def _create_dto_recursive(xblock_node, xblock_dictionary):
xblock_children.append(xblock_entry)

return {level: xblock_children} if level else None


def sort_course_sections(course_key, data):
"""Retrieve and sort course sections based on the published course structure."""
course_blocks = modulestore().get_items(
course_key,
qualifiers={'category': 'course'},
revision=ModuleStoreEnum.RevisionOption.published_only
)

if not course_blocks or 'LinkCheckOutput' not in data or 'sections' not in data['LinkCheckOutput']:
return data # Return unchanged data if course_blocks or required keys are missing

sorted_section_ids = [section.location.block_id for section in course_blocks[0].get_children()]

sections_map = {section['id']: section for section in data['LinkCheckOutput']['sections']}
data['LinkCheckOutput']['sections'] = [
sections_map[section_id]
for section_id in sorted_section_ids
if section_id in sections_map
]

return data
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""
Tests for course optimizer
"""
from unittest import mock
from unittest.mock import Mock

from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.core.course_optimizer_provider import (
_update_node_tree_and_dictionary,
_create_dto_recursive
_create_dto_recursive,
sort_course_sections
)
from cms.djangoapps.contentstore.tasks import LinkState

Expand Down Expand Up @@ -222,3 +224,74 @@ def test_create_dto_recursive_returns_for_full_tree(self):
expected = _create_dto_recursive(mock_node_tree, mock_dictionary)

self.assertEqual(expected_result, expected)

@mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True)
def test_returns_unchanged_data_if_no_course_blocks(self, mock_modulestore):
"""Test that the function returns unchanged data if no course blocks exist."""
mock_modulestore_instance = Mock()
mock_modulestore.return_value = mock_modulestore_instance
mock_modulestore_instance.get_items.return_value = []

data = {}
result = sort_course_sections("course-v1:Test+Course", data)
assert result == data # Should return the original data

@mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True)
def test_returns_unchanged_data_if_linkcheckoutput_missing(self, mock_modulestore):
"""Test that the function returns unchanged data if 'LinkCheckOutput' is missing."""

mock_modulestore_instance = Mock()
mock_modulestore.return_value = mock_modulestore_instance

data = {'LinkCheckStatus': 'Uninitiated'} # No 'LinkCheckOutput'
mock_modulestore_instance.get_items.return_value = data

result = sort_course_sections("course-v1:Test+Course", data)
assert result == data

@mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True)
def test_returns_unchanged_data_if_sections_missing(self, mock_modulestore):
"""Test that the function returns unchanged data if 'sections' is missing."""

mock_modulestore_instance = Mock()
mock_modulestore.return_value = mock_modulestore_instance

data = {'LinkCheckStatus': 'Success', 'LinkCheckOutput': {}} # No 'LinkCheckOutput'
mock_modulestore_instance.get_items.return_value = data

result = sort_course_sections("course-v1:Test+Course", data)
assert result == data

@mock.patch('cms.djangoapps.contentstore.core.course_optimizer_provider.modulestore', autospec=True)
def test_sorts_sections_correctly(self, mock_modulestore):
"""Test that the function correctly sorts sections based on published course structure."""

mock_course_block = Mock()
mock_course_block.get_children.return_value = [
Mock(location=Mock(block_id="section2")),
Mock(location=Mock(block_id="section3")),
Mock(location=Mock(block_id="section1")),
]

mock_modulestore_instance = Mock()
mock_modulestore.return_value = mock_modulestore_instance
mock_modulestore_instance.get_items.return_value = [mock_course_block]

data = {
"LinkCheckOutput": {
"sections": [
{"id": "section1", "name": "Intro"},
{"id": "section2", "name": "Advanced"},
{"id": "section3", "name": "Bonus"}, # Not in course structure
]
}
}

result = sort_course_sections("course-v1:Test+Course", data)
expected_sections = [
{"id": "section2", "name": "Advanced"},
{"id": "section3", "name": "Bonus"},
{"id": "section1", "name": "Intro"},
]

assert result["LinkCheckOutput"]["sections"] == expected_sections
16 changes: 16 additions & 0 deletions cms/djangoapps/contentstore/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework.response import Response
from user_tasks.models import UserTaskStatus

from cms.djangoapps.contentstore.core.course_optimizer_provider import get_link_check_data
from cms.djangoapps.contentstore.core.course_optimizer_provider import get_link_check_data, sort_course_sections
from cms.djangoapps.contentstore.rest_api.v0.serializers.course_optimizer import LinkCheckSerializer
from cms.djangoapps.contentstore.tasks import check_broken_links
from common.djangoapps.student.auth import has_course_author_access, has_studio_read_access
Expand Down Expand Up @@ -139,6 +139,7 @@ def get(self, request: Request, course_id: str):
self.permission_denied(request)

data = get_link_check_data(request, course_id)
serializer = LinkCheckSerializer(data)
data = sort_course_sections(course_key, data)

serializer = LinkCheckSerializer(data)
return Response(serializer.data)
4 changes: 2 additions & 2 deletions cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,13 +1369,13 @@ def _filter_by_status(results):
retry_list = []
for result in results:
status, block_id, url = result['status'], result['block_id'], result['url']
if status is None:
if status is None and _is_studio_url(url):
retry_list.append([block_id, url])
elif status == 200:
continue
elif status == 403 and _is_studio_url(url):
filtered_results.append([block_id, url, LinkState.LOCKED])
elif status == 403 and not _is_studio_url(url):
elif status in [403, None] and not _is_studio_url(url):
filtered_results.append([block_id, url, LinkState.EXTERNAL_FORBIDDEN])
else:
filtered_results.append([block_id, url, LinkState.BROKEN])
Expand Down
4 changes: 3 additions & 1 deletion cms/djangoapps/contentstore/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,18 +498,20 @@ def test_filter_by_status(self):
{'status': 200, 'block_id': 'block1', 'url': 'https://example.com'},
{'status': None, 'block_id': 'block2', 'url': 'https://retry.com'},
{'status': 403, 'block_id': 'block3', 'url': 'https://' + settings.CMS_BASE},
{'status': None, 'block_id': 'block3', 'url': 'https://' + settings.CMS_BASE},
{'status': 403, 'block_id': 'block4', 'url': 'https://external.com'},
{'status': 404, 'block_id': 'block5', 'url': 'https://broken.com'}
]

expected_filtered_results = [
['block2', 'https://retry.com', LinkState.EXTERNAL_FORBIDDEN],
['block3', 'https://' + settings.CMS_BASE, LinkState.LOCKED],
['block4', 'https://external.com', LinkState.EXTERNAL_FORBIDDEN],
['block5', 'https://broken.com', LinkState.BROKEN],
]

expected_retry_list = [
['block2', 'https://retry.com']
['block3', 'https://' + settings.CMS_BASE]
]

filtered_results, retry_list = _filter_by_status(results)
Expand Down
15 changes: 10 additions & 5 deletions cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
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,
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1879,6 +1879,7 @@
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
"openedx_learning.apps.authoring.units",
]


Expand Down
6 changes: 6 additions & 0 deletions cms/static/images/advanced-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions cms/static/images/drag-and-drop-v2-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions cms/static/images/itembank-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions cms/static/images/library-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions cms/static/images/library_v2-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions cms/static/images/openassessment-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions cms/static/images/problem-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions cms/static/images/text-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions cms/static/images/video-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 44 additions & 2 deletions cms/static/js/views/components/add_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
Loading