refactor: switch to use openedx_content app (#37924)

The openedx-learning repo was recently refactored to consolidate its
authoring apps into a single openedx_content app:

  https://github.com/openedx/openedx-platform/pull/37924

This commit makes the following changes to accommodate this:

- Bumps the openedx-learning version to 0.31.0 to get the changes.
- Creates new migrations in content_libraries, contentstore, and
  modulestore_migrator to foreign key references to authoring apps
  to point to the new openedx_content app. This is done without
  actually making database changes, since the openedx_content app
  models are taking over the existing tables that the authoring apps
  once pointed to.
- Creates new squashed migrations in these apps that create these
  foreign keys to reference openedx_content app models from the start.

The full rationale for how and why this was done is in the following
openedx-learning ADR:

  https://github.com/openedx/openedx-learning/blob/main/docs/decisions/0020-merge-authoring-apps-into-openedx-content.rst

These migrations should run fine from either a from-scratch scenario
(i.e. a new install or CI), or when upgrading from an Ulmo-or-later
database state. If you have a database state that comes from the middle
of the Ulmo development cycle (e.g. October 2025), you may encounter
migration errors in content_libraries, contentstore, or
modulestore_migrator, with an error message complaining about missing
tables. If you receive this message, run the following command:

  python manage.py lms migrate openedx_content 0001

Then try to run the migrations for the app that failed. Repeat if
necessary for multiple apps.
This commit is contained in:
David Ormsbee
2026-02-04 12:41:13 -05:00
committed by GitHub
parent b6904be3a4
commit 9bf7a72a3a
13 changed files with 541 additions and 21 deletions

View File

@@ -0,0 +1,183 @@
# Generated by Django 5.2.10 on 2026-01-30 01:23
import django.db.migrations.operations.special
import django.db.models.deletion
import opaque_keys.edx.django.models
import openedx_learning.lib.fields
import openedx_learning.lib.validators
import uuid
from django.conf import settings
from django.db import migrations, models
from cms.djangoapps.contentstore.config.waffle import ENABLE_CHECKLISTS_QUALITY
from cms.djangoapps.contentstore.toggles import ENABLE_REACT_MARKDOWN_EDITOR
def create_checklists_quality_waffle_flag(apps, schema_editor):
Flag = apps.get_model('waffle', 'Flag')
# Replacement for flag_undefined_default=True on flag definition
Flag.objects.get_or_create(name=ENABLE_CHECKLISTS_QUALITY.name, defaults={'everyone': True})
def create_markdown_editor_waffle_flag(apps, schema_editor):
Flag = apps.get_model('waffle', 'Flag')
Flag.objects.get_or_create(
name=ENABLE_REACT_MARKDOWN_EDITOR.name, defaults={'everyone': True}
)
class Migration(migrations.Migration):
replaces = [('contentstore', '0001_initial'), ('contentstore', '0002_add_assets_page_flag'), ('contentstore', '0003_remove_assets_page_flag'), ('contentstore', '0004_remove_push_notification_configmodel_table'), ('contentstore', '0005_add_enable_checklists_quality_waffle_flag'), ('contentstore', '0006_courseoutlineregenerate'), ('contentstore', '0007_backfillcoursetabsconfig'), ('contentstore', '0008_cleanstalecertificateavailabilitydatesconfig'), ('contentstore', '0009_learningcontextlinksstatus_publishableentitylink'), ('contentstore', '0010_container_link_models'), ('contentstore', '0011_enable_markdown_editor_flag_by_default'), ('contentstore', '0012_componentlink_top_level_parent_and_more'), ('contentstore', '0013_componentlink_downstream_is_modified_and_more'), ('contentstore', '0014_remove_componentlink_downstream_is_modified_and_more'), ('contentstore', '0015_switch_to_openedx_content')]
initial = True
dependencies = [
('course_overviews', '0024_overview_adds_has_highlights'),
('oel_components', '0003_remove_componentversioncontent_learner_downloadable'),
('oel_publishing', '0002_alter_learningpackage_key_and_more'),
('oel_publishing', '0003_containers'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
('waffle', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='VideoUploadConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('profile_whitelist', models.TextField(blank=True, help_text='A comma-separated list of names of profiles to include in video encoding downloads.')),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
),
migrations.RunPython(
code=create_checklists_quality_waffle_flag,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.CreateModel(
name='CourseOutlineRegenerate',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('course_overviews.courseoverview',),
),
migrations.CreateModel(
name='BackfillCourseTabsConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('start_index', models.IntegerField(default=0, help_text='Index of first course to start backfilling (in an alphabetically sorted list of courses)')),
('count', models.IntegerField(default=0, help_text='How many courses to backfill in this run (or zero for all courses)')),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
options={
'verbose_name': 'Arguments for backfill_course_tabs',
'verbose_name_plural': 'Arguments for backfill_course_tabs',
},
),
migrations.CreateModel(
name='CleanStaleCertificateAvailabilityDatesConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('arguments', models.TextField(blank=True, help_text="A space seperated collection of arguments to be used when running the `clean_stale_certificate_available_dates` management command.' See the management command for options.")),
('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')),
],
options={
'verbose_name': "Arguments for 'clean_stale_certificate_availability_dates'",
'verbose_name_plural': "Arguments for 'clean_stale_certificate_availability_dates'",
},
),
migrations.CreateModel(
name='LearningContextLinksStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('context_key', opaque_keys.edx.django.models.CourseKeyField(help_text='Linking status for course context key', max_length=255, unique=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('failed', 'Failed'), ('completed', 'Completed')], help_text='Status of links in given learning context/course.', max_length=20)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
],
options={
'verbose_name': 'Learning Context Links status',
'verbose_name_plural': 'Learning Context Links status',
},
),
migrations.CreateModel(
name='ComponentLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
('upstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(help_text='Upstream block usage key, this value cannot be null and useful to track upstream library blocks that do not exist yet', max_length=255)),
('upstream_context_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('downstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(max_length=255, unique=True)),
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('upstream_block', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.component')),
],
options={
'verbose_name': 'Component Link',
'verbose_name_plural': 'Component Links',
},
),
migrations.CreateModel(
name='ContainerLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
('upstream_context_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_index=True, help_text='Upstream context key i.e., learning_package/library key', max_length=500)),
('downstream_usage_key', opaque_keys.edx.django.models.UsageKeyField(max_length=255, unique=True)),
('downstream_context_key', opaque_keys.edx.django.models.CourseKeyField(db_index=True, max_length=255)),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('upstream_container_key', opaque_keys.edx.django.models.ContainerKeyField(help_text='Upstream block key (e.g. lct:...), this value cannot be null and is useful to track upstream library blocks that do not exist yet or were deleted.', max_length=255)),
('upstream_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.container')),
],
options={
'abstract': False,
'verbose_name': 'Container Link',
'verbose_name_plural': 'Container Links',
},
),
migrations.RunPython(
code=create_markdown_editor_waffle_flag,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AddField(
model_name='componentlink',
name='top_level_parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contentstore.containerlink'),
),
migrations.AddField(
model_name='containerlink',
name='top_level_parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='contentstore.containerlink'),
),
migrations.AddField(
model_name='componentlink',
name='downstream_customized',
field=models.JSONField(default=list, help_text='Names of the fields which have values set on the upstream block yet have been explicitly overridden on this downstream block'),
),
migrations.AddField(
model_name='containerlink',
name='downstream_customized',
field=models.JSONField(default=list, help_text='Names of the fields which have values set on the upstream block yet have been explicitly overridden on this downstream block'),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.10 on 2026-01-25 21:52
import django.db.models.deletion
from django.db import migrations, models
from django.db.migrations.operations.special import SeparateDatabaseAndState
class Migration(migrations.Migration):
dependencies = [
('contentstore', '0014_remove_componentlink_downstream_is_modified_and_more'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
]
operations = [
SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.AlterField(
model_name='componentlink',
name='upstream_block',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.component'),
),
migrations.AlterField(
model_name='containerlink',
name='upstream_container',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='links', to='openedx_content.container'),
),
]
),
]

