openedx-learning 0.27.0__tar.gz → 0.28.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openedx_learning-0.27.0/openedx_learning.egg-info → openedx_learning-0.28.0}/PKG-INFO +6 -6
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/__init__.py +1 -1
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/api/authoring.py +1 -0
- openedx_learning-0.28.0/openedx_learning/apps/authoring/backup_restore/api.py +15 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +0 -1
- openedx_learning-0.28.0/openedx_learning/apps/authoring/backup_restore/toml.py +76 -0
- openedx_learning-0.28.0/openedx_learning/apps/authoring/backup_restore/zipper.py +242 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/apps.py +2 -2
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/api.py +67 -13
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/apps.py +2 -2
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/publishable_entity.py +7 -3
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/api.py +6 -1
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/apps.py +2 -2
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/api.py +6 -1
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/apps.py +2 -2
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/api.py +6 -1
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/apps.py +2 -2
- {openedx_learning-0.27.0 → openedx_learning-0.28.0/openedx_learning.egg-info}/PKG-INFO +6 -6
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning.egg-info/SOURCES.txt +1 -0
- openedx_learning-0.28.0/openedx_tagging/core/tagging/models/utils.py +82 -0
- openedx_learning-0.27.0/openedx_learning/apps/authoring/backup_restore/api.py +0 -25
- openedx_learning-0.27.0/openedx_learning/apps/authoring/backup_restore/toml.py +0 -72
- openedx_learning-0.27.0/openedx_tagging/core/tagging/models/utils.py +0 -54
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/CHANGELOG.rst +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/LICENSE.txt +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/MANIFEST.in +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/README.rst +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/api/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/api/authoring_models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/apps.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/backup_restore/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/api.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/apps.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0003_collection_entities.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0004_collection_key.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/collections/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/api.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/commands/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/components/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/api.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/apps.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/contents/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/contextmanagers.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0003_containers.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0005_alter_entitylistrow_options.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0006_draftchangelog.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0007_bootstrap_draftchangelog.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/container.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/draft_log.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/entity_list.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/learning_package.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/publishing/models/publish_log.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/subsections/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/units/models.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/contrib/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/apps.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/urls.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/contrib/media_server/views.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/admin_utils.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/cache.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/collations.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/fields.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/managers.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/test_utils.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/lib/validators.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/py.typed +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning.egg-info/dependency_links.txt +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning.egg-info/not-zip-safe +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning.egg-info/requires.txt +5 -5
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning.egg-info/top_level.txt +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/admin.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/api.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/apps.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/data.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/api.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/import_export/template.json +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/base.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/import_export.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/views.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/rules.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/core/tagging/urls.py +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_tagging/py.typed +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/requirements/base.in +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/setup.cfg +0 -0
- {openedx_learning-0.27.0 → openedx_learning-0.28.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: Open edX Learning Core and Tagging.
|
|
5
5
|
Home-page: https://github.com/openedx/openedx-learning
|
|
6
6
|
Author: David Ormsbee
|
|
@@ -19,13 +19,13 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
License-File: LICENSE.txt
|
|
22
|
-
Requires-Dist: Django
|
|
23
|
-
Requires-Dist: rules<4.0
|
|
24
22
|
Requires-Dist: celery
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist: tomlkit
|
|
23
|
+
Requires-Dist: rules<4.0
|
|
24
|
+
Requires-Dist: Django
|
|
28
25
|
Requires-Dist: edx-drf-extensions
|
|
26
|
+
Requires-Dist: tomlkit
|
|
27
|
+
Requires-Dist: attrs
|
|
28
|
+
Requires-Dist: djangorestframework<4.0
|
|
29
29
|
Dynamic: author
|
|
30
30
|
Dynamic: author-email
|
|
31
31
|
Dynamic: classifier
|
|
@@ -9,6 +9,7 @@ APIs.
|
|
|
9
9
|
"""
|
|
10
10
|
# These wildcard imports are okay because these api modules declare __all__.
|
|
11
11
|
# pylint: disable=wildcard-import
|
|
12
|
+
from ..apps.authoring.backup_restore.api import *
|
|
12
13
|
from ..apps.authoring.collections.api import *
|
|
13
14
|
from ..apps.authoring.components.api import *
|
|
14
15
|
from ..apps.authoring.contents.api import *
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backup Restore API
|
|
3
|
+
"""
|
|
4
|
+
from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackageZipper
|
|
5
|
+
from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_zip_file(lp_key: str, path: str) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Creates a zip file with a toml file so far (WIP)
|
|
11
|
+
|
|
12
|
+
Can throw a NotFoundError at get_learning_package_by_key
|
|
13
|
+
"""
|
|
14
|
+
learning_package = get_learning_package_by_key(lp_key)
|
|
15
|
+
LearningPackageZipper(learning_package).create_zip(path)
|
|
@@ -33,7 +33,6 @@ class Command(BaseCommand):
|
|
|
33
33
|
self.stdout.write(self.style.SUCCESS(message))
|
|
34
34
|
except LearningPackage.DoesNotExist as exc:
|
|
35
35
|
message = f"Learning package with key {lp_key} not found"
|
|
36
|
-
logger.exception(message)
|
|
37
36
|
raise CommandError(message) from exc
|
|
38
37
|
except Exception as e:
|
|
39
38
|
message = f"Failed to export learning package '{lp_key}': {e}"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TOML serialization for learning packages and publishable entities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
import tomlkit
|
|
8
|
+
|
|
9
|
+
from openedx_learning.apps.authoring.publishing import api as publishing_api
|
|
10
|
+
from openedx_learning.apps.authoring.publishing.models import PublishableEntity, PublishableEntityVersion
|
|
11
|
+
from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def toml_learning_package(learning_package: LearningPackage) -> str:
|
|
15
|
+
"""Create a TOML representation of the learning package."""
|
|
16
|
+
doc = tomlkit.document()
|
|
17
|
+
doc.add(tomlkit.comment(f"Datetime of the export: {datetime.now()}"))
|
|
18
|
+
section = tomlkit.table()
|
|
19
|
+
section.add("title", learning_package.title)
|
|
20
|
+
section.add("key", learning_package.key)
|
|
21
|
+
section.add("description", learning_package.description)
|
|
22
|
+
section.add("created", learning_package.created)
|
|
23
|
+
section.add("updated", learning_package.updated)
|
|
24
|
+
doc.add("learning_package", section)
|
|
25
|
+
return tomlkit.dumps(doc)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def toml_publishable_entity(entity: PublishableEntity) -> str:
|
|
29
|
+
"""Create a TOML representation of a publishable entity."""
|
|
30
|
+
|
|
31
|
+
current_draft_version = publishing_api.get_draft_version(entity)
|
|
32
|
+
current_published_version = publishing_api.get_published_version(entity)
|
|
33
|
+
|
|
34
|
+
doc = tomlkit.document()
|
|
35
|
+
entity_table = tomlkit.table()
|
|
36
|
+
entity_table.add("uuid", str(entity.uuid))
|
|
37
|
+
entity_table.add("can_stand_alone", entity.can_stand_alone)
|
|
38
|
+
|
|
39
|
+
if current_draft_version:
|
|
40
|
+
draft_table = tomlkit.table()
|
|
41
|
+
draft_table.add("version_num", current_draft_version.version_num)
|
|
42
|
+
entity_table.add("draft", draft_table)
|
|
43
|
+
|
|
44
|
+
published_table = tomlkit.table()
|
|
45
|
+
if current_published_version:
|
|
46
|
+
published_table.add("version_num", current_published_version.version_num)
|
|
47
|
+
else:
|
|
48
|
+
published_table.add(tomlkit.comment("unpublished: no published_version_num"))
|
|
49
|
+
entity_table.add("published", published_table)
|
|
50
|
+
|
|
51
|
+
doc.add("entity", entity_table)
|
|
52
|
+
doc.add(tomlkit.nl())
|
|
53
|
+
doc.add(tomlkit.comment("### Versions"))
|
|
54
|
+
|
|
55
|
+
for entity_version in entity.versions.all():
|
|
56
|
+
version = tomlkit.aot()
|
|
57
|
+
version_table = toml_publishable_entity_version(entity_version)
|
|
58
|
+
version.append(version_table)
|
|
59
|
+
doc.add("version", version)
|
|
60
|
+
|
|
61
|
+
return tomlkit.dumps(doc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def toml_publishable_entity_version(version: PublishableEntityVersion) -> tomlkit.items.Table:
|
|
65
|
+
"""Create a TOML representation of a publishable entity version."""
|
|
66
|
+
version_table = tomlkit.table()
|
|
67
|
+
version_table.add("title", version.title)
|
|
68
|
+
version_table.add("uuid", str(version.uuid))
|
|
69
|
+
version_table.add("version_num", version.version_num)
|
|
70
|
+
container_table = tomlkit.table()
|
|
71
|
+
container_table.add("children", [])
|
|
72
|
+
unit_table = tomlkit.table()
|
|
73
|
+
unit_table.add("graded", True)
|
|
74
|
+
container_table.add("unit", unit_table)
|
|
75
|
+
version_table.add("container", container_table)
|
|
76
|
+
return version_table # For use in AoT
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides functionality to create a zip file containing the learning package data,
|
|
3
|
+
including a TOML representation of the learning package and its entities.
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from django.db.models import Prefetch, QuerySet
|
|
11
|
+
from django.utils.text import slugify
|
|
12
|
+
|
|
13
|
+
from openedx_learning.api.authoring_models import (
|
|
14
|
+
ComponentVersion,
|
|
15
|
+
ComponentVersionContent,
|
|
16
|
+
Content,
|
|
17
|
+
LearningPackage,
|
|
18
|
+
PublishableEntity,
|
|
19
|
+
PublishableEntityVersion,
|
|
20
|
+
)
|
|
21
|
+
from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
|
|
22
|
+
from openedx_learning.apps.authoring.publishing import api as publishing_api
|
|
23
|
+
|
|
24
|
+
TOML_PACKAGE_NAME = "package.toml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def slugify_hashed_filename(identifier: str) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Generate a filesystem-safe filename from an identifier.
|
|
30
|
+
|
|
31
|
+
Why:
|
|
32
|
+
Identifiers may contain characters that are invalid or ambiguous
|
|
33
|
+
in filesystems (e.g., slashes, colons, case differences).
|
|
34
|
+
Additionally, two different identifiers might normalize to the same
|
|
35
|
+
slug after cleaning. To avoid collisions and ensure uniqueness,
|
|
36
|
+
we append a short blake2b hash.
|
|
37
|
+
|
|
38
|
+
What:
|
|
39
|
+
- Slugify the identifier (preserves most characters, only strips
|
|
40
|
+
filesystem-invalid ones).
|
|
41
|
+
- Append a short hash for uniqueness.
|
|
42
|
+
- Result: human-readable but still unique and filesystem-safe filename.
|
|
43
|
+
"""
|
|
44
|
+
slug = slugify(identifier, allow_unicode=True)
|
|
45
|
+
# Short digest ensures uniqueness without overly long filenames
|
|
46
|
+
short_hash = hashlib.blake2b(
|
|
47
|
+
identifier.encode("utf-8"),
|
|
48
|
+
digest_size=3,
|
|
49
|
+
).hexdigest()
|
|
50
|
+
return f"{slug}_{short_hash}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LearningPackageZipper:
|
|
54
|
+
"""
|
|
55
|
+
A class to handle the zipping of learning content for backup and restore.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, learning_package: LearningPackage):
|
|
59
|
+
self.learning_package = learning_package
|
|
60
|
+
self.folders_already_created: set[Path] = set()
|
|
61
|
+
|
|
62
|
+
def create_folder(self, folder_path: Path, zip_file: zipfile.ZipFile) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Create a folder for the zip file structure.
|
|
65
|
+
Skips creating the folder if it already exists based on the folder path.
|
|
66
|
+
Args:
|
|
67
|
+
folder_path (Path): The path of the folder to create.
|
|
68
|
+
"""
|
|
69
|
+
if folder_path not in self.folders_already_created:
|
|
70
|
+
zip_info = zipfile.ZipInfo(str(folder_path) + "/")
|
|
71
|
+
zip_file.writestr(zip_info, "") # Add explicit empty directory entry
|
|
72
|
+
self.folders_already_created.add(folder_path)
|
|
73
|
+
|
|
74
|
+
def get_publishable_entities(self) -> QuerySet[PublishableEntity]:
|
|
75
|
+
"""
|
|
76
|
+
Retrieve the publishable entities associated with the learning package.
|
|
77
|
+
Prefetches related data for efficiency.
|
|
78
|
+
"""
|
|
79
|
+
lp_id = self.learning_package.pk
|
|
80
|
+
publishable_entities: QuerySet[PublishableEntity] = publishing_api.get_publishable_entities(lp_id)
|
|
81
|
+
return (
|
|
82
|
+
publishable_entities
|
|
83
|
+
.select_related(
|
|
84
|
+
"container",
|
|
85
|
+
"component__component_type",
|
|
86
|
+
"draft__version__componentversion",
|
|
87
|
+
"published__version__componentversion",
|
|
88
|
+
)
|
|
89
|
+
.prefetch_related(
|
|
90
|
+
# We should re-evaluate the prefetching strategy here,
|
|
91
|
+
# as the current approach may cause performance issues—
|
|
92
|
+
# especially with large libraries (up to 100K items),
|
|
93
|
+
# which is too large for this type of prefetch.
|
|
94
|
+
Prefetch(
|
|
95
|
+
"draft__version__componentversion__componentversioncontent_set",
|
|
96
|
+
queryset=ComponentVersionContent.objects.select_related("content"),
|
|
97
|
+
to_attr="prefetched_contents",
|
|
98
|
+
),
|
|
99
|
+
Prefetch(
|
|
100
|
+
"published__version__componentversion__componentversioncontent_set",
|
|
101
|
+
queryset=ComponentVersionContent.objects.select_related("content"),
|
|
102
|
+
to_attr="prefetched_contents",
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def get_versions_to_write(self, entity: PublishableEntity):
|
|
108
|
+
"""
|
|
109
|
+
Get the versions of a publishable entity that should be written to the zip file.
|
|
110
|
+
It retrieves both draft and published versions.
|
|
111
|
+
"""
|
|
112
|
+
draft_version: Optional[PublishableEntityVersion] = publishing_api.get_draft_version(entity)
|
|
113
|
+
published_version: Optional[PublishableEntityVersion] = publishing_api.get_published_version(entity)
|
|
114
|
+
|
|
115
|
+
versions_to_write = [draft_version] if draft_version else []
|
|
116
|
+
|
|
117
|
+
if published_version and published_version != draft_version:
|
|
118
|
+
versions_to_write.append(published_version)
|
|
119
|
+
return versions_to_write
|
|
120
|
+
|
|
121
|
+
def create_zip(self, path: str) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Creates a zip file containing the learning package data.
|
|
124
|
+
Args:
|
|
125
|
+
path (str): The path where the zip file will be created.
|
|
126
|
+
Raises:
|
|
127
|
+
Exception: If the learning package cannot be found or if the zip creation fails.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
|
131
|
+
# Add the package.toml file
|
|
132
|
+
package_toml_content: str = toml_learning_package(self.learning_package)
|
|
133
|
+
zipf.writestr(TOML_PACKAGE_NAME, package_toml_content)
|
|
134
|
+
|
|
135
|
+
# Add the entities directory
|
|
136
|
+
entities_folder = Path("entities")
|
|
137
|
+
self.create_folder(entities_folder, zipf)
|
|
138
|
+
|
|
139
|
+
# Add the collections directory
|
|
140
|
+
collections_folder = Path("collections")
|
|
141
|
+
self.create_folder(collections_folder, zipf)
|
|
142
|
+
|
|
143
|
+
# ------ ENTITIES SERIALIZATION -------------
|
|
144
|
+
|
|
145
|
+
# get the publishable entities
|
|
146
|
+
publishable_entities: QuerySet[PublishableEntity] = self.get_publishable_entities()
|
|
147
|
+
|
|
148
|
+
for entity in publishable_entities:
|
|
149
|
+
# entity: PublishableEntity = entity # Type hint for clarity
|
|
150
|
+
|
|
151
|
+
# Create a TOML representation of the entity
|
|
152
|
+
entity_toml_content: str = toml_publishable_entity(entity)
|
|
153
|
+
|
|
154
|
+
if hasattr(entity, 'container'):
|
|
155
|
+
entity_slugify_hash = slugify_hashed_filename(entity.key)
|
|
156
|
+
entity_toml_filename = f"{entity_slugify_hash}.toml"
|
|
157
|
+
entity_toml_path = entities_folder / entity_toml_filename
|
|
158
|
+
zipf.writestr(str(entity_toml_path), entity_toml_content)
|
|
159
|
+
|
|
160
|
+
if hasattr(entity, 'component'):
|
|
161
|
+
# Create the component folder structure for the entity. The structure is as follows:
|
|
162
|
+
# entities/
|
|
163
|
+
# xblock.v1/ (component namespace)
|
|
164
|
+
# html/ (component type)
|
|
165
|
+
# my_component.toml (entity TOML file)
|
|
166
|
+
# my_component/ (component id)
|
|
167
|
+
# component_versions/
|
|
168
|
+
# v1/
|
|
169
|
+
# static/
|
|
170
|
+
|
|
171
|
+
# Generate the slugified hash for the component local key
|
|
172
|
+
# Example: if the local key is "my_component", the slugified hash might be "my_component_123456"
|
|
173
|
+
# It's a combination of the local key and a hash and should be unique
|
|
174
|
+
entity_slugify_hash = slugify_hashed_filename(entity.component.local_key)
|
|
175
|
+
|
|
176
|
+
# Create the component namespace folder
|
|
177
|
+
# Example of component namespace is: "entities/xblock.v1/"
|
|
178
|
+
component_namespace_folder = entities_folder / entity.component.component_type.namespace
|
|
179
|
+
self.create_folder(component_namespace_folder, zipf)
|
|
180
|
+
|
|
181
|
+
# Create the component type folder
|
|
182
|
+
# Example of component type is: "entities/xblock.v1/html/"
|
|
183
|
+
component_type_folder = component_namespace_folder / entity.component.component_type.name
|
|
184
|
+
self.create_folder(component_type_folder, zipf)
|
|
185
|
+
|
|
186
|
+
# Create the component id folder
|
|
187
|
+
# Example of component id is: "entities/xblock.v1/html/my_component_123456/"
|
|
188
|
+
component_id_folder = component_type_folder / entity_slugify_hash
|
|
189
|
+
self.create_folder(component_id_folder, zipf)
|
|
190
|
+
|
|
191
|
+
# Add the entity TOML file inside the component type folder as well
|
|
192
|
+
# Example: "entities/xblock.v1/html/my_component_123456.toml"
|
|
193
|
+
component_entity_toml_path = component_type_folder / f"{entity_slugify_hash}.toml"
|
|
194
|
+
zipf.writestr(str(component_entity_toml_path), entity_toml_content)
|
|
195
|
+
|
|
196
|
+
# Add component version folder into the component id folder
|
|
197
|
+
# Example: "entities/xblock.v1/html/my_component_123456/component_versions/"
|
|
198
|
+
component_version_folder = component_id_folder / "component_versions"
|
|
199
|
+
self.create_folder(component_version_folder, zipf)
|
|
200
|
+
|
|
201
|
+
# ------ COMPONENT VERSIONING -------------
|
|
202
|
+
# Focusing on draft and published versions
|
|
203
|
+
|
|
204
|
+
# Get the draft and published versions
|
|
205
|
+
versions_to_write: List[PublishableEntityVersion] = self.get_versions_to_write(entity)
|
|
206
|
+
|
|
207
|
+
for version in versions_to_write:
|
|
208
|
+
# Create a folder for the version
|
|
209
|
+
version_number = f"v{version.version_num}"
|
|
210
|
+
version_folder = component_version_folder / version_number
|
|
211
|
+
self.create_folder(version_folder, zipf)
|
|
212
|
+
|
|
213
|
+
# Add static folder for the version
|
|
214
|
+
static_folder = version_folder / "static"
|
|
215
|
+
self.create_folder(static_folder, zipf)
|
|
216
|
+
|
|
217
|
+
# ------ COMPONENT STATIC CONTENT -------------
|
|
218
|
+
component_version: ComponentVersion = version.componentversion
|
|
219
|
+
|
|
220
|
+
# Get content data associated with this version
|
|
221
|
+
contents: QuerySet[
|
|
222
|
+
ComponentVersionContent
|
|
223
|
+
] = component_version.prefetched_contents # type: ignore[attr-defined]
|
|
224
|
+
|
|
225
|
+
for component_version_content in contents:
|
|
226
|
+
content: Content = component_version_content.content
|
|
227
|
+
|
|
228
|
+
# Important: The component_version_content.key contains implicitly
|
|
229
|
+
# the file name and the file extension
|
|
230
|
+
file_path = version_folder / component_version_content.key
|
|
231
|
+
|
|
232
|
+
if content.has_file and content.path:
|
|
233
|
+
# If has_file, we pull it from the file system
|
|
234
|
+
with content.read_file() as f:
|
|
235
|
+
file_data = f.read()
|
|
236
|
+
elif not content.has_file and content.text:
|
|
237
|
+
# Otherwise, we use the text content as the file data
|
|
238
|
+
file_data = content.text
|
|
239
|
+
else:
|
|
240
|
+
# If no file and no text, we skip this content
|
|
241
|
+
continue
|
|
242
|
+
zipf.writestr(str(file_path), file_data)
|
|
@@ -18,7 +18,7 @@ class ComponentsConfig(AppConfig):
|
|
|
18
18
|
"""
|
|
19
19
|
Register Component and ComponentVersion.
|
|
20
20
|
"""
|
|
21
|
-
from ..publishing.api import
|
|
21
|
+
from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
|
|
22
22
|
from .models import Component, ComponentVersion # pylint: disable=import-outside-toplevel
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
register_publishable_models(Component, ComponentVersion)
|
|
@@ -10,7 +10,7 @@ from contextlib import nullcontext
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
from enum import Enum
|
|
13
|
-
from typing import ContextManager, TypeVar
|
|
13
|
+
from typing import ContextManager, Optional, TypeVar
|
|
14
14
|
|
|
15
15
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
16
16
|
from django.db.models import F, Q, QuerySet
|
|
@@ -58,6 +58,7 @@ __all__ = [
|
|
|
58
58
|
"create_publishable_entity_version",
|
|
59
59
|
"get_publishable_entity",
|
|
60
60
|
"get_publishable_entity_by_key",
|
|
61
|
+
"get_publishable_entities",
|
|
61
62
|
"get_last_publish",
|
|
62
63
|
"get_all_drafts",
|
|
63
64
|
"get_entities_with_unpublished_changes",
|
|
@@ -69,7 +70,7 @@ __all__ = [
|
|
|
69
70
|
"set_draft_version",
|
|
70
71
|
"soft_delete_draft",
|
|
71
72
|
"reset_drafts_to_published",
|
|
72
|
-
"
|
|
73
|
+
"register_publishable_models",
|
|
73
74
|
"filter_publishable_entities",
|
|
74
75
|
# 🛑 UNSTABLE: All APIs related to containers are unstable until we've figured
|
|
75
76
|
# out our approach to dynamic content (randomized, A/B tests, etc.)
|
|
@@ -261,6 +262,20 @@ def get_all_drafts(learning_package_id: int, /) -> QuerySet[Draft]:
|
|
|
261
262
|
)
|
|
262
263
|
|
|
263
264
|
|
|
265
|
+
def get_publishable_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
|
|
266
|
+
"""
|
|
267
|
+
Get all entities in a learning package.
|
|
268
|
+
"""
|
|
269
|
+
return (
|
|
270
|
+
PublishableEntity.objects
|
|
271
|
+
.filter(learning_package_id=learning_package_id)
|
|
272
|
+
.select_related(
|
|
273
|
+
"draft__version",
|
|
274
|
+
"published__version",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
264
279
|
def get_entities_with_unpublished_changes(
|
|
265
280
|
learning_package_id: int,
|
|
266
281
|
/,
|
|
@@ -417,15 +432,22 @@ def publish_from_drafts(
|
|
|
417
432
|
return publish_log
|
|
418
433
|
|
|
419
434
|
|
|
420
|
-
def get_draft_version(
|
|
435
|
+
def get_draft_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
|
|
421
436
|
"""
|
|
422
437
|
Return current draft PublishableEntityVersion for this PublishableEntity.
|
|
423
438
|
|
|
424
439
|
This function will return None if there is no current draft.
|
|
425
440
|
"""
|
|
441
|
+
if isinstance(publishable_entity_or_id, PublishableEntity):
|
|
442
|
+
# Fetches the draft version for a given PublishableEntity.
|
|
443
|
+
# Gracefully handles cases where no draft is present.
|
|
444
|
+
draft: Optional[Draft] = getattr(publishable_entity_or_id, "draft", None)
|
|
445
|
+
if draft is None:
|
|
446
|
+
return None
|
|
447
|
+
return draft.version
|
|
426
448
|
try:
|
|
427
449
|
draft = Draft.objects.select_related("version").get(
|
|
428
|
-
entity_id=
|
|
450
|
+
entity_id=publishable_entity_or_id
|
|
429
451
|
)
|
|
430
452
|
except Draft.DoesNotExist:
|
|
431
453
|
# No draft was ever created.
|
|
@@ -437,15 +459,22 @@ def get_draft_version(publishable_entity_id: int, /) -> PublishableEntityVersion
|
|
|
437
459
|
return draft.version
|
|
438
460
|
|
|
439
461
|
|
|
440
|
-
def get_published_version(
|
|
462
|
+
def get_published_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
|
|
441
463
|
"""
|
|
442
464
|
Return current published PublishableEntityVersion for this PublishableEntity.
|
|
443
465
|
|
|
444
466
|
This function will return None if there is no current published version.
|
|
445
467
|
"""
|
|
468
|
+
if isinstance(publishable_entity_or_id, PublishableEntity):
|
|
469
|
+
# Fetches the published version for a given PublishableEntity.
|
|
470
|
+
# Gracefully handles cases where no published version is present.
|
|
471
|
+
published: Optional[Published] = getattr(publishable_entity_or_id, "published", None)
|
|
472
|
+
if published is None:
|
|
473
|
+
return None
|
|
474
|
+
return published.version
|
|
446
475
|
try:
|
|
447
476
|
published = Published.objects.select_related("version").get(
|
|
448
|
-
entity_id=
|
|
477
|
+
entity_id=publishable_entity_or_id
|
|
449
478
|
)
|
|
450
479
|
except Published.DoesNotExist:
|
|
451
480
|
return None
|
|
@@ -789,7 +818,7 @@ def reset_drafts_to_published(
|
|
|
789
818
|
set_draft_version(draft, published_version_id)
|
|
790
819
|
|
|
791
820
|
|
|
792
|
-
def
|
|
821
|
+
def register_publishable_models(
|
|
793
822
|
content_model_cls: type[PublishableEntityMixin],
|
|
794
823
|
content_version_model_cls: type[PublishableEntityVersionMixin],
|
|
795
824
|
) -> PublishableContentModelRegistry:
|
|
@@ -805,10 +834,10 @@ def register_content_models(
|
|
|
805
834
|
method. For example, in the components app, this looks like:
|
|
806
835
|
|
|
807
836
|
def ready(self):
|
|
808
|
-
from ..publishing.api import
|
|
837
|
+
from ..publishing.api import register_publishable_models
|
|
809
838
|
from .models import Component, ComponentVersion
|
|
810
839
|
|
|
811
|
-
|
|
840
|
+
register_publishable_models(Component, ComponentVersion)
|
|
812
841
|
|
|
813
842
|
There may be a more clever way to introspect this information from the model
|
|
814
843
|
metadata, but this is simple and explicit.
|
|
@@ -1275,6 +1304,7 @@ def get_entities_in_container(
|
|
|
1275
1304
|
container: Container,
|
|
1276
1305
|
*,
|
|
1277
1306
|
published: bool,
|
|
1307
|
+
select_related_version: str | None = None,
|
|
1278
1308
|
) -> list[ContainerEntityListEntry]:
|
|
1279
1309
|
"""
|
|
1280
1310
|
[ 🛑 UNSTABLE ]
|
|
@@ -1285,14 +1315,35 @@ def get_entities_in_container(
|
|
|
1285
1315
|
container: The Container, e.g. returned by `get_container()`
|
|
1286
1316
|
published: `True` if we want the published version of the container, or
|
|
1287
1317
|
`False` for the draft version.
|
|
1318
|
+
select_related_version: An optional optimization; specify a relationship
|
|
1319
|
+
on ContainerVersion, like `componentversion` or `containerversion__x`
|
|
1320
|
+
to preload via select_related.
|
|
1288
1321
|
"""
|
|
1289
1322
|
assert isinstance(container, Container)
|
|
1290
|
-
|
|
1323
|
+
if published:
|
|
1324
|
+
# Very minor optimization: reload the container with related 1:1 entities
|
|
1325
|
+
container = Container.objects.select_related(
|
|
1326
|
+
"publishable_entity__published__version__containerversion__entity_list").get(pk=container.pk)
|
|
1327
|
+
container_version = container.versioning.published
|
|
1328
|
+
select_related = ["entity__published__version"]
|
|
1329
|
+
if select_related_version:
|
|
1330
|
+
select_related.append(f"entity__published__version__{select_related_version}")
|
|
1331
|
+
else:
|
|
1332
|
+
# Very minor optimization: reload the container with related 1:1 entities
|
|
1333
|
+
container = Container.objects.select_related(
|
|
1334
|
+
"publishable_entity__draft__version__containerversion__entity_list").get(pk=container.pk)
|
|
1335
|
+
container_version = container.versioning.draft
|
|
1336
|
+
select_related = ["entity__draft__version"]
|
|
1337
|
+
if select_related_version:
|
|
1338
|
+
select_related.append(f"entity__draft__version__{select_related_version}")
|
|
1291
1339
|
if container_version is None:
|
|
1292
1340
|
raise ContainerVersion.DoesNotExist # This container has not been published yet, or has been deleted.
|
|
1293
1341
|
assert isinstance(container_version, ContainerVersion)
|
|
1294
|
-
entity_list = []
|
|
1295
|
-
for row in container_version.entity_list.entitylistrow_set.
|
|
1342
|
+
entity_list: list[ContainerEntityListEntry] = []
|
|
1343
|
+
for row in container_version.entity_list.entitylistrow_set.select_related(
|
|
1344
|
+
"entity_version",
|
|
1345
|
+
*select_related,
|
|
1346
|
+
).order_by("order_num"):
|
|
1296
1347
|
entity_version = row.entity_version # This will be set if pinned
|
|
1297
1348
|
if not entity_version: # If this entity is "unpinned", use the latest published/draft version:
|
|
1298
1349
|
entity_version = row.entity.published.version if published else row.entity.draft.version
|
|
@@ -1385,7 +1436,10 @@ def get_containers_with_entity(
|
|
|
1385
1436
|
qs = Container.objects.filter(
|
|
1386
1437
|
publishable_entity__draft__version__containerversion__entity_list__entitylistrow__entity_id=publishable_entity_pk, # pylint: disable=line-too-long # noqa: E501
|
|
1387
1438
|
)
|
|
1388
|
-
return qs.
|
|
1439
|
+
return qs.select_related(
|
|
1440
|
+
"publishable_entity__draft__version__containerversion",
|
|
1441
|
+
"publishable_entity__published__version__containerversion",
|
|
1442
|
+
).order_by("pk").distinct() # Ordering is mostly for consistent test cases.
|
|
1389
1443
|
|
|
1390
1444
|
|
|
1391
1445
|
def get_container_children_count(
|
|
@@ -19,7 +19,7 @@ class PublishingConfig(AppConfig):
|
|
|
19
19
|
"""
|
|
20
20
|
Register Container and ContainerVersion.
|
|
21
21
|
"""
|
|
22
|
-
from .api import
|
|
22
|
+
from .api import register_publishable_models # pylint: disable=import-outside-toplevel
|
|
23
23
|
from .models import Container, ContainerVersion # pylint: disable=import-outside-toplevel
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
register_publishable_models(Container, ContainerVersion)
|
|
@@ -272,7 +272,7 @@ class PublishableEntityMixin(models.Model):
|
|
|
272
272
|
Please see docstring for PublishableEntity for more details.
|
|
273
273
|
|
|
274
274
|
If you use this class, you *MUST* also use PublishableEntityVersionMixin and
|
|
275
|
-
the publishing app's api.
|
|
275
|
+
the publishing app's api.register_publishable_models (see its docstring for
|
|
276
276
|
details).
|
|
277
277
|
"""
|
|
278
278
|
# select these related entities by default for all queries
|
|
@@ -294,6 +294,10 @@ class PublishableEntityMixin(models.Model):
|
|
|
294
294
|
def uuid(self) -> str:
|
|
295
295
|
return self.publishable_entity.uuid
|
|
296
296
|
|
|
297
|
+
@property
|
|
298
|
+
def can_stand_alone(self) -> bool:
|
|
299
|
+
return self.publishable_entity.can_stand_alone
|
|
300
|
+
|
|
297
301
|
@property
|
|
298
302
|
def key(self) -> str:
|
|
299
303
|
return self.publishable_entity.key
|
|
@@ -551,7 +555,7 @@ class PublishableEntityVersionMixin(models.Model):
|
|
|
551
555
|
Please see docstring for PublishableEntityVersion for more details.
|
|
552
556
|
|
|
553
557
|
If you use this class, you *MUST* also use PublishableEntityMixin and the
|
|
554
|
-
publishing app's api.
|
|
558
|
+
publishing app's api.register_publishable_models (see its docstring for
|
|
555
559
|
details).
|
|
556
560
|
"""
|
|
557
561
|
|
|
@@ -609,7 +613,7 @@ class PublishableContentModelRegistry:
|
|
|
609
613
|
Register what content model maps to what content version model.
|
|
610
614
|
|
|
611
615
|
If you want to call this from another app, please use the
|
|
612
|
-
``
|
|
616
|
+
``register_publishable_models`` function in this app's ``api`` module
|
|
613
617
|
instead.
|
|
614
618
|
"""
|
|
615
619
|
if not issubclass(content_model_cls, PublishableEntityMixin):
|
{openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/api.py
RENAMED
|
@@ -257,7 +257,12 @@ def get_subsections_in_section(
|
|
|
257
257
|
"""
|
|
258
258
|
assert isinstance(section, Section)
|
|
259
259
|
subsections = []
|
|
260
|
-
|
|
260
|
+
entries = publishing_api.get_entities_in_container(
|
|
261
|
+
section,
|
|
262
|
+
published=published,
|
|
263
|
+
select_related_version="containerversion__subsectionversion",
|
|
264
|
+
)
|
|
265
|
+
for entry in entries:
|
|
261
266
|
# Convert from generic PublishableEntityVersion to SubsectionVersion:
|
|
262
267
|
subsection_version = entry.entity_version.containerversion.subsectionversion
|
|
263
268
|
assert isinstance(subsection_version, SubsectionVersion)
|
{openedx_learning-0.27.0 → openedx_learning-0.28.0}/openedx_learning/apps/authoring/sections/apps.py
RENAMED
|
@@ -19,7 +19,7 @@ class SectionsConfig(AppConfig):
|
|
|
19
19
|
"""
|
|
20
20
|
Register Section and SectionVersion.
|
|
21
21
|
"""
|
|
22
|
-
from ..publishing.api import
|
|
22
|
+
from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
|
|
23
23
|
from .models import Section, SectionVersion # pylint: disable=import-outside-toplevel
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
register_publishable_models(Section, SectionVersion)
|
|
@@ -256,7 +256,12 @@ def get_units_in_subsection(
|
|
|
256
256
|
"""
|
|
257
257
|
assert isinstance(subsection, Subsection)
|
|
258
258
|
units = []
|
|
259
|
-
|
|
259
|
+
entries = publishing_api.get_entities_in_container(
|
|
260
|
+
subsection,
|
|
261
|
+
published=published,
|
|
262
|
+
select_related_version="containerversion__unitversion",
|
|
263
|
+
)
|
|
264
|
+
for entry in entries:
|
|
260
265
|
# Convert from generic PublishableEntityVersion to UnitVersion:
|
|
261
266
|
unit_version = entry.entity_version.containerversion.unitversion
|
|
262
267
|
assert isinstance(unit_version, UnitVersion)
|