Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Generated migration for MariaDB UUID field conversion (Django 5.2)
"""
Migration to convert UUIDField from char(32) to uuid type for MariaDB compatibility.

This migration is necessary because Django 5 changed the behavior of UUIDField for MariaDB
databases from using CharField(32) to using a proper UUID type. This change isn't managed
automatically, so we need to generate migrations to safely convert the columns.

This migration only executes for MariaDB databases and is a no-op for other backends.

See: https://www.albertyw.com/note/django-5-mariadb-uuidfield
"""

from django.db import migrations


def apply_mariadb_migration(apps, schema_editor):
"""Apply the migration only for MariaDB databases."""
connection = schema_editor.connection

# Check if this is a MariaDB database
if connection.vendor != 'mysql':
return

# Additional check for MariaDB specifically (vs MySQL)
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
version = cursor.fetchone()[0]
if 'mariadb' not in version.lower():
return

# Apply the field changes for MariaDB
with connection.cursor() as cursor:
cursor.execute(
"ALTER TABLE oel_publishing_learningpackage "
"MODIFY uuid uuid NOT NULL"
)


def reverse_mariadb_migration(apps, schema_editor):
"""Reverse the migration only for MariaDB databases."""
connection = schema_editor.connection

# Check if this is a MariaDB database
if connection.vendor != 'mysql':
return

# Additional check for MariaDB specifically (vs MySQL)
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
version = cursor.fetchone()[0]
if 'mariadb' not in version.lower():
return

# Reverse the field changes for MariaDB
with connection.cursor() as cursor:
cursor.execute(
"ALTER TABLE oel_publishing_learningpackage "
"MODIFY uuid char(32) NOT NULL"
)


class Migration(migrations.Migration):

dependencies = [
('contentstore', '0015_switch_to_openedx_content'),
]

operations = [
migrations.RunPython(
code=apply_mariadb_migration,
reverse_code=reverse_mariadb_migration,
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CourseDetailsSerializer(serializers.Serializer):
pre_requisite_courses = serializers.ListField(child=CourseKeyField())
run = serializers.CharField()
self_paced = serializers.BooleanField()
has_changes = serializers.BooleanField()
short_description = serializers.CharField(allow_blank=True)
start_date = serializers.DateTimeField()
subtitle = serializers.CharField(allow_blank=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework import serializers

from cms.djangoapps.contentstore import toggles
from openedx.core import toggles as core_toggles


class CourseWaffleFlagsSerializer(serializers.Serializer):
Expand All @@ -31,6 +32,7 @@ class CourseWaffleFlagsSerializer(serializers.Serializer):
use_react_markdown_editor = serializers.SerializerMethodField()
use_video_gallery_flow = serializers.SerializerMethodField()
enable_course_optimizer_check_prev_run_links = serializers.SerializerMethodField()
enable_authz_course_authoring = serializers.SerializerMethodField()

def get_course_key(self):
"""
Expand Down Expand Up @@ -201,3 +203,10 @@ def get_enable_course_optimizer_check_prev_run_links(self, obj):
"""
course_key = self.get_course_key()
return toggles.enable_course_optimizer_check_prev_run_links(course_key)

def get_enable_authz_course_authoring(self, obj):
"""
Method to get the authz.enable_course_authoring waffle flag
"""
course_key = self.get_course_key()
return core_toggles.enable_authz_course_authoring(course_key)
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class UpstreamLinkSerializer(serializers.Serializer):
error_message = serializers.CharField(allow_null=True)
ready_to_sync = serializers.BooleanField()
downstream_customized = serializers.ListField(child=serializers.CharField(), allow_empty=True)
has_top_level_parent = serializers.BooleanField()
top_level_parent_key = serializers.CharField(allow_null=True)
ready_to_sync_children = UpstreamChildrenInfoSerializer(many=True, required=False)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CourseWaffleFlagsViewTest(CourseTestCase):
"use_react_markdown_editor": False,
"use_video_gallery_flow": False,
"enable_course_optimizer_check_prev_run_links": False,
"enable_authz_course_authoring": False,
}

def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def test_children_content(self):
"version_declined": None,
"error_message": None,
"ready_to_sync": True,
"has_top_level_parent": False,
"top_level_parent_key": None,
"downstream_customized": [],
},
"user_partition_info": expected_user_partition_info,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _get_upstream_link_good_and_syncable(downstream):
version_declined=downstream.upstream_version_declined,
error_message=None,
downstream_customized=[],
has_top_level_parent=False,
top_level_parent_key=None,
upstream_name=downstream.upstream_display_name,
)

