feat: update openedx-core: new catalog models + backfill migration (#38023)
Some checks failed
Pylint Checks / pylint xmodule (push) Has been cancelled
Quality checks / Quality Others (20, ubuntu-24.04, 3.11) (push) Has been cancelled
Semgrep code quality / Semgrep analysis (ubuntu-latest, 3.11) (push) Has been cancelled
ShellCheck / shellcheck (ubuntu) (push) Has been cancelled
static assets check for lms and cms / static-assets-check (7.0, 20, 10.7.x, ubuntu-24.04, 3.11) (push) Has been cancelled
unit-tests / xmodule-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-1(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-2(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-1(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-2(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-3(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-4(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-5(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-6(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
ShellCheck / shellcheck (macos) (push) Has been cancelled
Javascript tests / JS (20, ubuntu-latest, 3.11) (push) Has been cancelled
Pylint Checks / pylint cms (push) Has been cancelled
Lint Python Imports / Lint Python Imports (push) Has been cancelled
Lockfile Version check / version-check (push) Has been cancelled
Check Django Migrations / check migrations (pinned, 7, 8, ubuntu-24.04, 3.11) (push) Has been cancelled
Pylint Checks / pylint common (push) Has been cancelled
units-test-scripts-common / test (3.12) (push) Has been cancelled
units-test-scripts-user-retirement / test (3.12) (push) Has been cancelled
Verify Dunder __init__.py Files / Verify __init__.py Files (push) Has been cancelled
Pylint Checks / pylint lms-1 (push) Has been cancelled
Pylint Checks / pylint lms-2 (push) Has been cancelled
Pylint Checks / pylint openedx-1 (push) Has been cancelled
Pylint Checks / pylint openedx-2 (push) Has been cancelled
unit-tests / cms-1(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-2(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-1(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-2(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-3(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-4(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-5(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-6(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / collect-and-verify (push) Has been cancelled
Pylint Checks / Pylint checks successful (push) Has been cancelled
Check Django Migrations / Migrations checks successful (push) Has been cancelled
unit-tests / Unit tests successful (push) Has been cancelled
unit-tests / compile-warnings-report (push) Has been cancelled
unit-tests / merge-artifacts (push) Has been cancelled
unit-tests / coverage (3.11) (push) Has been cancelled
Some checks failed
Pylint Checks / pylint xmodule (push) Has been cancelled
Quality checks / Quality Others (20, ubuntu-24.04, 3.11) (push) Has been cancelled
Semgrep code quality / Semgrep analysis (ubuntu-latest, 3.11) (push) Has been cancelled
ShellCheck / shellcheck (ubuntu) (push) Has been cancelled
static assets check for lms and cms / static-assets-check (7.0, 20, 10.7.x, ubuntu-24.04, 3.11) (push) Has been cancelled
unit-tests / xmodule-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-1(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-2(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-1(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-2(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-3(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-4(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-5(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-6(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-cms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-lms(py=3.11,dj=pinned,mongo=7.0) (push) Has been cancelled
ShellCheck / shellcheck (macos) (push) Has been cancelled
Javascript tests / JS (20, ubuntu-latest, 3.11) (push) Has been cancelled
Pylint Checks / pylint cms (push) Has been cancelled
Lint Python Imports / Lint Python Imports (push) Has been cancelled
Lockfile Version check / version-check (push) Has been cancelled
Check Django Migrations / check migrations (pinned, 7, 8, ubuntu-24.04, 3.11) (push) Has been cancelled
Pylint Checks / pylint common (push) Has been cancelled
units-test-scripts-common / test (3.12) (push) Has been cancelled
units-test-scripts-user-retirement / test (3.12) (push) Has been cancelled
Verify Dunder __init__.py Files / Verify __init__.py Files (push) Has been cancelled
Pylint Checks / pylint lms-1 (push) Has been cancelled
Pylint Checks / pylint lms-2 (push) Has been cancelled
Pylint Checks / pylint openedx-1 (push) Has been cancelled
Pylint Checks / pylint openedx-2 (push) Has been cancelled
unit-tests / cms-1(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / cms-2(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / common-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-1(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-2(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-3(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-4(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-5(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / lms-6(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-1-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / openedx-2-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-cms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / xmodule-with-lms(py=3.12,dj=pinned,mongo=7.0) (push) Has been cancelled
unit-tests / collect-and-verify (push) Has been cancelled
Pylint Checks / Pylint checks successful (push) Has been cancelled
Check Django Migrations / Migrations checks successful (push) Has been cancelled
unit-tests / Unit tests successful (push) Has been cancelled
unit-tests / compile-warnings-report (push) Has been cancelled
unit-tests / merge-artifacts (push) Has been cancelled
unit-tests / coverage (3.11) (push) Has been cancelled
* feat: use new version of openedx-core * feat: Use openedx_catalog app, backfill it with all known courses * feat: properly set "created" timestamp on course runs during backfill * fix: better normalization of language codes * feat: keep courses in sync with CourseRun/CatalogCourse * feat: delete CourseRun/CatalogCourse when deleting a course * refactor: course_id -> course_key, run -> run_code, display_name -> title * fix: don't use SplitModulestoreCourseIndex for getting list of all courses
This commit is contained in:
@@ -907,6 +907,9 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
'openedx_events',
|
'openedx_events',
|
||||||
|
|
||||||
|
# Core models to represent courses
|
||||||
|
"openedx_catalog",
|
||||||
|
|
||||||
# Core apps that power libraries
|
# Core apps that power libraries
|
||||||
"openedx_content",
|
"openedx_content",
|
||||||
*openedx_content_backcompat_apps_to_install(),
|
*openedx_content_backcompat_apps_to_install(),
|
||||||
|
|||||||
@@ -2020,6 +2020,9 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
'openedx_events',
|
'openedx_events',
|
||||||
|
|
||||||
|
# Core models to represent courses
|
||||||
|
"openedx_catalog",
|
||||||
|
|
||||||
# Core apps that power libraries
|
# Core apps that power libraries
|
||||||
"openedx_content",
|
"openedx_content",
|
||||||
*openedx_content_backcompat_apps_to_install(),
|
*openedx_content_backcompat_apps_to_install(),
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
Data migration to populate the new CourseRun and CatalogCourse models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generated by Django 5.2.11 on 2026-02-13 21:47
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
from organizations.api import ensure_organization, exceptions as org_exceptions
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# https://github.com/openedx/openedx-platform/issues/38036
|
||||||
|
NORMALIZE_LANGUAGE_CODES = {
|
||||||
|
"zh-hans": "zh-cn",
|
||||||
|
"zh-hant": "zh-hk",
|
||||||
|
"ca@valencia": "ca-es-valencia",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_openedx_catalog(apps, schema_editor) -> None:
|
||||||
|
"""
|
||||||
|
Populate the new CourseRun and CatalogCourse models.
|
||||||
|
"""
|
||||||
|
CourseOverview = apps.get_model("course_overviews", "CourseOverview")
|
||||||
|
CatalogCourse = apps.get_model("openedx_catalog", "CatalogCourse")
|
||||||
|
CourseRun = apps.get_model("openedx_catalog", "CourseRun")
|
||||||
|
|
||||||
|
created_catalog_course_ids: set[int] = set()
|
||||||
|
all_course_runs = CourseOverview.objects.order_by("-created")
|
||||||
|
for course_overview in all_course_runs:
|
||||||
|
course_key = course_overview.id
|
||||||
|
org_code: str = course_key.org
|
||||||
|
course_code: str = course_key.course
|
||||||
|
run_code: str = course_key.run
|
||||||
|
|
||||||
|
# Ensure that the Organization exists.
|
||||||
|
try:
|
||||||
|
org_data = ensure_organization(org_code)
|
||||||
|
except org_exceptions.InvalidOrganizationException as exc:
|
||||||
|
# Note: IFF the org exists among the modulestore courses but not in the Organizations database table,
|
||||||
|
# and if auto-create is disabled (it's enabled by default), this will raise InvalidOrganizationException. It
|
||||||
|
# would be up to the operator to decide how they want to resolve that.
|
||||||
|
raise ValueError(
|
||||||
|
f'The organization short code "{org_code}" exists in modulestore ({course_key}) but '
|
||||||
|
"not the Organizations table, and auto-creating organizations is disabled. You can resolve this by "
|
||||||
|
"creating the Organization manually (e.g. from the Django admin) or turning on auto-creation. "
|
||||||
|
"You can set active=False to prevent this Organization from being used other than for historical data. "
|
||||||
|
) from exc
|
||||||
|
if org_data["short_name"] != org_code:
|
||||||
|
# On most installations, the 'short_name' database column is case insensitive (unfortunately)
|
||||||
|
log.warning(
|
||||||
|
'The course with ID "%s" does not match its Organization.short_name "%s"',
|
||||||
|
course_key,
|
||||||
|
org_data["short_name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch the CourseOverview if it exists
|
||||||
|
try:
|
||||||
|
course_overview = CourseOverview.objects.get(id=course_key)
|
||||||
|
except CourseOverview.DoesNotExist:
|
||||||
|
course_overview = None # Course exists in modulestore but details aren't cached into CourseOverview yet
|
||||||
|
title: str = (course_overview.display_name if course_overview else None) or course_code
|
||||||
|
|
||||||
|
# Determine the course language.
|
||||||
|
# Note that in Studio, the options for course language generally came from the ALL_LANGUAGES setting, which is
|
||||||
|
# mostly two-letter language codes with no locale, except it uses "zh_HANS" for Mandarin and "zh_HANT" for
|
||||||
|
# Cantonese. We normalize those to "zh-cn" and "zh-hk" for consistency with our platform UI languages /
|
||||||
|
# Transifex, but you can still access the "old" version using the CatalogCourse.language_short
|
||||||
|
# getter/setter for backwards compatbility. See https://github.com/openedx/openedx-platform/issues/38036
|
||||||
|
language = settings.LANGUAGE_CODE
|
||||||
|
if course_overview and course_overview.language:
|
||||||
|
language = course_overview.language.lower()
|
||||||
|
language = language.replace("_", "-") # Ensure we use hyphens for consistency (`en-us` not `en_us`)
|
||||||
|
# Normalize this language code. The previous/non-normalized code will still be available via the
|
||||||
|
# "language_short" property for backwards compatibility.
|
||||||
|
language = NORMALIZE_LANGUAGE_CODES.get(language, language)
|
||||||
|
if len(language) > 2 and language[2] != "-":
|
||||||
|
# This seems like an invalid value; revert to the default:
|
||||||
|
log.warning(
|
||||||
|
'The course with ID "%s" has invalid language "%s" - using default language "%s" instead.',
|
||||||
|
course_key,
|
||||||
|
language,
|
||||||
|
settings.LANGUAGE_CODE,
|
||||||
|
)
|
||||||
|
language = settings.LANGUAGE_CODE
|
||||||
|
|
||||||
|
# Ensure that the CatalogCourse exists.
|
||||||
|
cc, cc_created = CatalogCourse.objects.get_or_create(
|
||||||
|
org_id=org_data["id"],
|
||||||
|
course_code=course_code,
|
||||||
|
defaults={
|
||||||
|
# The default title for the catalog course will be the same name as the newest run, since we iterate
|
||||||
|
# over "all_course_runs" in "-created" order.
|
||||||
|
"title": title,
|
||||||
|
"language": language,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if cc_created:
|
||||||
|
created_catalog_course_ids.add(cc.pk)
|
||||||
|
|
||||||
|
if cc.course_code != course_code:
|
||||||
|
raise ValueError(
|
||||||
|
f"The course {course_key} exists in modulestore with a different capitalization of its "
|
||||||
|
f'course code compared to other instances of the same run ("{course_code}" vs "{cc.course_code}"). '
|
||||||
|
"This really should not happen. To fix it, delete the inconsistent course runs (!). "
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the CourseRun
|
||||||
|
new_run, run_created = CourseRun.objects.get_or_create(
|
||||||
|
catalog_course=cc,
|
||||||
|
run_code=run_code,
|
||||||
|
course_key=course_key,
|
||||||
|
defaults={"title": title},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Correct the "created" timestamp. Since it has auto_now_add=True, we can't set its value except using update()
|
||||||
|
# The CourseOverview should have the "created" date unless it's missing or the course was created before
|
||||||
|
# the CourseOverview model existed. In any case, it should be good enough. Otherwise use the default (now).
|
||||||
|
if course_overview:
|
||||||
|
if course_overview.created < cc.created and cc.pk in created_catalog_course_ids:
|
||||||
|
# Use the 'created' date from the oldest course run that we process.
|
||||||
|
CatalogCourse.objects.filter(pk=cc.pk).update(created=course_overview.created)
|
||||||
|
if run_created:
|
||||||
|
CourseRun.objects.filter(pk=new_run.pk).update(created=course_overview.created)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("openedx_catalog", "0001_initial"),
|
||||||
|
("course_overviews", "0029_alter_historicalcourseoverview_options"),
|
||||||
|
("split_modulestore_django", "0003_alter_historicalsplitmodulestorecourseindex_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(backfill_openedx_catalog, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
Signal handler for invalidating cached course overviews
|
Signal handler for invalidating cached course overviews
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -10,6 +9,8 @@ from django.db.models.signals import post_save
|
|||||||
from django.dispatch import Signal
|
from django.dispatch import Signal
|
||||||
from django.dispatch.dispatcher import receiver
|
from django.dispatch.dispatcher import receiver
|
||||||
|
|
||||||
|
from openedx_catalog import api as catalog_api
|
||||||
|
from openedx_catalog.models_api import CourseRun
|
||||||
from openedx.core.djangoapps.signals.signals import COURSE_CERT_DATE_CHANGE
|
from openedx.core.djangoapps.signals.signals import COURSE_CERT_DATE_CHANGE
|
||||||
from xmodule.data import CertificatesDisplayBehaviors
|
from xmodule.data import CertificatesDisplayBehaviors
|
||||||
from xmodule.modulestore.django import SignalHandler
|
from xmodule.modulestore.django import SignalHandler
|
||||||
@@ -33,6 +34,8 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
|
|||||||
"""
|
"""
|
||||||
Catches the signal that a course has been published in Studio and updates the corresponding CourseOverview cache
|
Catches the signal that a course has been published in Studio and updates the corresponding CourseOverview cache
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
Also sync course data to the openedx_catalog CourseRun model.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
previous_course_overview = CourseOverview.objects.get(id=course_key)
|
previous_course_overview = CourseOverview.objects.get(id=course_key)
|
||||||
@@ -41,6 +44,51 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
|
|||||||
updated_course_overview = CourseOverview.load_from_module_store(course_key)
|
updated_course_overview = CourseOverview.load_from_module_store(course_key)
|
||||||
_check_for_course_changes(previous_course_overview, updated_course_overview)
|
_check_for_course_changes(previous_course_overview, updated_course_overview)
|
||||||
|
|
||||||
|
# Currently, SplitModulestoreCourseIndex is the ultimate source of truth for
|
||||||
|
# which courses exist. When a course is published, we sync that data to
|
||||||
|
# CourseOverview, and from CourseOverview to CourseRun.
|
||||||
|
|
||||||
|
# In the future, CourseRun will be the "source of truth" and each CourseRun
|
||||||
|
# may optionally point to content and get synced to CourseOverview.
|
||||||
|
|
||||||
|
# Ensure a CourseRun exists for this course
|
||||||
|
try:
|
||||||
|
course_run = catalog_api.get_course_run(course_key)
|
||||||
|
except CourseRun.DoesNotExist:
|
||||||
|
# Presumably this is a newly-created course. Create the CourseRun.
|
||||||
|
course_run = catalog_api.create_course_run_for_modulestore_course_with(
|
||||||
|
course_key=course_key,
|
||||||
|
title=updated_course_overview.display_name,
|
||||||
|
language_short=updated_course_overview.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep the CourseRun up to date as the course is edited:
|
||||||
|
if updated_course_overview.display_name != course_run.title:
|
||||||
|
catalog_api.sync_course_run_details(course_key, title=updated_course_overview.display_name)
|
||||||
|
# If this course is the only run in the CatalogCourse, should we update the title of
|
||||||
|
# the CatalogCourse to match the run's new title? Currently the only way to edit the name of
|
||||||
|
# a CatalogCourse is via the Django admin. But it's also not used anywhere yet.
|
||||||
|
|
||||||
|
if (
|
||||||
|
updated_course_overview.language
|
||||||
|
and updated_course_overview.language != course_run.catalog_course.language_short
|
||||||
|
):
|
||||||
|
if course_run.catalog_course.runs.count() == 1:
|
||||||
|
# This is the only run in this CatalogCourse. Update the language of the CatalogCourse
|
||||||
|
catalog_api.update_catalog_course(
|
||||||
|
course_run.catalog_course,
|
||||||
|
language_short=updated_course_overview.language,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOG.warning(
|
||||||
|
'Course run "%s" language "%s" does not match its catalog course language, "%s"',
|
||||||
|
str(course_key),
|
||||||
|
updated_course_overview.language,
|
||||||
|
course_run.catalog_course.language_short,
|
||||||
|
)
|
||||||
|
|
||||||
|
# In the future, this will also sync schedule and other metadata to the CourseRun's related models
|
||||||
|
|
||||||
|
|
||||||
@receiver(SignalHandler.course_deleted)
|
@receiver(SignalHandler.course_deleted)
|
||||||
def _listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
def _listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
||||||
@@ -56,6 +104,16 @@ def _listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=
|
|||||||
sender=None,
|
sender=None,
|
||||||
courserun_key=courserun_key,
|
courserun_key=courserun_key,
|
||||||
)
|
)
|
||||||
|
# Delete the openedx_catalog CourseRun to keep it in sync:
|
||||||
|
try:
|
||||||
|
course_run_obj = catalog_api.get_course_run(course_key)
|
||||||
|
except CourseRun.DoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
catalog_course = course_run_obj.catalog_course
|
||||||
|
catalog_api.delete_course_run(course_key)
|
||||||
|
if catalog_course.runs.count() == 0:
|
||||||
|
catalog_api.delete_catalog_course(catalog_course)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=CourseOverview)
|
@receiver(post_save, sender=CourseOverview)
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Test that changes to courses get synced into the new openedx_catalog models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from openedx_catalog import api as catalog_api
|
||||||
|
from openedx_catalog.models_api import CatalogCourse, CourseRun
|
||||||
|
|
||||||
|
from openedx.core.djangolib.testing.utils import skip_unless_cms
|
||||||
|
from xmodule.modulestore import ModuleStoreEnum
|
||||||
|
from xmodule.modulestore.tests.django_utils import (
|
||||||
|
TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED,
|
||||||
|
ModuleStoreTestCase,
|
||||||
|
ImmediateOnCommitMixin,
|
||||||
|
)
|
||||||
|
from xmodule.modulestore.tests.factories import CourseFactory
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_cms
|
||||||
|
class CourseOverviewSyncTestCase(ImmediateOnCommitMixin, ModuleStoreTestCase):
|
||||||
|
"""
|
||||||
|
Test that changes to courses get synced into the new openedx_catalog models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODULESTORE = TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED
|
||||||
|
ENABLED_SIGNALS = ["course_deleted", "course_published"]
|
||||||
|
|
||||||
|
def test_courserun_creation(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests that when a course is created, the `CourseRun` record gets created.
|
||||||
|
|
||||||
|
(Also the corresponding `CatalogCourse`.)
|
||||||
|
"""
|
||||||
|
course = CourseFactory.create(display_name="Intro to Testing", emit_signals=True)
|
||||||
|
course_key = course.location.context_key
|
||||||
|
|
||||||
|
run = catalog_api.get_course_run(course_key)
|
||||||
|
assert run.title == "Intro to Testing"
|
||||||
|
assert run.course_key == course_key
|
||||||
|
assert run.catalog_course.course_code == course_key.course
|
||||||
|
assert run.catalog_course.org_code == course_key.org
|
||||||
|
|
||||||
|
def test_courserun_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests that when a course is updated, the catalog records get updated.
|
||||||
|
|
||||||
|
Because the "language" of a course cannot be set in Studio before you
|
||||||
|
create the course, when a Catalog Course has only a single run, we need
|
||||||
|
to keep the language of the catalog course in sync with any changes to
|
||||||
|
the language field of the course run. (Because authors necessarily
|
||||||
|
create a new course with the default language then edit it to have the
|
||||||
|
correct language that they actually intended to use for that [catalog]
|
||||||
|
course.) This is in contrast with display_name (title), which can
|
||||||
|
actually be set before creating a course.
|
||||||
|
"""
|
||||||
|
# Create a course
|
||||||
|
course = CourseFactory.create(display_name="Intro to Testing", emit_signals=True)
|
||||||
|
course_id = course.location.context_key
|
||||||
|
run = catalog_api.get_course_run(course_id)
|
||||||
|
assert run.title == "Intro to Testing"
|
||||||
|
assert run.catalog_course.language_short == "en"
|
||||||
|
|
||||||
|
# Update the course's title and language:
|
||||||
|
course.language = "es"
|
||||||
|
course.display_name = "Introducción a las pruebas"
|
||||||
|
self.store.update_item(course, ModuleStoreEnum.UserID.test)
|
||||||
|
|
||||||
|
# Check if the catalog data is updated:
|
||||||
|
run.refresh_from_db()
|
||||||
|
assert run.title == "Introducción a las pruebas"
|
||||||
|
assert run.catalog_course.language_short == "es"
|
||||||
|
# Note: for now we don't update the title of the catalog course after it has been created.
|
||||||
|
# We _could_ decide to sync the name from run -> catalog course if there is only one run.
|
||||||
|
assert run.catalog_course.title == "Intro to Testing"
|
||||||
|
|
||||||
|
def test_courserun_of_many_sync(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests that when a course is updated, the catalog records get updated,
|
||||||
|
but if there are several runs of the same course, the changes don't
|
||||||
|
propagate to the `CatalogCourse` and only affect the `CourseRun.
|
||||||
|
"""
|
||||||
|
# This import causes problems at top level when tests run on the LMS shard
|
||||||
|
from cms.djangoapps.contentstore.views.course import rerun_course
|
||||||
|
|
||||||
|
# Create a course
|
||||||
|
course = CourseFactory.create(display_name="Intro to Testing", emit_signals=True)
|
||||||
|
course_id = course.location.context_key
|
||||||
|
run = catalog_api.get_course_run(course_id)
|
||||||
|
assert run.title == "Intro to Testing"
|
||||||
|
assert run.catalog_course.language_short == "en"
|
||||||
|
|
||||||
|
# re-run the course:
|
||||||
|
new_run_course_id = rerun_course(
|
||||||
|
self.user,
|
||||||
|
source_course_key=course_id,
|
||||||
|
org=course_id.org,
|
||||||
|
number=course_id.course,
|
||||||
|
run="newRUN",
|
||||||
|
fields={"display_name": "Intro to Testing TEMPORARY NAME"},
|
||||||
|
background=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the re-run's title (display_name) and language:
|
||||||
|
new_course = self.store.get_course(new_run_course_id)
|
||||||
|
new_course.language = "es"
|
||||||
|
new_course.display_name = "Introducción a las pruebas"
|
||||||
|
self.store.update_item(new_course, self.user.id)
|
||||||
|
|
||||||
|
# Check if the catalog data is updated correctly.
|
||||||
|
# The original CourseRun object should be unchanged:
|
||||||
|
run.refresh_from_db()
|
||||||
|
assert run.title == "Intro to Testing"
|
||||||
|
assert run.catalog_course.language_short == "en"
|
||||||
|
# The new CourseRun object should be created:
|
||||||
|
new_run = catalog_api.get_course_run(new_run_course_id)
|
||||||
|
assert new_run.title == "Introducción a las pruebas"
|
||||||
|
# Changing the language of the second run doesn't affect the lanugage of the overall catalog course (since the
|
||||||
|
# first run is still in English)
|
||||||
|
assert new_run.catalog_course.language_short == "en"
|
||||||
|
|
||||||
|
def test_courserun_deletion(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests that when a course run is deleted, the corresponding CourseRun is
|
||||||
|
deleted, and when it's the last run, the CatalogCourse is deleted too.
|
||||||
|
"""
|
||||||
|
# This import causes problems at top level when tests run on the LMS shard
|
||||||
|
from cms.djangoapps.contentstore.views.course import rerun_course
|
||||||
|
|
||||||
|
# Create a course with two runs:
|
||||||
|
course = CourseFactory.create(display_name="Intro to Testing", emit_signals=True)
|
||||||
|
course_id1 = course.location.context_key
|
||||||
|
run1 = catalog_api.get_course_run(course_id1)
|
||||||
|
# re-run the course:
|
||||||
|
course_id2 = rerun_course(
|
||||||
|
self.user,
|
||||||
|
source_course_key=course_id1,
|
||||||
|
org=course_id1.org,
|
||||||
|
number=course_id1.course,
|
||||||
|
run="run2",
|
||||||
|
fields={"display_name": "ItT run2"},
|
||||||
|
background=False,
|
||||||
|
)
|
||||||
|
run2 = catalog_api.get_course_run(course_id2)
|
||||||
|
catalog_course = run1.catalog_course
|
||||||
|
assert catalog_course == run2.catalog_course # Same for run1 and run2
|
||||||
|
|
||||||
|
self.store.delete_course(course_id1, ModuleStoreEnum.UserID.test)
|
||||||
|
with pytest.raises(CourseRun.DoesNotExist):
|
||||||
|
run1.refresh_from_db()
|
||||||
|
|
||||||
|
# run2 should still exist:
|
||||||
|
run2.refresh_from_db()
|
||||||
|
assert run2.catalog_course.title == "Intro to Testing" # The catalog course still exists and works
|
||||||
|
|
||||||
|
# delete run 2:
|
||||||
|
self.store.delete_course(course_id2, ModuleStoreEnum.UserID.test)
|
||||||
|
with pytest.raises(CourseRun.DoesNotExist):
|
||||||
|
run2.refresh_from_db()
|
||||||
|
# With no runs left, the CatalogCourse also gets auto-deleted:
|
||||||
|
with pytest.raises(CatalogCourse.DoesNotExist):
|
||||||
|
catalog_course.refresh_from_db()
|
||||||
@@ -233,7 +233,7 @@ multi_line_output = 3
|
|||||||
skip = ["envs", "migrations"]
|
skip = ["envs", "migrations"]
|
||||||
|
|
||||||
[tool.importlinter]
|
[tool.importlinter]
|
||||||
root_packages = ["lms", "cms", "openedx", "openedx_content"]
|
root_packages = ["lms", "cms", "openedx", "openedx_content", "openedx_catalog"]
|
||||||
include_external_packages = true
|
include_external_packages = true
|
||||||
# Our custom contract which checks that we're only importing from 'api.py'
|
# Our custom contract which checks that we're only importing from 'api.py'
|
||||||
# for participating packages.
|
# for participating packages.
|
||||||
@@ -356,11 +356,13 @@ isolated_apps = [
|
|||||||
"openedx.core.djangoapps.olx_rest_api",
|
"openedx.core.djangoapps.olx_rest_api",
|
||||||
"openedx.core.djangoapps.xblock",
|
"openedx.core.djangoapps.xblock",
|
||||||
"openedx.core.lib.xblock_serializer",
|
"openedx.core.lib.xblock_serializer",
|
||||||
|
"openedx_catalog",
|
||||||
]
|
]
|
||||||
allowed_modules = [
|
allowed_modules = [
|
||||||
# Only imports from api.py and data.py are allowed elsewhere in the code
|
# Only imports from api.py and data.py are allowed elsewhere in the code
|
||||||
# See https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0049-django-app-patterns.html#api-py
|
# See https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0049-django-app-patterns.html#api-py
|
||||||
"api",
|
"api",
|
||||||
|
"models_api",
|
||||||
"data",
|
"data",
|
||||||
"tests",
|
"tests",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ numpy<2.0.0
|
|||||||
# breaking changes which openedx-core devs want to roll out manually. New patch versions
|
# breaking changes which openedx-core devs want to roll out manually. New patch versions
|
||||||
# are OK to accept automatically.
|
# are OK to accept automatically.
|
||||||
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269
|
# Issue for unpinning: https://github.com/openedx/edx-platform/issues/35269
|
||||||
openedx-core<0.36
|
openedx-core<0.37
|
||||||
|
|
||||||
# Date: 2023-11-29
|
# Date: 2023-11-29
|
||||||
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
|
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.
|
||||||
|
|||||||
@@ -515,7 +515,9 @@ edx-opaque-keys[django]==3.1.0
|
|||||||
# ora2
|
# ora2
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
edx-organizations==7.3.0
|
edx-organizations==7.3.0
|
||||||
# via -r requirements/edx/kernel.in
|
# via
|
||||||
|
# -r requirements/edx/kernel.in
|
||||||
|
# openedx-core
|
||||||
edx-proctoring==5.2.0
|
edx-proctoring==5.2.0
|
||||||
# via -r requirements/edx/kernel.in
|
# via -r requirements/edx/kernel.in
|
||||||
edx-rbac==2.1.0
|
edx-rbac==2.1.0
|
||||||
@@ -826,7 +828,7 @@ openedx-calc==4.0.3
|
|||||||
# via
|
# via
|
||||||
# -r requirements/edx/kernel.in
|
# -r requirements/edx/kernel.in
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
openedx-core==0.35.0
|
openedx-core==0.36.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/constraints.txt
|
# -c requirements/constraints.txt
|
||||||
# -r requirements/edx/kernel.in
|
# -r requirements/edx/kernel.in
|
||||||
|
|||||||
@@ -810,6 +810,7 @@ edx-organizations==7.3.0
|
|||||||
# via
|
# via
|
||||||
# -r requirements/edx/doc.txt
|
# -r requirements/edx/doc.txt
|
||||||
# -r requirements/edx/testing.txt
|
# -r requirements/edx/testing.txt
|
||||||
|
# openedx-core
|
||||||
edx-proctoring==5.2.0
|
edx-proctoring==5.2.0
|
||||||
# via
|
# via
|
||||||
# -r requirements/edx/doc.txt
|
# -r requirements/edx/doc.txt
|
||||||
@@ -1382,7 +1383,7 @@ openedx-calc==4.0.3
|
|||||||
# -r requirements/edx/doc.txt
|
# -r requirements/edx/doc.txt
|
||||||
# -r requirements/edx/testing.txt
|
# -r requirements/edx/testing.txt
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
openedx-core==0.35.0
|
openedx-core==0.36.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/constraints.txt
|
# -c requirements/constraints.txt
|
||||||
# -r requirements/edx/doc.txt
|
# -r requirements/edx/doc.txt
|
||||||
|
|||||||
@@ -608,7 +608,9 @@ edx-opaque-keys[django]==3.1.0
|
|||||||
# ora2
|
# ora2
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
edx-organizations==7.3.0
|
edx-organizations==7.3.0
|
||||||
# via -r requirements/edx/base.txt
|
# via
|
||||||
|
# -r requirements/edx/base.txt
|
||||||
|
# openedx-core
|
||||||
edx-proctoring==5.2.0
|
edx-proctoring==5.2.0
|
||||||
# via -r requirements/edx/base.txt
|
# via -r requirements/edx/base.txt
|
||||||
edx-rbac==2.1.0
|
edx-rbac==2.1.0
|
||||||
@@ -1006,7 +1008,7 @@ openedx-calc==4.0.3
|
|||||||
# via
|
# via
|
||||||
# -r requirements/edx/base.txt
|
# -r requirements/edx/base.txt
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
openedx-core==0.35.0
|
openedx-core==0.36.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/constraints.txt
|
# -c requirements/constraints.txt
|
||||||
# -r requirements/edx/base.txt
|
# -r requirements/edx/base.txt
|
||||||
|
|||||||
@@ -629,7 +629,9 @@ edx-opaque-keys[django]==3.1.0
|
|||||||
# ora2
|
# ora2
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
edx-organizations==7.3.0
|
edx-organizations==7.3.0
|
||||||
# via -r requirements/edx/base.txt
|
# via
|
||||||
|
# -r requirements/edx/base.txt
|
||||||
|
# openedx-core
|
||||||
edx-proctoring==5.2.0
|
edx-proctoring==5.2.0
|
||||||
# via -r requirements/edx/base.txt
|
# via -r requirements/edx/base.txt
|
||||||
edx-rbac==2.1.0
|
edx-rbac==2.1.0
|
||||||
@@ -1056,7 +1058,7 @@ openedx-calc==4.0.3
|
|||||||
# via
|
# via
|
||||||
# -r requirements/edx/base.txt
|
# -r requirements/edx/base.txt
|
||||||
# xblocks-contrib
|
# xblocks-contrib
|
||||||
openedx-core==0.35.0
|
openedx-core==0.36.0
|
||||||
# via
|
# via
|
||||||
# -c requirements/constraints.txt
|
# -c requirements/constraints.txt
|
||||||
# -r requirements/edx/base.txt
|
# -r requirements/edx/base.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user