View File

@@ -0,0 +1,146 @@
# Generated by Django 5.2.10 on 2026-01-30 01:29
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import opaque_keys.edx.django.models
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('modulestore_migrator', '0001_initial'), ('modulestore_migrator', '0002_alter_modulestoremigration_task_status'), ('modulestore_migrator', '0003_modulestoremigration_is_failed'), ('modulestore_migrator', '0004_alter_modulestoreblockmigration_target_squashed_0005_modulestoreblockmigration_unsupported_reason'), ('modulestore_migrator', '0006_alter_modulestoreblocksource_forwarded_and_more'), ('modulestore_migrator', '0007_switch_to_openedx_content')]
initial = True
dependencies = [
('content_staging', '0006_alter_userclipboard_source_usage_key'),
('oel_collections', '0005_alter_collection_options_alter_collection_enabled'),
('oel_publishing', '0008_alter_draftchangelogrecord_options_and_more'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
('user_tasks', '0004_url_textfield'),
]
operations = [
migrations.CreateModel(
name='ModulestoreBlockMigration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('change_log_record', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.draftchangelogrecord')),
],
),
migrations.CreateModel(
name='ModulestoreMigration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source_version', models.CharField(blank=True, help_text='Migrated content version, the hash of published content version', max_length=255, null=True)),
('composition_level', models.CharField(choices=[('component', 'Component'), ('unit', 'Unit'), ('subsection', 'Subsection'), ('section', 'Section')], default='component', help_text='Maximum hierachy level at which content should be aggregated in target library', max_length=255)),
('repeat_handling_strategy', models.CharField(choices=[('skip', 'Skip'), ('fork', 'Fork'), ('update', 'Update')], default='skip', help_text='If a piece of content already exists in the content library, choose how to handle it.', max_length=24)),
('preserve_url_slugs', models.BooleanField(default=False, help_text='Should the migration preserve the location IDs of the existing blocks?If not, then new, unique human-readable IDs will be generated based on the block titles.')),
('change_log', models.ForeignKey(help_text='Changelog entry in the target learning package which records this migration', null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.draftchangelog')),
],
),
migrations.CreateModel(
name='ModulestoreSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', opaque_keys.edx.django.models.LearningContextKeyField(help_text='Key of the content source (a course or a legacy library)', max_length=255, unique=True)),
('forwarded', models.OneToOneField(blank=True, help_text='If set, the system will forward references of this source over to the target of this migration', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='forwards', to='modulestore_migrator.modulestoremigration')),
],
),
migrations.AddField(
model_name='modulestoremigration',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='migrations', to='modulestore_migrator.modulestoresource'),
),
migrations.AddField(
model_name='modulestoremigration',
name='staged_content',
field=models.OneToOneField(help_text='Modulestore content is processed and staged before importing it to a learning packge. We temporarily save the staged content to allow for troubleshooting of failed migrations.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='content_staging.stagedcontent'),
),
migrations.AddField(
model_name='modulestoremigration',
name='target',
field=models.ForeignKey(help_text='Content will be imported into this library', on_delete=django.db.models.deletion.CASCADE, to='openedx_content.learningpackage'),
),
migrations.AddField(
model_name='modulestoremigration',
name='target_collection',
field=models.ForeignKey(blank=True, help_text='Optional - Collection (within the target library) into which imported content will be grouped', null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.collection'),
),
migrations.AddField(
model_name='modulestoremigration',
name='task_status',
field=models.ForeignKey(help_text='Tracks the status of the task which is executing this migration. In a bulk migration, the same task can be multiple migrations', on_delete=django.db.models.deletion.RESTRICT, related_name='migrations', to='user_tasks.usertaskstatus'),
),
migrations.CreateModel(
name='ModulestoreBlockSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('key', opaque_keys.edx.django.models.UsageKeyField(help_text='Original usage key of the XBlock that has been imported.', max_length=255)),
('forwarded', models.OneToOneField(help_text='If set, the system will forward references of this block source over to the target of this block migration', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='forwards', to='modulestore_migrator.modulestoreblockmigration')),
('overall_source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks', to='modulestore_migrator.modulestoresource')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='modulestoreblockmigration',
name='overall_migration',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='block_migrations', to='modulestore_migrator.modulestoremigration'),
),
migrations.AddField(
model_name='modulestoreblockmigration',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='modulestore_migrator.modulestoreblocksource'),
),
migrations.AddField(
model_name='modulestoreblockmigration',
name='target',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='openedx_content.publishableentity'),
),
migrations.AlterUniqueTogether(
name='modulestoreblockmigration',
unique_together={('overall_migration', 'source'), ('overall_migration', 'target')},
),
migrations.AddField(
model_name='modulestoremigration',
name='is_failed',
field=models.BooleanField(default=False, help_text='is the migration failed?'),
),
migrations.AlterField(
model_name='modulestoreblockmigration',
name='target',
field=models.ForeignKey(blank=True, help_text='The target entity of this block migration, set to null if it fails to migrate', null=True, on_delete=django.db.models.deletion.CASCADE, to='openedx_content.publishableentity'),
),
migrations.AddField(
model_name='modulestoreblockmigration',
name='unsupported_reason',
field=models.TextField(blank=True, help_text='Reason if the block is unsupported and target is set to null', null=True),
),
migrations.AlterField(
model_name='modulestoreblocksource',
name='forwarded',
field=models.OneToOneField(help_text='If set, the system will forward references of this block source over to the target of this block migration', null=True, on_delete=django.db.models.deletion.SET_NULL, to='modulestore_migrator.modulestoreblockmigration'),
),
migrations.AlterField(
model_name='modulestoreblocksource',
name='key',
field=opaque_keys.edx.django.models.UsageKeyField(help_text='Original usage key of the XBlock that has been imported.', max_length=255, unique=True),
),
migrations.AlterField(
model_name='modulestoresource',
name='forwarded',
field=models.OneToOneField(blank=True, help_text='If set, the system will forward references of this source over to the target of this migration', null=True, on_delete=django.db.models.deletion.SET_NULL, to='modulestore_migrator.modulestoremigration'),
),
migrations.AlterField(
model_name='modulestoremigration',
name='change_log',
field=models.ForeignKey(help_text='Changelog entry in the target learning package which records this migration', null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.draftchangelog'),
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.2.10 on 2026-01-25 21:52
import django.db.models.deletion
from django.db import migrations, models
from django.db.migrations.operations.special import SeparateDatabaseAndState
class Migration(migrations.Migration):
dependencies = [
('modulestore_migrator', '0006_alter_modulestoreblocksource_forwarded_and_more'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
]
operations = [
SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.AlterField(
model_name='modulestoreblockmigration',
name='change_log_record',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.draftchangelogrecord'),
),
migrations.AlterField(
model_name='modulestoreblockmigration',
name='target',
field=models.ForeignKey(blank=True, help_text='The target entity of this block migration, set to null if it fails to migrate', null=True, on_delete=django.db.models.deletion.CASCADE, to='openedx_content.publishableentity'),
),
migrations.AlterField(
model_name='modulestoremigration',
name='change_log',
field=models.ForeignKey(help_text='Changelog entry in the target learning package which records this migration', null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.draftchangelog'),
),
migrations.AlterField(
model_name='modulestoremigration',
name='target',
field=models.ForeignKey(help_text='Content will be imported into this library', on_delete=django.db.models.deletion.CASCADE, to='openedx_content.learningpackage'),
),
migrations.AlterField(
model_name='modulestoremigration',
name='target_collection',
field=models.ForeignKey(blank=True, help_text='Optional - Collection (within the target library) into which imported content will be grouped', null=True, on_delete=django.db.models.deletion.SET_NULL, to='openedx_content.collection'),
),
]
),
]

View File

@@ -45,6 +45,7 @@ from corsheaders.defaults import default_headers as corsheaders_default_headers
from datetime import timedelta
from django.utils.translation import gettext_lazy as _
from openedx_learning.api.django import openedx_learning_apps_to_install
from openedx.envs.common import * # pylint: disable=wildcard-import
@@ -897,14 +898,7 @@ INSTALLED_APPS = [
'openedx_events',
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.apps.authoring.collections",
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
"openedx_learning.apps.authoring.units",
"openedx_learning.apps.authoring.subsections",
"openedx_learning.apps.authoring.sections",
*openedx_learning_apps_to_install(),
]
### Apps only installed in some instances

View File

@@ -60,6 +60,7 @@ from enterprise.constants import (
PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
)
from openedx_learning.api.django import openedx_learning_apps_to_install
from openedx.core.lib.derived import Derived
from openedx.envs.common import * # pylint: disable=wildcard-import
@@ -2019,14 +2020,8 @@ INSTALLED_APPS = [
'openedx_events',
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.apps.authoring.collections",
"openedx_learning.apps.authoring.components",
"openedx_learning.apps.authoring.contents",
"openedx_learning.apps.authoring.publishing",
"openedx_learning.apps.authoring.units",
"openedx_learning.apps.authoring.subsections",
"openedx_learning.apps.authoring.sections",
# Learning Core apps that power libraries
*openedx_learning_apps_to_install(),
]
# Add LMS specific optional apps

View File

@@ -0,0 +1,99 @@
# Generated by Django 5.2.10 on 2026-01-30 01:20
import django.db.models.deletion
import opaque_keys.edx.django.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('content_libraries', '0001_initial'), ('content_libraries', '0002_group_permissions'), ('content_libraries', '0003_contentlibrary_type'), ('content_libraries', '0004_contentlibrary_license'), ('content_libraries', '0005_ltigradedresource_ltiprofile'), ('content_libraries', '0006_auto_20210615_1916'), ('content_libraries', '0005_contentlibraryblockimporttask'), ('content_libraries', '0007_merge_20210818_0614'), ('content_libraries', '0008_auto_20210818_2148'), ('content_libraries', '0009_alter_contentlibrary_authorized_lti_configs'), ('content_libraries', '0010_contentlibrary_learning_package_and_more'), ('content_libraries', '0011_remove_contentlibrary_bundle_uuid_and_more'), ('content_libraries', '0012_switch_to_openedx_content')]
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('lti1p3_tool_config', '0001_initial'),
('oel_publishing', '0001_initial'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
('organizations', '0007_historicalorganization'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ContentLibrary',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('slug', models.SlugField(allow_unicode=True)),
('allow_public_learning', models.BooleanField(default=False, help_text='\n Allow any user (even unregistered users) to view and interact with\n content in this library (in the LMS; not in Studio). If this is not\n enabled, then the content in this library is not directly accessible\n in the LMS, and learners will only ever see this content if it is\n explicitly added to a course. If in doubt, leave this unchecked.\n ')),
('allow_public_read', models.BooleanField(default=False, help_text="\n Allow any user with Studio access to view this library's content in\n Studio, use it in their courses, and copy content out of this\n library. If in doubt, leave this unchecked.\n ")),
('org', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='organizations.organization')),
('license', models.CharField(choices=[('', 'All Rights Reserved.'), ('CC:4.0:BY', 'Creative Commons Attribution 4.0'), ('CC:4.0:BY:NC', 'Creative Commons Attribution-NonCommercial 4.0'), ('CC:4.0:BY:NC:ND', 'Creative Commons Attribution-NonCommercial-NoDerivatives 4.0'), ('CC:4.0:BY:NC:SA', 'Creative Commons Attribution-NonCommercial-ShareAlike 4.0'), ('CC:4.0:BY:ND', 'Creative Commons Attribution-NoDerivatives 4.0'), ('CC:4.0:BY:SA', 'Creative Commons Attribution-ShareAlike 4.0')], default='', max_length=25)),
('authorized_lti_configs', models.ManyToManyField(blank=True, help_text="List of authorized LTI tool configurations that can access this library's content through LTI launches, if empty no LTI launch is allowed.", related_name='content_libraries', to='lti1p3_tool_config.ltitool')),
('learning_package', models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.RESTRICT, to='openedx_content.learningpackage')),
],
options={
'verbose_name_plural': 'Content Libraries',
'unique_together': {('org', 'slug')},
},
),
migrations.CreateModel(
name='ContentLibraryPermission',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access_level', models.CharField(choices=[('admin', 'Administer users and author content'), ('author', 'Author content'), ('read', 'Read-only')], max_length=30)),
('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='permission_grants', to='content_libraries.contentlibrary')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.group')),
],
options={
'ordering': ('user__username', 'group__name'),
'unique_together': {('library', 'group'), ('library', 'user')},
},
),
migrations.CreateModel(
name='LtiProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('platform_id', models.CharField(help_text='The LTI platform identifier to which this profile belongs to.', max_length=255, verbose_name='lti platform identifier')),
('client_id', models.CharField(help_text='The LTI client identifier generated by the LTI platform.', max_length=255, verbose_name='client identifier')),
('subject_id', models.CharField(help_text='Identifies the entity that initiated the launch request, commonly a user.', max_length=255, verbose_name='subject identifier')),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contentlibraries_lti_profile', to=settings.AUTH_USER_MODEL, verbose_name='open edx user')),
],
options={
'unique_together': {('platform_id', 'client_id', 'subject_id')},
},
),
migrations.CreateModel(
name='ContentLibraryBlockImportTask',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.CharField(choices=[('created', 'Task was created, but not queued to run.'), ('pending', 'Task was created and queued to run.'), ('running', 'Task is running.'), ('failed', 'Task finished, but some blocks failed to import.'), ('successful', 'Task finished successfully.')], default='created', help_text='The state of the block import task.', max_length=30, verbose_name='state')),
('progress', models.FloatField(default=0.0, help_text='A float from 0.0 to 1.0 representing the task progress.', verbose_name='progress')),
('course_id', opaque_keys.edx.django.models.CourseKeyField(db_index=True, help_text='ID of the imported course.', max_length=255, verbose_name='course ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_tasks', to='content_libraries.contentlibrary')),
],
options={
'ordering': ['-created_at', '-updated_at'],
},
),
migrations.CreateModel(
name='LtiGradedResource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('usage_key', opaque_keys.edx.django.models.UsageKeyField(help_text='The usage key string of the resource serving the content of this launch.', max_length=255)),
('resource_id', models.CharField(help_text='The LTI platform unique identifier of this resource, also known as the "resource link id".', max_length=255)),
('resource_title', models.CharField(help_text='The LTI platform descriptive title for this resource.', max_length=255, null=True)),
('ags_lineitem', models.CharField(help_text='If AGS was enabled during launch, this should hold the lineitem ID.', max_length=255)),
('profile', models.ForeignKey(help_text='The authorized LTI profile that launched the resource (identifies the user).', on_delete=django.db.models.deletion.CASCADE, related_name='lti_resources', to='content_libraries.ltiprofile')),
],
options={
'unique_together': {('usage_key', 'profile')},
},
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 5.2.9 on 2026-01-25 19:44
import django.db.models.deletion
from django.db import migrations, models
from django.db.migrations.operations.special import SeparateDatabaseAndState
class Migration(migrations.Migration):
dependencies = [
('content_libraries', '0011_remove_contentlibrary_bundle_uuid_and_more'),
('openedx_content', '0002_rename_tables_to_openedx_content'),
]
operations = [
SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.AlterField(
model_name='contentlibrary',
name='learning_package',
field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.RESTRICT, to='openedx_content.learningpackage'),
),
]
),
]

View File

@@ -63,7 +63,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.30.2
openedx-learning==0.31.0
# Date: 2023-11-29
# Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise.

View File

@@ -834,7 +834,7 @@ openedx-filters==2.1.0
# ora2
openedx-forum==0.4.0
# via -r requirements/edx/kernel.in
openedx-learning==0.30.2
openedx-learning==0.31.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/kernel.in

View File

@@ -1404,7 +1404,7 @@ openedx-forum==0.4.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
openedx-learning==0.30.2
openedx-learning==0.31.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/doc.txt

View File

@@ -1013,7 +1013,7 @@ openedx-filters==2.1.0
# ora2
openedx-forum==0.4.0
# via -r requirements/edx/base.txt
openedx-learning==0.30.2
openedx-learning==0.31.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/base.txt

View File

@@ -1063,7 +1063,7 @@ openedx-filters==2.1.0
# ora2
openedx-forum==0.4.0
# via -r requirements/edx/base.txt
openedx-learning==0.30.2
openedx-learning==0.31.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/base.txt