diff --git a/cms/djangoapps/contentstore/migrations/0001_squashed_0015_alter_componentlink_upstream_block_and_more.py b/cms/djangoapps/contentstore/migrations/0001_squashed_0015_alter_componentlink_upstream_block_and_more.py new file mode 100644 index 000000000000..7385e9b0ddd8 --- /dev/null +++ b/cms/djangoapps/contentstore/migrations/0001_squashed_0015_alter_componentlink_upstream_block_and_more.py @@ -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_alter_componentlink_upstream_block_and_more')] + + 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'), + ), + ] diff --git a/cms/djangoapps/contentstore/migrations/0015_alter_componentlink_upstream_block_and_more.py b/cms/djangoapps/contentstore/migrations/0015_alter_componentlink_upstream_block_and_more.py new file mode 100644 index 000000000000..c5fab7274812 --- /dev/null +++ b/cms/djangoapps/contentstore/migrations/0015_alter_componentlink_upstream_block_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.10 on 2026-01-25 21:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentstore', '0014_remove_componentlink_downstream_is_modified_and_more'), + ('openedx_content', '0002_rename_tables_to_openedx_content'), + ] + + 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'), + ), + ] diff --git a/cms/djangoapps/modulestore_migrator/migrations/0001_squashed_0007_alter_modulestoreblockmigration_change_log_record_and_more.py b/cms/djangoapps/modulestore_migrator/migrations/0001_squashed_0007_alter_modulestoreblockmigration_change_log_record_and_more.py new file mode 100644 index 000000000000..361d4afaf513 --- /dev/null +++ b/cms/djangoapps/modulestore_migrator/migrations/0001_squashed_0007_alter_modulestoreblockmigration_change_log_record_and_more.py @@ -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_alter_modulestoreblockmigration_change_log_record_and_more')] + + 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'), + ), + ] diff --git a/cms/djangoapps/modulestore_migrator/migrations/0007_alter_modulestoreblockmigration_change_log_record_and_more.py b/cms/djangoapps/modulestore_migrator/migrations/0007_alter_modulestoreblockmigration_change_log_record_and_more.py new file mode 100644 index 000000000000..11d66b4a89ff --- /dev/null +++ b/cms/djangoapps/modulestore_migrator/migrations/0007_alter_modulestoreblockmigration_change_log_record_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.10 on 2026-01-25 21:52 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('modulestore_migrator', '0006_alter_modulestoreblocksource_forwarded_and_more'), + ('openedx_content', '0002_rename_tables_to_openedx_content'), + ] + + 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'), + ), + ] diff --git a/cms/envs/common.py b/cms/envs/common.py index ceaa86756caa..f0371952f833 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -45,6 +45,7 @@ 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 @@ def make_lms_template_path(settings): '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 diff --git a/lms/envs/common.py b/lms/envs/common.py index 0419633f583e..c1a9dc9689f4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -60,6 +60,7 @@ 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,15 @@ '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", + # The openedx_learning apps require contentstore, modulestore_migrator, + # content.search, and content_staging to be in INSTALLED_APPS. If they are + # not here and LMS migrations are run before CMS migrations, it will cause + # errors (certain openedx_learning apps ) + *openedx_learning_apps_to_install(), +# 'cms.djangoapps.contentstore', +# 'cms.djangoapps.modulestore_migrator', +# 'openedx.core.djangoapps.content.search', +# 'openedx.core.djangoapps.content_staging', ] # Add LMS specific optional apps diff --git a/openedx/core/djangoapps/content_libraries/migrations/0001_squashed_0012_alter_contentlibrary_learning_package.py b/openedx/core/djangoapps/content_libraries/migrations/0001_squashed_0012_alter_contentlibrary_learning_package.py new file mode 100644 index 000000000000..61c3d2b77dbc --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/migrations/0001_squashed_0012_alter_contentlibrary_learning_package.py @@ -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_alter_contentlibrary_learning_package')] + + 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')}, + }, + ), + ] diff --git a/openedx/core/djangoapps/content_libraries/migrations/0012_alter_contentlibrary_learning_package.py b/openedx/core/djangoapps/content_libraries/migrations/0012_alter_contentlibrary_learning_package.py new file mode 100644 index 000000000000..1ec8fb4a78eb --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/migrations/0012_alter_contentlibrary_learning_package.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.9 on 2026-01-25 19:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content_libraries', '0011_remove_contentlibrary_bundle_uuid_and_more'), + ('openedx_content', '0002_rename_tables_to_openedx_content'), + ] + + 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'), + ), + ] diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d4b7fcb7cf46..39d038294c80 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -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.30.1 # Date: 2023-11-29 # Open AI version 1.0.0 dropped support for openai.ChatCompletion which is currently in use in enterprise. diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index d59b577a21dc..fa1d48847408 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -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 +git+https://github.com/ormsbee/openedx-learning.git@big-authoring#egg=openedx_learning # via # -c requirements/constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index b24c0fb4bebe..88a39e5c2ea1 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -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 +git+https://github.com/ormsbee/openedx-learning.git@big-authoring#egg=openedx_learning # via # -c requirements/constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index c721a39e06fb..b4ed9843329c 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -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 +git+https://github.com/ormsbee/openedx-learning.git@big-authoring#egg=openedx_learning # via # -c requirements/constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 20cf145c3c2a..b17bb26aecae 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -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 +git+https://github.com/ormsbee/openedx-learning.git@big-authoring#egg=openedx_learning # via # -c requirements/constraints.txt # -r requirements/edx/base.txt