Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a221c03
feat: [FC-0092] Optimize Course Info Blocks API (#37122)
Serj-N Oct 30, 2025
b48c4af
Merge pull request #37569 from mitodl/arslan/fix-validation-api
pdpinch Nov 3, 2025
93f361e
fix: mark container as ready to sync if any child block is deleted (#…
navinkarkera Nov 5, 2025
72c23ac
fix: bump learning-core to 0.30.0 (#37615)
ormsbee Nov 7, 2025
711ae03
chore: Adds sandbox requirements to ulmo (#37584)
farhaanbukhsh Nov 7, 2025
3cf5e34
fix: Call `LIBRARY_CONTAINER_PUBLISHED` for parent of containers (#37…
ChrisChV Nov 13, 2025
45f94d4
Merge pull request #37602 from mitodl/arslan/backport-PR-37569
feanil Nov 19, 2025
d9ec5be
[Backport FC-0099] feat: add openedx-authz and update libraries enfor…
MaferMazu Nov 20, 2025
699c831
fix: restrict forum bulk delete to global staff
ormsbee Nov 24, 2025
97ccbc7
fix: Publish components/container in legacy libraries migration (#376…
ChrisChV Nov 25, 2025
d7665ba
fix: send thread_created signal after transaction commit (#37675) (#3…
taimoor-ahmed-1 Nov 26, 2025
f1d4165
feat: include user and origin_server info in library archive (#37626)
dwong2708 Nov 20, 2025
98a9ee2
chore: change release line from 'master' to 'ulmo'
felipemontoya Nov 26, 2025
cf48323
feat: Upgrade Python dependency openedx-authz
mariajgrimaldi Nov 27, 2025
0ee2faa
chore: updated pref settings for misc notification types (#37738)
AhtishamShahid Dec 9, 2025
d0f72f8
Merge pull request #37593 from mitodl/arslan/6850-add-lti-logging
pdpinch Nov 30, 2025
9956dd7
fix: Course search pill not cleared when text deleted. (#37709)
Asespinel Dec 3, 2025
6e50d3e
Merge branch 'release/ulmo' into arslan/37593-backport-lti-logging
pdpinch Dec 10, 2025
f3b9719
revert: feat: [FC-0092] Optimize Course Info Blocks API (#37122) (#37…
asadali145 Nov 20, 2025
ba5113c
fix: don't send emails on library backup/restore
ormsbee Dec 3, 2025
9091801
fix: CourseLimitedStaffRole should not be able to access studio.
feanil Dec 11, 2025
dd91e54
fix: sanitize HTML for course overview & sidebar
ormsbee Dec 12, 2025
dcf6008
Merge pull request #37773 from openedx/feanil/ulmo_limited_staff_fix
feanil Dec 18, 2025
670c81f
fix: allow library creation by course creators
ormsbee Dec 17, 2025
e3084cf
build: Don't update common_constraints.txt on re-compilation.
feanil Dec 19, 2025
38292df
Merge pull request #37799 from openedx/feanil/common_constraint_backport
feanil Dec 19, 2025
0237bfa
fix: bump learning-core to 0.30.2
dwong2708 Dec 5, 2025
a0c83f7
fix: Various fixes to modulestore_migrator [ulmo backport] (#37796)
kdmccormick Dec 19, 2025
e3a54a8
Merge branch 'release/ulmo' into arslan/37593-backport-lti-logging
arslanashraf7 Dec 22, 2025
4319cca
fix: remove text from base query_params (#37836)
marslanabdulrauf Jan 5, 2026
17703dc
fix: correct upstream field for migrated libraries (#37838)
ormsbee Jan 6, 2026
5d33a79
fix: race condition in shared runtime services (#37825)
marslanabdulrauf Jan 8, 2026
ea91c4c
fix: remove the branch/version while building BS (#37866)
marslanabdulrauf Jan 15, 2026
6f552f3
Merge branch 'release/ulmo' into arslan/37593-backport-lti-logging
pdpinch Jan 19, 2026
1db37f3
chore(requirements): update `edx-search` to `4.4.0` (#37949)
brian-smith-tcril Jan 27, 2026
5f95ef0
feat: Upgrade Python dependency Django (#37975)
github-actions[bot] Feb 6, 2026
e05ed0a
build: Upgrade to `ora2==6.17.2` which removes loremipsum base dep (#…
kdmccormick Feb 10, 2026
942ee94
Merge branch 'release/ulmo' into arslan/37593-backport-lti-logging
arslanashraf7 Feb 11, 2026
59d2db2
Merge pull request #37707 from mitodl/arslan/37593-backport-lti-logging
pdpinch Feb 19, 2026
21cead2
fix: remove activation_key from account REST API response
feanil Mar 9, 2026
30d292d
build: Fix the docs build.
feanil Mar 4, 2026
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
4 changes: 4 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ sphinx:

python:
install:
# Need to install this to set the correct version of steuptools for now
# because it is needed by fs
# See https://github.com/openedx/openedx-platform/issues/38068 for details.
- requirements: "requirements/pip-tools.txt"
- requirements: "requirements/edx/doc.txt"
- method: pip
path: .
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ $(COMMON_CONSTRAINTS_TXT):
printf "$(COMMON_CONSTRAINTS_TEMP_COMMENT)" | cat - $(@) > temp && mv temp $(@)

compile-requirements: export CUSTOM_COMPILE_COMMAND=make upgrade
compile-requirements: pre-requirements $(COMMON_CONSTRAINTS_TXT) ## Re-compile *.in requirements to *.txt
compile-requirements: pre-requirements ## Re-compile *.in requirements to *.txt
@# Bootstrapping: Rebuild pip and pip-tools first, and then install them
@# so that if there are any failures we'll know now, rather than the next
@# time someone tries to use the outputs.
Expand All @@ -139,7 +139,7 @@ compile-requirements: pre-requirements $(COMMON_CONSTRAINTS_TXT) ## Re-compile *
export REBUILD=''; \
done

upgrade: ## update the pip requirements files to use the latest releases satisfying our constraints
upgrade: $(COMMON_CONSTRAINTS_TXT) ## update the pip requirements files to use the latest releases satisfying our constraints
$(MAKE) compile-requirements COMPILE_OPTS="--upgrade"

upgrade-package: ## update just one package to the latest usable release
Expand Down
25 changes: 24 additions & 1 deletion cms/djangoapps/cms_user_tasks/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from cms.djangoapps.contentstore.toggles import bypass_olx_failure_enabled
from cms.djangoapps.contentstore.utils import course_import_olx_validation_is_enabled
from openedx.core.djangoapps.content_libraries.api import is_library_backup_task, is_library_restore_task

from .tasks import send_task_complete_email

Expand Down Expand Up @@ -64,6 +65,28 @@ def get_olx_validation_from_artifact():
if olx_artifact and not bypass_olx_failure_enabled():
return olx_artifact.text

def should_skip_end_of_task_email(task_name) -> bool:
"""
Studio tasks generally send an email when finished, but not always.

Some tasks can last many minutes, e.g. course import/export. For these
tasks, there is a high chance that the user has navigated away and will
want to check back in later. Yet email notification is unnecessary and
distracting for things like the Library restore task, which is
relatively quick and cannot be resumed (i.e. if you navigate away, you
have to upload again).

The task_name passed in will be lowercase.
"""
# We currently have to pattern match on the name to differentiate
# between tasks. A better long term solution would be to add a separate
# task type identifier field to Django User Tasks.
return (
is_library_content_update(task_name) or
is_library_backup_task(task_name) or
is_library_restore_task(task_name)
)

status = kwargs['status']

# Only send email when the entire task is complete, should only send when
Expand All @@ -72,7 +95,7 @@ def get_olx_validation_from_artifact():
task_name = status.name.lower()

# Also suppress emails on library content XBlock updates (too much like spam)
if is_library_content_update(task_name):
if should_skip_end_of_task_email(task_name):
LOGGER.info(f"Suppressing end-of-task email on task {task_name}")
return

Expand Down
94 changes: 57 additions & 37 deletions cms/djangoapps/contentstore/api/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@
Tests for the course import API views
"""


import factory
from datetime import datetime
from django.conf import settings

import ddt
from django.test.utils import override_settings
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.tests.factories import StaffFactory
from common.djangoapps.student.tests.factories import UserFactory


@ddt.ddt
@override_settings(PROCTORING_BACKENDS={'DEFAULT': 'proctortrack', 'proctortrack': {}})
class CourseValidationViewTest(SharedModuleStoreTestCase, APITestCase):
"""
Expand Down Expand Up @@ -82,39 +87,54 @@ def test_student_fails(self):
resp = self.client.get(self.get_url(self.course_key))
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)

def test_staff_succeeds(self):
self.client.login(username=self.staff.username, password=self.password)
resp = self.client.get(self.get_url(self.course_key), {'all': 'true'})
self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = {
'assignments': {
'total_number': 1,
'total_visible': 1,
'assignments_with_dates_before_start': [],
'assignments_with_dates_after_end': [],
'assignments_with_ora_dates_after_end': [],
'assignments_with_ora_dates_before_start': [],
},
'dates': {
'has_start_date': True,
'has_end_date': False,
},
'updates': {
'has_update': True,
},
'certificates': {
'is_enabled': False,
'is_activated': False,
'has_certificate': False,
},
'grades': {
'has_grading_policy': False,
'sum_of_weights': 1.0,
},
'proctoring': {
'needs_proctoring_escalation_email': True,
'has_proctoring_escalation_email': True,
},
'is_self_paced': True,
}
self.assertDictEqual(resp.data, expected_data)
@ddt.data(
(False, False),
(True, False),
(False, True),
(True, True),
)
@ddt.unpack
def test_staff_succeeds(self, certs_html_view, with_modes):
features = dict(settings.FEATURES, CERTIFICATES_HTML_VIEW=certs_html_view)
with override_settings(FEATURES=features):
if with_modes:
CourseModeFactory.create_batch(
2,
course_id=self.course.id,
mode_slug=factory.Iterator([CourseMode.AUDIT, CourseMode.VERIFIED]),
)
self.client.login(username=self.staff.username, password=self.password)
resp = self.client.get(self.get_url(self.course_key), {'all': 'true'})
self.assertEqual(resp.status_code, status.HTTP_200_OK)
expected_data = {
'assignments': {
'total_number': 1,
'total_visible': 1,
'assignments_with_dates_before_start': [],
'assignments_with_dates_after_end': [],
'assignments_with_ora_dates_after_end': [],
'assignments_with_ora_dates_before_start': [],
},
'dates': {
'has_start_date': True,
'has_end_date': False,
},
'updates': {
'has_update': True,
},
'certificates': {
'is_enabled': with_modes,
'is_activated': False,
'has_certificate': False,
},
'grades': {
'has_grading_policy': False,
'sum_of_weights': 1.0,
},
'proctoring': {
'needs_proctoring_escalation_email': True,
'has_proctoring_escalation_email': True,
},
'is_self_paced': True,
}
self.assertDictEqual(resp.data, expected_data)
19 changes: 14 additions & 5 deletions cms/djangoapps/contentstore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

from config_models.models import ConfigurationModel
from django.db import models
from django.db.models import QuerySet, OuterRef, Case, When, Exists, Value, ExpressionWrapper
from django.db.models.fields import IntegerField, TextField, BooleanField
from django.db.models import Case, Exists, ExpressionWrapper, OuterRef, Q, QuerySet, Value, When
from django.db.models.fields import BooleanField, IntegerField, TextField
from django.db.models.functions import Coalesce
from django.db.models.lookups import GreaterThan
from django.utils.translation import gettext_lazy as _
from opaque_keys.edx.django.models import CourseKeyField, ContainerKeyField, UsageKeyField
from opaque_keys.edx.django.models import ContainerKeyField, CourseKeyField, UsageKeyField
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryContainerLocator
from openedx_learning.api.authoring import get_published_version
Expand All @@ -23,7 +23,6 @@
manual_date_time_field,
)


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -391,7 +390,7 @@ def filter_links(
cls.objects.filter(**link_filter).select_related(*RELATED_FIELDS),
)
if ready_to_sync is not None:
result = result.filter(ready_to_sync=ready_to_sync)
result = result.filter(Q(ready_to_sync=ready_to_sync) | Q(ready_to_sync_from_children=ready_to_sync))

# Handle top-level parents logic
if use_top_level_parents:
Expand Down Expand Up @@ -436,6 +435,11 @@ def _annotate_query_with_ready_to_sync(cls, query_set: QuerySet["EntityLinkBase"
),
then=1
),
# If upstream block was deleted, set ready_to_sync = True
When(
Q(upstream_container__publishable_entity__published__version__version_num__isnull=True),
then=1
),
default=0,
output_field=models.IntegerField()
)
Expand All @@ -457,6 +461,11 @@ def _annotate_query_with_ready_to_sync(cls, query_set: QuerySet["EntityLinkBase"
),
then=1
),
# If upstream block was deleted, set ready_to_sync = True
When(
Q(upstream_block__publishable_entity__published__version__version_num__isnull=True),
then=1
),
default=0,
output_field=models.IntegerField()
)
Expand Down
25 changes: 5 additions & 20 deletions cms/djangoapps/contentstore/rest_api/v1/serializers/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,11 @@ class LibraryViewSerializer(serializers.Serializer):
org = serializers.CharField()
number = serializers.CharField()
can_edit = serializers.BooleanField()
is_migrated = serializers.SerializerMethodField()
migrated_to_title = serializers.CharField(
source="migrations__target__title",
required=False
)
migrated_to_key = serializers.CharField(
source="migrations__target__key",
required=False
)
migrated_to_collection_key = serializers.CharField(
source="migrations__target_collection__key",
required=False
)
migrated_to_collection_title = serializers.CharField(
source="migrations__target_collection__title",
required=False
)

def get_is_migrated(self, obj):
return "migrations__target__key" in obj
is_migrated = serializers.BooleanField()
migrated_to_title = serializers.CharField(required=False)
migrated_to_key = serializers.CharField(required=False)
migrated_to_collection_key = serializers.CharField(required=False)
migrated_to_collection_title = serializers.CharField(required=False)


class CourseHomeTabSerializer(serializers.Serializer):
Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def get(self, request: Request):
"number": "CPSPR",
"can_edit": true
}
], }
],
```
"""

Expand Down
47 changes: 39 additions & 8 deletions cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.modulestore_migrator import api as migrator_api
from cms.djangoapps.modulestore_migrator.data import CompositionLevel, RepeatHandlingStrategy
from cms.djangoapps.modulestore_migrator.tests.factories import ModulestoreSourceFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.content_libraries import api as lib_api

Expand Down Expand Up @@ -253,8 +252,9 @@ class HomePageLibrariesViewTest(LibraryTestCase):

def setUp(self):
super().setUp()
# Create an additional legacy library
# Create an two additional legacy libaries
self.lib_key_1 = self._create_library(library="lib1")
self.lib_key_2 = self._create_library(library="lib2")
self.organization = OrganizationFactory()

# Create a new v2 library
Expand All @@ -269,7 +269,6 @@ def setUp(self):
library = lib_api.ContentLibrary.objects.get(slug=self.lib_key_v2.slug)
learning_package = library.learning_package
# Create a migration source for the legacy library
self.source = ModulestoreSourceFactory(key=self.lib_key_1)
self.url = reverse("cms.djangoapps.contentstore:v1:libraries")
# Create a collection to migrate this library to
collection_key = "test-collection"
Expand All @@ -280,20 +279,32 @@ def setUp(self):
created_by=self.user.id,
)

# Migrate self.lib_key_1 to self.lib_key_v2
# Migrate both lib_key_1 and lib_key_2 to v2
# Only make lib_key_1 a "forwarding" migration.
migrator_api.start_migration_to_library(
user=self.user,
source_key=self.source.key,
source_key=self.lib_key_1,
target_library_key=self.lib_key_v2,
target_collection_slug=collection_key,
composition_level=CompositionLevel.Component.value,
repeat_handling_strategy=RepeatHandlingStrategy.Skip.value,
composition_level=CompositionLevel.Component,
repeat_handling_strategy=RepeatHandlingStrategy.Skip,
preserve_url_slugs=True,
forward_source_to_target=True,
)
migrator_api.start_migration_to_library(
user=self.user,
source_key=self.lib_key_2,
target_library_key=self.lib_key_v2,
target_collection_slug=collection_key,
composition_level=CompositionLevel.Component,
repeat_handling_strategy=RepeatHandlingStrategy.Skip,
preserve_url_slugs=True,
forward_source_to_target=False,
)

def test_home_page_libraries_response(self):
"""Check successful response content"""
"""Check sucessful response content"""
self.maxDiff = None
response = self.client.get(self.url)

expected_response = {
Expand Down Expand Up @@ -322,6 +333,17 @@ def test_home_page_libraries_response(self):
'migrated_to_collection_key': 'test-collection',
'migrated_to_collection_title': 'Test Collection',
},
# Third library was migrated, but not with forwarding.
# So, it appears just like the unmigrated library.
{
'display_name': 'Test Library',
'library_key': 'library-v1:org+lib2',
'url': '/library/library-v1:org+lib2',
'org': 'org',
'number': 'lib2',
'can_edit': True,
'is_migrated': False,
},
]
}

Expand Down Expand Up @@ -366,6 +388,15 @@ def test_home_page_libraries_response(self):
'can_edit': True,
'is_migrated': False,
},
{
'display_name': 'Test Library',
'library_key': 'library-v1:org+lib2',
'url': '/library/library-v1:org+lib2',
'org': 'org',
'number': 'lib2',
'can_edit': True,
'is_migrated': False,
},
],
}

Expand Down
Loading
Loading