Expand Down
8 changes: 2 additions & 6 deletions cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from olxcleaner.reporting import report_error_summary, report_errors
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator, BlockUsageLocator
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocator
from openedx_events.content_authoring.data import CourseData
from openedx_events.content_authoring.signals import COURSE_RERUN_COMPLETED
from organizations.api import add_organization_course, ensure_organization
Expand Down Expand Up @@ -1641,11 +1641,7 @@ def handle_create_xblock_upstream_link(usage_key):
return
if xblock.top_level_downstream_parent_key is not None:
block_key = BlockKey.from_string(xblock.top_level_downstream_parent_key)
top_level_parent_usage_key = BlockUsageLocator(
xblock.course_id,
block_key.type,
block_key.id,
)
top_level_parent_usage_key = block_key.to_usage_key(xblock.course_id)
try:
ContainerLink.get_by_downstream_usage_key(top_level_parent_usage_key)
except ContainerLink.DoesNotExist:
Expand Down
2 changes: 2 additions & 0 deletions cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from xblock.runtime import KvsFieldData

from openedx.core.djangoapps.video_config.services import VideoConfigService
from openedx.core.djangoapps.discussions.services import DiscussionConfigService
from xmodule.contentstore.django import contentstore
from xmodule.exceptions import NotFoundError as XModuleNotFoundError
from xmodule.modulestore.django import XBlockI18nService, modulestore
Expand Down Expand Up @@ -228,6 +229,7 @@ def _prepare_runtime_for_preview(request, block):
"cache": CacheService(cache),
'replace_urls': ReplaceURLService,
'video_config': VideoConfigService(),
'discussion_config_service': DiscussionConfigService(),
}

block.runtime.get_block_for_descriptor = partial(_load_preview_block, request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,9 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
"edited_on": get_default_time_display(xblock.subtree_edited_on)
if xblock.subtree_edited_on
else None,
"edited_on_raw": str(xblock.subtree_edited_on)
if xblock.subtree_edited_on
else None,
"published": published,
"published_on": published_on,
"studio_url": xblock_studio_url(xblock, parent_xblock),
Expand Down Expand Up @@ -1331,7 +1334,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
# Disable adding or removing children component if xblock is imported from library
xblock_actions["childAddable"] = False
# Enable unlinking only for top level imported components
xblock_actions["unlinkable"] = not upstream_info["has_top_level_parent"]
xblock_actions["unlinkable"] = not upstream_info["top_level_parent_key"]

if is_xblock_unit:
# if xblock is a Unit we add the discussion_enabled option
Expand Down
16 changes: 12 additions & 4 deletions cms/lib/xblock/upstream_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class UpstreamLink:
version_declined: int | None # Latest version which the user has declined to sync with, if any.
error_message: str | None # If link is valid, None. Otherwise, a localized, human-friendly error message.
downstream_customized: list[str] | None # List of fields modified in downstream
has_top_level_parent: bool # True if this Upstream link has a top-level parent
top_level_parent_key: str | None # key of top-level parent if Upstream link has a one.

@property
def is_upstream_deleted(self) -> bool:
Expand Down Expand Up @@ -153,7 +153,7 @@ def ready_to_sync(self) -> bool:
from xmodule.modulestore.django import modulestore

# If this component/container has top-level parent, so we need to sync the parent
if self.has_top_level_parent:
if self.top_level_parent_key:
return False

if isinstance(self.upstream_key, LibraryUsageLocatorV2):
Expand Down Expand Up @@ -222,6 +222,10 @@ def try_get_for_block(cls, downstream: XBlock, log_error: bool = True) -> t.Self
downstream.usage_key,
downstream.upstream,
)
if top_level_parent_key := getattr(downstream, "top_level_downstream_parent_key", None):
top_level_parent_key = str(
BlockKey.from_string(top_level_parent_key).to_usage_key(downstream.usage_key.context_key)
)
return cls(
upstream_ref=getattr(downstream, "upstream", None),
upstream_name=getattr(downstream, "upstream_display_name", None),
Expand All @@ -232,7 +236,7 @@ def try_get_for_block(cls, downstream: XBlock, log_error: bool = True) -> t.Self
version_declined=None,
error_message=str(exc),
downstream_customized=getattr(downstream, "downstream_customized", []),
has_top_level_parent=getattr(downstream, "top_level_downstream_parent_key", None) is not None,
top_level_parent_key=top_level_parent_key,
)

@classmethod
Expand Down Expand Up @@ -306,6 +310,10 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
)
)

if top_level_parent_key := getattr(downstream, "top_level_downstream_parent_key", None):
top_level_parent_key = str(
BlockKey.from_string(top_level_parent_key).to_usage_key(downstream.usage_key.context_key)
)
result = cls(
upstream_ref=downstream.upstream,
upstream_key=upstream_key,
Expand All @@ -316,7 +324,7 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
version_declined=downstream.upstream_version_declined,
error_message=None,
downstream_customized=getattr(downstream, "downstream_customized", []),
has_top_level_parent=downstream.top_level_downstream_parent_key is not None,
top_level_parent_key=top_level_parent_key,
)

return result
Expand Down
8 changes: 8 additions & 0 deletions cms/static/sass/course-unit-mfe-iframe-bundle.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1174,3 +1174,11 @@ select {
@extend %button-primary-outline;
}
}

.tooltip {
background: $primary-base;
white-space: normal;
max-width: 200px;
line-height: 1.5;
text-align: center;
}
20 changes: 10 additions & 10 deletions cms/templates/studio_xblock_wrapper.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import gettext as _
from openedx.core.djangolib.markup import Text
from openedx.core.djangolib.markup import HTML, Text
from cms.djangoapps.contentstore.helpers import xblock_studio_url
from cms.djangoapps.contentstore.utils import is_visible_to_specific_partition_groups, get_editor_page_base_url, determine_label
from lms.lib.utils import is_unit
Expand All @@ -25,7 +25,7 @@
block_is_unit = is_unit(xblock)

upstream_info = UpstreamLink.try_get_for_block(xblock, log_error=False)
can_unlink = upstream_info.upstream_ref and not upstream_info.has_top_level_parent
can_unlink = upstream_info.upstream_ref and not upstream_info.top_level_parent_key
%>

<%namespace name='static' file='static_content.html'/>
Expand Down Expand Up @@ -111,7 +111,7 @@
% else:
% if upstream_info.upstream_ref:
% if upstream_info.error_message:
<div class="library-info-icon two-icons" data-tooltip="${_("The referenced library or library object is not available.")}">
<div class="library-info-icon two-icons" data-tooltip="${_("The referenced library or library object is not available")}">
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
Expand All @@ -120,10 +120,10 @@
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/>
</svg>
<span class="sr-only">${_("The referenced library or library object is not available.")}</span>
<span class="sr-only">${_("The referenced library or library object is not available")}</span>
</div>
% elif upstream_info.ready_to_sync:
<button class="library-info-icon two-icons library-sync-button sync-state" data-tooltip="${Text(_("The linked {upstream_name} has updates available.")).format(upstream_name=upstream_info.upstream_name)}">
<button class="library-info-icon two-icons library-sync-button sync-state" data-tooltip="${Text(_("The linked {upstream_name} has updates available")).format(upstream_name=HTML('<strong>{name}</strong>').format(name=upstream_info.upstream_name))}">
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
Expand All @@ -132,10 +132,10 @@
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M160-160v-80h110l-16-14q-52-46-73-105t-21-119q0-111 66.5-197.5T400-790v84q-72 26-116 88.5T240-478q0 45 17 87.5t53 78.5l10 10v-98h80v240H160Zm400-10v-84q72-26 116-88.5T720-482q0-45-17-87.5T650-648l-10-10v98h-80v-240h240v80H690l16 14q49 49 71.5 106.5T800-482q0 111-66.5 197.5T560-170Z"/>
</svg>
<span class="sr-only">${_("The linked library object has updates available.")}</span>
<span class="sr-only">${_("The linked library object has updates available")}</span>
</button>
% elif len(upstream_info.downstream_customized) > 0:
<div class="library-info-icon two-icons" data-tooltip="${_("This library reference has course overrides applied.")}">
<div class="library-info-icon two-icons" data-tooltip="${_("This library reference has course overrides applied")}">
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
Expand All @@ -144,15 +144,15 @@
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M440-160v-304L240-664v104h-80v-240h240v80H296l224 224v336h-80Zm154-376-58-58 128-126H560v-80h240v240h-80v-104L594-536Z"/>
</svg>
<span class="sr-only">${_("This library reference has course overrides applied.")}</span>
<span class="sr-only">${_("This library reference has course overrides applied")}</span>
</div>
% else:
<div class="library-info-icon" data-tooltip="${Text(_("This is referenced via {upstream_name}")).format(upstream_name=upstream_info.upstream_name)}">
<div class="library-info-icon" data-tooltip="${Text(_("This is referenced via {upstream_name}")).format(upstream_name=HTML('<strong>{name}</strong>').format(name=upstream_info.upstream_name))}">
<!-- "library" icon from https://fonts.google.com/icons?selected=Material+Symbols+Outlined:newsstand:FILL@0;wght@400;GRAD@0;opsz@24&icon.size=24 -->
<svg role="img" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 -960 960 960" fill="currentColor" style="vertical-align: middle;">
<path d="M80-160v-80h800v80H80Zm80-160v-320h80v320h-80Zm160 0v-480h80v480h-80Zm160 0v-480h80v480h-80Zm280 0L600-600l70-40 160 280-70 40Z"/>
</svg>
<span class="sr-only">${_("This item is linked to a library item.")}</span>
<span class="sr-only">${_("This item is linked to a library item")}</span>
</div>
% endif
% endif
Expand Down
4 changes: 3 additions & 1 deletion common/static/js/src/tooltip_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@
pageX = typeof pageX !== 'undefined' ? pageX : element.offset().left + element.width() / 2;
pageY = typeof pageY !== 'undefined' ? pageY : element.offset().top + element.height() / 2;
var tooltipText = $(element).attr('data-tooltip');
// Tooltip content comes from data-tooltip attributes which are server-rendered
// with proper escaping using Text() and HTML() from openedx.core.djangolib.markup
this.tooltip
.text(tooltipText)
.html(tooltipText) // xss-lint: disable=javascript-jquery-html
.css(this.getCoords(pageX, pageY));
},

Expand Down
Loading