openedx-core 1.4.1__py3-none-any.whl
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_catalog/ARCHITECTURE.md +36 -0
- openedx_catalog/README.rst +22 -0
- openedx_catalog/__init__.py +0 -0
- openedx_catalog/admin.py +112 -0
- openedx_catalog/api.py +30 -0
- openedx_catalog/api_impl.py +251 -0
- openedx_catalog/apps.py +21 -0
- openedx_catalog/migrations/0001_initial.py +209 -0
- openedx_catalog/migrations/__init__.py +0 -0
- openedx_catalog/models/__init__.py +6 -0
- openedx_catalog/models/catalog_course.py +246 -0
- openedx_catalog/models/course_run.py +256 -0
- openedx_catalog/models_api.py +10 -0
- openedx_catalog/py.typed +2 -0
- openedx_catalog/signals.py +51 -0
- openedx_content/README.rst +5 -0
- openedx_content/__init__.py +0 -0
- openedx_content/admin.py +11 -0
- openedx_content/api.py +28 -0
- openedx_content/applets/__init__.py +0 -0
- openedx_content/applets/backup_restore/__init__.py +0 -0
- openedx_content/applets/backup_restore/admin.py +3 -0
- openedx_content/applets/backup_restore/api.py +42 -0
- openedx_content/applets/backup_restore/models.py +3 -0
- openedx_content/applets/backup_restore/serializers.py +216 -0
- openedx_content/applets/backup_restore/toml.py +259 -0
- openedx_content/applets/backup_restore/zipper.py +1139 -0
- openedx_content/applets/collections/__init__.py +0 -0
- openedx_content/applets/collections/admin.py +42 -0
- openedx_content/applets/collections/api.py +336 -0
- openedx_content/applets/collections/models.py +253 -0
- openedx_content/applets/collections/readme.rst +23 -0
- openedx_content/applets/collections/signals.py +80 -0
- openedx_content/applets/collections/tasks.py +86 -0
- openedx_content/applets/components/__init__.py +0 -0
- openedx_content/applets/components/admin.py +159 -0
- openedx_content/applets/components/api.py +696 -0
- openedx_content/applets/components/models.py +324 -0
- openedx_content/applets/components/readme.rst +23 -0
- openedx_content/applets/containers/__init__.py +0 -0
- openedx_content/applets/containers/admin.py +343 -0
- openedx_content/applets/containers/api.py +920 -0
- openedx_content/applets/containers/models.py +350 -0
- openedx_content/applets/media/__init__.py +0 -0
- openedx_content/applets/media/admin.py +71 -0
- openedx_content/applets/media/api.py +241 -0
- openedx_content/applets/media/models.py +486 -0
- openedx_content/applets/publishing/__init__.py +0 -0
- openedx_content/applets/publishing/admin.py +344 -0
- openedx_content/applets/publishing/api.py +1793 -0
- openedx_content/applets/publishing/contextmanagers.py +157 -0
- openedx_content/applets/publishing/models/__init__.py +26 -0
- openedx_content/applets/publishing/models/draft_log.py +438 -0
- openedx_content/applets/publishing/models/learning_package.py +91 -0
- openedx_content/applets/publishing/models/publish_log.py +316 -0
- openedx_content/applets/publishing/models/publishable_entity.py +740 -0
- openedx_content/applets/publishing/readme.rst +28 -0
- openedx_content/applets/publishing/signal_handlers.py +41 -0
- openedx_content/applets/publishing/signals.py +247 -0
- openedx_content/applets/sections/__init__.py +0 -0
- openedx_content/applets/sections/api.py +134 -0
- openedx_content/applets/sections/models.py +78 -0
- openedx_content/applets/subsections/__init__.py +0 -0
- openedx_content/applets/subsections/api.py +134 -0
- openedx_content/applets/subsections/models.py +79 -0
- openedx_content/applets/units/__init__.py +0 -0
- openedx_content/applets/units/api.py +134 -0
- openedx_content/applets/units/models.py +74 -0
- openedx_content/apps.py +52 -0
- openedx_content/backcompat/__init__.py +0 -0
- openedx_content/backcompat/backup_restore/__init__.py +0 -0
- openedx_content/backcompat/backup_restore/apps.py +12 -0
- openedx_content/backcompat/backup_restore/migrations/__init__.py +0 -0
- openedx_content/backcompat/collections/__init__.py +0 -0
- openedx_content/backcompat/collections/apps.py +14 -0
- openedx_content/backcompat/collections/migrations/0001_initial.py +33 -0
- openedx_content/backcompat/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +53 -0
- openedx_content/backcompat/collections/migrations/0003_collection_entities.py +38 -0
- openedx_content/backcompat/collections/migrations/0004_collection_key.py +57 -0
- openedx_content/backcompat/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +18 -0
- openedx_content/backcompat/collections/migrations/0006_remove_all_field_state_for_move_to_applet.py +82 -0
- openedx_content/backcompat/collections/migrations/__init__.py +0 -0
- openedx_content/backcompat/collections/models.py +9 -0
- openedx_content/backcompat/components/__init__.py +0 -0
- openedx_content/backcompat/components/apps.py +14 -0
- openedx_content/backcompat/components/migrations/0001_initial.py +97 -0
- openedx_content/backcompat/components/migrations/0002_alter_componentversioncontent_key.py +20 -0
- openedx_content/backcompat/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +17 -0
- openedx_content/backcompat/components/migrations/0004_remove_componentversioncontent_uuid.py +17 -0
- openedx_content/backcompat/components/migrations/0005_remove_all_field_state_for_move_to_applet.py +72 -0
- openedx_content/backcompat/components/migrations/__init__.py +0 -0
- openedx_content/backcompat/components/models.py +11 -0
- openedx_content/backcompat/contents/__init__.py +0 -0
- openedx_content/backcompat/contents/apps.py +14 -0
- openedx_content/backcompat/contents/migrations/0001_initial.py +66 -0
- openedx_content/backcompat/contents/migrations/0002_remove_all_field_state_for_move_to_applet.py +26 -0
- openedx_content/backcompat/contents/migrations/__init__.py +0 -0
- openedx_content/backcompat/publishing/__init__.py +0 -0
- openedx_content/backcompat/publishing/apps.py +14 -0
- openedx_content/backcompat/publishing/migrations/0001_initial.py +166 -0
- openedx_content/backcompat/publishing/migrations/0002_alter_learningpackage_key_and_more.py +25 -0
- openedx_content/backcompat/publishing/migrations/0003_containers.py +54 -0
- openedx_content/backcompat/publishing/migrations/0004_publishableentity_can_stand_alone.py +21 -0
- openedx_content/backcompat/publishing/migrations/0005_alter_entitylistrow_options.py +17 -0
- openedx_content/backcompat/publishing/migrations/0006_draftchangelog.py +68 -0
- openedx_content/backcompat/publishing/migrations/0007_bootstrap_draftchangelog.py +94 -0
- openedx_content/backcompat/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +31 -0
- openedx_content/backcompat/publishing/migrations/0009_dependencies_and_hashing.py +62 -0
- openedx_content/backcompat/publishing/migrations/0010_backfill_dependencies.py +441 -0
- openedx_content/backcompat/publishing/migrations/0011_remove_all_field_state_for_move_to_applet.py +288 -0
- openedx_content/backcompat/publishing/migrations/__init__.py +0 -0
- openedx_content/backcompat/publishing/models.py +27 -0
- openedx_content/backcompat/readme.rst +6 -0
- openedx_content/backcompat/sections/__init__.py +0 -0
- openedx_content/backcompat/sections/apps.py +15 -0
- openedx_content/backcompat/sections/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/sections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/sections/migrations/__init__.py +0 -0
- openedx_content/backcompat/subsections/__init__.py +0 -0
- openedx_content/backcompat/subsections/apps.py +15 -0
- openedx_content/backcompat/subsections/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/subsections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/subsections/migrations/__init__.py +0 -0
- openedx_content/backcompat/units/__init__.py +0 -0
- openedx_content/backcompat/units/apps.py +15 -0
- openedx_content/backcompat/units/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/units/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/units/migrations/__init__.py +0 -0
- openedx_content/management/__init__.py +0 -0
- openedx_content/management/commands/__init__.py +0 -0
- openedx_content/management/commands/add_assets_to_component.py +86 -0
- openedx_content/management/commands/lp_dump.py +69 -0
- openedx_content/management/commands/lp_load.py +57 -0
- openedx_content/migrations/0001_initial.py +654 -0
- openedx_content/migrations/0002_rename_tables_to_openedx_content.py +138 -0
- openedx_content/migrations/0003_rename_content_to_media.py +51 -0
- openedx_content/migrations/0004_componenttype_constraint.py +49 -0
- openedx_content/migrations/0005_containertypes.py +94 -0
- openedx_content/migrations/0006_typed_ids.py +29 -0
- openedx_content/migrations/0007_publishlogrecord_direct.py +32 -0
- openedx_content/migrations/0008_rename_collection_key_to_collection_code.py +72 -0
- openedx_content/migrations/0009_rename_component_local_key_to_component_code.py +81 -0
- openedx_content/migrations/0010_add_container_code.py +104 -0
- openedx_content/migrations/0011_rename_entity_key_and_package_key_to_refs.py +84 -0
- openedx_content/migrations/0012_rename_componentversionmedia_key_to_path.py +46 -0
- openedx_content/migrations/0013_unicode_container_component_codes.py +60 -0
- openedx_content/migrations/0014_typed_media_id.py +28 -0
- openedx_content/migrations/__init__.py +0 -0
- openedx_content/models.py +18 -0
- openedx_content/models_api.py +18 -0
- openedx_content/py.typed +2 -0
- openedx_content/settings_api.py +38 -0
- openedx_content/signals.py +20 -0
- openedx_core/__init__.py +10 -0
- openedx_core-1.4.1.dist-info/METADATA +137 -0
- openedx_core-1.4.1.dist-info/RECORD +244 -0
- openedx_core-1.4.1.dist-info/WHEEL +5 -0
- openedx_core-1.4.1.dist-info/licenses/LICENSE.txt +674 -0
- openedx_core-1.4.1.dist-info/top_level.txt +6 -0
- openedx_django_lib/README.rst +9 -0
- openedx_django_lib/__init__.py +6 -0
- openedx_django_lib/admin_utils.py +116 -0
- openedx_django_lib/collations.py +65 -0
- openedx_django_lib/fields.py +282 -0
- openedx_django_lib/id_fields.py +57 -0
- openedx_django_lib/id_fields.pyi +17 -0
- openedx_django_lib/managers.py +48 -0
- openedx_django_lib/py.typed +2 -0
- openedx_django_lib/validators.py +15 -0
- openedx_learning/README.rst +11 -0
- openedx_learning/__init__.py +3 -0
- openedx_learning/admin.py +6 -0
- openedx_learning/api.py +6 -0
- openedx_learning/applets/__init__.py +3 -0
- openedx_learning/applets/cbe/README.rst +21 -0
- openedx_learning/applets/cbe/__init__.py +3 -0
- openedx_learning/applets/cbe/admin.py +21 -0
- openedx_learning/applets/cbe/api.py +65 -0
- openedx_learning/applets/cbe/models.py +40 -0
- openedx_learning/apps.py +15 -0
- openedx_learning/migrations/0001_initial.py +27 -0
- openedx_learning/migrations/__init__.py +0 -0
- openedx_learning/models.py +7 -0
- openedx_learning/models_api.py +9 -0
- openedx_learning/py.typed +0 -0
- openedx_tagging/__init__.py +0 -0
- openedx_tagging/admin.py +44 -0
- openedx_tagging/api.py +573 -0
- openedx_tagging/apps.py +23 -0
- openedx_tagging/data.py +39 -0
- openedx_tagging/import_export/__init__.py +4 -0
- openedx_tagging/import_export/actions.py +451 -0
- openedx_tagging/import_export/api.py +231 -0
- openedx_tagging/import_export/exceptions.py +133 -0
- openedx_tagging/import_export/import_plan.py +218 -0
- openedx_tagging/import_export/parsers.py +351 -0
- openedx_tagging/import_export/tasks.py +43 -0
- openedx_tagging/import_export/template.csv +30 -0
- openedx_tagging/import_export/template.json +158 -0
- openedx_tagging/migrations/0001_initial.py +212 -0
- openedx_tagging/migrations/0001_squashed.py +152 -0
- openedx_tagging/migrations/0002_auto_20230718_2026.py +79 -0
- openedx_tagging/migrations/0003_auto_20230721_1238.py +76 -0
- openedx_tagging/migrations/0004_auto_20230723_2001.py +35 -0
- openedx_tagging/migrations/0005_language_taxonomy.py +31 -0
- openedx_tagging/migrations/0006_alter_objecttag_unique_together.py +16 -0
- openedx_tagging/migrations/0006_auto_20230802_1631.py +82 -0
- openedx_tagging/migrations/0007_tag_import_task_log_null_fix.py +19 -0
- openedx_tagging/migrations/0008_taxonomy_description_not_null.py +24 -0
- openedx_tagging/migrations/0009_alter_objecttag_object_id.py +20 -0
- openedx_tagging/migrations/0010_cleanups.py +32 -0
- openedx_tagging/migrations/0011_remove_required.py +22 -0
- openedx_tagging/migrations/0012_language_taxonomy.py +43 -0
- openedx_tagging/migrations/0013_tag_parent_blank.py +19 -0
- openedx_tagging/migrations/0014_minor_fixes.py +36 -0
- openedx_tagging/migrations/0015_taxonomy_export_id.py +38 -0
- openedx_tagging/migrations/0016_object_tag_export_id.py +62 -0
- openedx_tagging/migrations/0017_alter_tagimporttask_status.py +18 -0
- openedx_tagging/migrations/0018_objecttag_is_copied.py +18 -0
- openedx_tagging/migrations/0019_language_taxonomy_class.py +33 -0
- openedx_tagging/migrations/0020_tag_depth_and_lineage.py +163 -0
- openedx_tagging/migrations/0021_remove_system_defined_add_read_only.py +89 -0
- openedx_tagging/migrations/__init__.py +0 -0
- openedx_tagging/models/__init__.py +5 -0
- openedx_tagging/models/base.py +885 -0
- openedx_tagging/models/import_export.py +150 -0
- openedx_tagging/models/utils.py +13 -0
- openedx_tagging/py.typed +0 -0
- openedx_tagging/readme.rst +15 -0
- openedx_tagging/rest_api/__init__.py +0 -0
- openedx_tagging/rest_api/paginators.py +105 -0
- openedx_tagging/rest_api/urls.py +9 -0
- openedx_tagging/rest_api/utils.py +110 -0
- openedx_tagging/rest_api/v1/__init__.py +0 -0
- openedx_tagging/rest_api/v1/exception_handlers.py +58 -0
- openedx_tagging/rest_api/v1/permissions.py +68 -0
- openedx_tagging/rest_api/v1/serializers.py +476 -0
- openedx_tagging/rest_api/v1/urls.py +27 -0
- openedx_tagging/rest_api/v1/views.py +969 -0
- openedx_tagging/rest_api/v1/views_import.py +53 -0
- openedx_tagging/rules.py +210 -0
- openedx_tagging/signal_handlers.py +119 -0
- openedx_tagging/tasks.py +82 -0
- openedx_tagging/urls.py +10 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Generated by Django 5.2.11 on 2026-02-10 03:08
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django.db.models.functions.text
|
|
5
|
+
import django.db.models.lookups
|
|
6
|
+
import opaque_keys.edx.django.models
|
|
7
|
+
from django.db import migrations, models
|
|
8
|
+
|
|
9
|
+
import openedx_catalog.models.catalog_course
|
|
10
|
+
import openedx_django_lib.fields
|
|
11
|
+
import openedx_django_lib.validators
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Migration(migrations.Migration):
|
|
15
|
+
initial = True
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
("organizations", "0004_auto_20230727_2054"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
operations = [
|
|
22
|
+
migrations.CreateModel(
|
|
23
|
+
name="CatalogCourse",
|
|
24
|
+
fields=[
|
|
25
|
+
(
|
|
26
|
+
"id",
|
|
27
|
+
models.BigAutoField(
|
|
28
|
+
editable=False,
|
|
29
|
+
help_text="The internal database ID for this catalog course. Should not be exposed to users nor in APIs.",
|
|
30
|
+
primary_key=True,
|
|
31
|
+
serialize=False,
|
|
32
|
+
verbose_name="Primary Key",
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
(
|
|
36
|
+
"course_code",
|
|
37
|
+
openedx_django_lib.fields.MultiCollationCharField(
|
|
38
|
+
db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"},
|
|
39
|
+
help_text='The course code/number, e.g. "Math100".',
|
|
40
|
+
max_length=255,
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"created",
|
|
45
|
+
models.DateTimeField(
|
|
46
|
+
auto_now_add=True, validators=[openedx_django_lib.validators.validate_utc_datetime]
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
"title",
|
|
51
|
+
openedx_django_lib.fields.MultiCollationCharField(
|
|
52
|
+
db_collations={"mysql": "utf8mb4_unicode_ci", "sqlite": "NOCASE"},
|
|
53
|
+
help_text='The full title (display name) of this catalog course. e.g. "Introduction to Calculus". Individual course runs may override this, e.g. "Intro to Calc (Fall 2026 with Dr. Newton)".',
|
|
54
|
+
max_length=255,
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"language",
|
|
59
|
+
openedx_django_lib.fields.MultiCollationCharField(
|
|
60
|
+
db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"},
|
|
61
|
+
default=openedx_catalog.models.catalog_course.get_default_language_code,
|
|
62
|
+
help_text='The code representing the primary language of this catalog course\'s content - the language in which the content is authored and which learners can learn without translation. (Translated versions of the course or parts of the course may be available in other languages if supported by the platform.) The first two digits must be the lowercase ISO 639-1 language code, optionally followed by a country/locale code. e.g. "en", "es", "fr-ca", "pt-br", "zh-cn", "zh-hk". ',
|
|
63
|
+
max_length=64,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"org",
|
|
68
|
+
models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="organizations.organization"),
|
|
69
|
+
),
|
|
70
|
+
],
|
|
71
|
+
options={
|
|
72
|
+
"verbose_name": "Catalog Course",
|
|
73
|
+
"verbose_name_plural": "Catalog Courses",
|
|
74
|
+
"ordering": ("-created",),
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
migrations.CreateModel(
|
|
78
|
+
name="CourseRun",
|
|
79
|
+
fields=[
|
|
80
|
+
(
|
|
81
|
+
"id",
|
|
82
|
+
models.BigAutoField(
|
|
83
|
+
editable=False,
|
|
84
|
+
help_text="The internal database ID for this course. Should not be exposed to users nor in APIs.",
|
|
85
|
+
primary_key=True,
|
|
86
|
+
serialize=False,
|
|
87
|
+
verbose_name="Primary Key",
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
(
|
|
91
|
+
"course_key",
|
|
92
|
+
opaque_keys.edx.django.models.CourseKeyField(
|
|
93
|
+
case_sensitive=True,
|
|
94
|
+
db_index=True,
|
|
95
|
+
editable=False,
|
|
96
|
+
help_text="The main identifier for this course. Includes the org, course code, and run.",
|
|
97
|
+
max_length=255,
|
|
98
|
+
verbose_name="Course Key",
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"run_code",
|
|
103
|
+
openedx_django_lib.fields.MultiCollationCharField(
|
|
104
|
+
db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"},
|
|
105
|
+
help_text='The code that identifies this particular run of the course, e.g. "2026", "2026Fall" or "2T2026"',
|
|
106
|
+
max_length=128,
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
"created",
|
|
111
|
+
models.DateTimeField(
|
|
112
|
+
auto_now_add=True, validators=[openedx_django_lib.validators.validate_utc_datetime]
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
(
|
|
116
|
+
"title",
|
|
117
|
+
openedx_django_lib.fields.MultiCollationCharField(
|
|
118
|
+
blank=True,
|
|
119
|
+
db_collations={"mysql": "utf8mb4_unicode_ci", "sqlite": "NOCASE"},
|
|
120
|
+
help_text='The full title (display name) of this course. e.g. "Introduction to Calculus". This is required and will override the title of the catalog course. Leave blank to use the same title as the catalog course. ',
|
|
121
|
+
max_length=255,
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
"catalog_course",
|
|
126
|
+
models.ForeignKey(
|
|
127
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
128
|
+
related_name="runs",
|
|
129
|
+
to="openedx_catalog.catalogcourse",
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
],
|
|
133
|
+
options={
|
|
134
|
+
"verbose_name": "Course Run",
|
|
135
|
+
"verbose_name_plural": "Course Runs",
|
|
136
|
+
"ordering": ("-created",),
|
|
137
|
+
},
|
|
138
|
+
),
|
|
139
|
+
migrations.AddIndex(
|
|
140
|
+
model_name="catalogcourse",
|
|
141
|
+
index=models.Index(fields=["org", "course_code"], name="openedx_cat_org_id_bd314b_idx"),
|
|
142
|
+
),
|
|
143
|
+
migrations.AddConstraint(
|
|
144
|
+
model_name="catalogcourse",
|
|
145
|
+
constraint=models.UniqueConstraint(
|
|
146
|
+
models.F("org"),
|
|
147
|
+
django.db.models.functions.text.Lower("course_code"),
|
|
148
|
+
name="oex_catalog_catalogcourse_org_code_uniq_ci",
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
migrations.AddConstraint(
|
|
152
|
+
model_name="catalogcourse",
|
|
153
|
+
constraint=models.CheckConstraint(
|
|
154
|
+
condition=models.Q(("course_code__length__gt", 0)),
|
|
155
|
+
name="oex_catalog_catalogcourse_course_code_not_blank",
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
migrations.AddConstraint(
|
|
159
|
+
model_name="catalogcourse",
|
|
160
|
+
constraint=models.CheckConstraint(
|
|
161
|
+
condition=models.Q(("title__length__gt", 0)), name="oex_catalog_catalogcourse_title_not_blank"
|
|
162
|
+
),
|
|
163
|
+
),
|
|
164
|
+
migrations.AddConstraint(
|
|
165
|
+
model_name="catalogcourse",
|
|
166
|
+
constraint=models.CheckConstraint(
|
|
167
|
+
condition=django.db.models.lookups.Regex(models.F("language"), "^[a-z][a-z](\\-[a-z0-9]+)*$"),
|
|
168
|
+
name="oex_catalog_catalogcourse_language_regex",
|
|
169
|
+
violation_error_message='The language code must be lowercase, e.g. "en". If a country/locale code is provided, it must be separated by a hyphen, e.g. "en-us", "zh-hk". ',
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
migrations.AddConstraint(
|
|
173
|
+
model_name="courserun",
|
|
174
|
+
constraint=models.UniqueConstraint(
|
|
175
|
+
models.F("catalog_course"),
|
|
176
|
+
models.F("run_code"),
|
|
177
|
+
condition=models.Q(("course_key__startswith", "ccx"), _negated=True),
|
|
178
|
+
name="oex_catalog_courserun_catalog_course_run_code_uniq",
|
|
179
|
+
),
|
|
180
|
+
),
|
|
181
|
+
migrations.AddConstraint(
|
|
182
|
+
model_name="courserun",
|
|
183
|
+
constraint=models.UniqueConstraint(
|
|
184
|
+
django.db.models.functions.text.Lower("course_key"), name="oex_catalog_courserun_course_key_ci"
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
migrations.AddConstraint(
|
|
188
|
+
model_name="courserun",
|
|
189
|
+
constraint=models.CheckConstraint(
|
|
190
|
+
condition=models.Q(("run_code__length__gt", 0)), name="oex_catalog_courserun_run_code_not_blank"
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
migrations.AddConstraint(
|
|
194
|
+
model_name="courserun",
|
|
195
|
+
constraint=models.CheckConstraint(
|
|
196
|
+
condition=models.Q(("title__length__gt", 0)), name="oex_catalog_courserun_title_not_blank"
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
migrations.AddConstraint(
|
|
200
|
+
model_name="courserun",
|
|
201
|
+
constraint=models.CheckConstraint(
|
|
202
|
+
condition=django.db.models.lookups.GreaterThan(
|
|
203
|
+
django.db.models.functions.text.StrIndex("course_key", models.F("run_code")), 0
|
|
204
|
+
),
|
|
205
|
+
name="oex_catalog_courserun_course_key_run_code_match_exactly",
|
|
206
|
+
violation_error_message="The CourseRun 'run_code' field should match the run in the 'course_key'.",
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CatalogCourse model
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import NewType
|
|
7
|
+
|
|
8
|
+
from django.conf import settings
|
|
9
|
+
from django.contrib import admin
|
|
10
|
+
from django.db import models
|
|
11
|
+
from django.db.models.functions import Length, Lower
|
|
12
|
+
from django.db.models.lookups import Regex
|
|
13
|
+
from django.utils.translation import gettext_lazy as _
|
|
14
|
+
from organizations.models import Organization # type: ignore[import]
|
|
15
|
+
|
|
16
|
+
from openedx_django_lib.fields import TypedBigAutoField, case_insensitive_char_field, case_sensitive_char_field
|
|
17
|
+
from openedx_django_lib.validators import validate_utc_datetime
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_default_language_code() -> str:
|
|
23
|
+
"""
|
|
24
|
+
Default language code used for CatalogCourse.language
|
|
25
|
+
|
|
26
|
+
Note: this function is used in migration 0001 so update that migration if
|
|
27
|
+
moving it or changing its signature.
|
|
28
|
+
"""
|
|
29
|
+
return settings.LANGUAGE_CODE # e.g. "en-us", "fr-ca"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Make 'length' available for CHECK constraints. OK if this is called multiple times.
|
|
33
|
+
models.CharField.register_lookup(Length)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CatalogCourse(models.Model):
|
|
37
|
+
"""
|
|
38
|
+
A **catalog course** is a set of course runs.
|
|
39
|
+
|
|
40
|
+
For example, the "Math 100" catalog course is comprised of one or more
|
|
41
|
+
"course runs" (e.g. "Math 100 2025Fall", "Math 100 2026Spring").
|
|
42
|
+
|
|
43
|
+
Note that most parts of the Open edX system only need to know/care about
|
|
44
|
+
course runs, not catalog courses, but this package nevertheless provides the
|
|
45
|
+
CatalogCourse model as a foundation for the few use cases that require it.
|
|
46
|
+
|
|
47
|
+
⚠️ `CatalogCourse`s and `CourseRun`s may exist as marketing/enrollment
|
|
48
|
+
placeholders for courses that do not yet otherwise exist (do not yet have
|
|
49
|
+
content). So in general, you should not assume that any related models /
|
|
50
|
+
content exist just because the `CatalogCourse` or `CourseRun` exists.
|
|
51
|
+
|
|
52
|
+
A `CatalogCourse` may exist without any `CourseRun`s, but a `CourseRun`
|
|
53
|
+
cannot exist without a `CatalogCourse`, and this is enforced by the
|
|
54
|
+
database.
|
|
55
|
+
|
|
56
|
+
🔍 This model is intentionally as minimal as possible. For anything else you
|
|
57
|
+
may want to do that involves new fields, please add them via a related model
|
|
58
|
+
in your own app, and make a `ForeignKey` or `OneToOneField` relationship to
|
|
59
|
+
this model. (Unless you really think it's something core that all catalog
|
|
60
|
+
courses in all instances of Open edX will need.)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
CatalogCourseID = NewType("CatalogCourseID", int)
|
|
64
|
+
type ID = CatalogCourseID
|
|
65
|
+
|
|
66
|
+
class IDField(TypedBigAutoField[ID]): # Boilerplate for fully-typed ID field.
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
id = IDField(
|
|
70
|
+
primary_key=True,
|
|
71
|
+
verbose_name=_("Primary Key"),
|
|
72
|
+
help_text=_("The internal database ID for this catalog course. Should not be exposed to users nor in APIs."),
|
|
73
|
+
editable=False,
|
|
74
|
+
)
|
|
75
|
+
org = models.ForeignKey(
|
|
76
|
+
Organization,
|
|
77
|
+
on_delete=models.PROTECT,
|
|
78
|
+
null=False,
|
|
79
|
+
# Initially, I had to_field="short_name" here, which has the nice property that we can look up an org's
|
|
80
|
+
# short_name without doing a JOIN. But that also prevents changing the org's short_name, which could be
|
|
81
|
+
# necessary to fix capitalization problems. (We wouldn't want to allow other changes to an org's short_name
|
|
82
|
+
# though; only fixing capitalization - see openedx_catalog.signals.verify_organization_change.)
|
|
83
|
+
)
|
|
84
|
+
course_code = case_sensitive_char_field(
|
|
85
|
+
max_length=255,
|
|
86
|
+
blank=False,
|
|
87
|
+
null=False,
|
|
88
|
+
help_text=_('The course code/number, e.g. "Math100".'),
|
|
89
|
+
)
|
|
90
|
+
# When this catalog course was first created. We don't track "modified" as this model should be basically immutable,
|
|
91
|
+
# and the only field that may ever change is "title"; we also don't want people to think that the "modified" time
|
|
92
|
+
# reflects when other related models/data was updated.
|
|
93
|
+
created = models.DateTimeField(
|
|
94
|
+
auto_now_add=True,
|
|
95
|
+
validators=[validate_utc_datetime],
|
|
96
|
+
editable=False,
|
|
97
|
+
)
|
|
98
|
+
title = case_insensitive_char_field(
|
|
99
|
+
max_length=255,
|
|
100
|
+
blank=False,
|
|
101
|
+
help_text=_(
|
|
102
|
+
'The full title (display name) of this catalog course. e.g. "Introduction to Calculus". '
|
|
103
|
+
'Individual course runs may override this, e.g. "Intro to Calc (Fall 2026 with Dr. Newton)".'
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
# Note: language codes used on the Open edX platform are inconsistent.
|
|
107
|
+
# See https://github.com/openedx/openedx-platform/issues/38036
|
|
108
|
+
# For this model going forward, we normalized them to match settings.LANGUAGES (en, fr-ca, zh-cn, zh-hk) but for
|
|
109
|
+
# backwards compatibility, you can get/set the language_short field which uses the mostly two-letter values from
|
|
110
|
+
# the platform's ALL_LANGUAGES setting (en, fr, es, zh_HANS, zh_HANT).
|
|
111
|
+
language = case_sensitive_char_field( # Case sensitive but constraints force it to be lowercase.
|
|
112
|
+
max_length=64,
|
|
113
|
+
blank=False,
|
|
114
|
+
null=False,
|
|
115
|
+
default=get_default_language_code,
|
|
116
|
+
help_text=_(
|
|
117
|
+
"The code representing the primary language of this catalog course's content - the language in which the "
|
|
118
|
+
"content is authored and which learners can learn without translation. (Translated versions of the course "
|
|
119
|
+
"or parts of the course may be available in other languages if supported by the platform.) "
|
|
120
|
+
"The first two digits must be the lowercase ISO 639-1 language code, optionally followed by a "
|
|
121
|
+
'country/locale code. e.g. "en", "es", "fr-ca", "pt-br", "zh-cn", "zh-hk". '
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# 🛑 Avoid adding additional fields here. The core Open edX platform doesn't do much with catalog courses, so
|
|
126
|
+
# we don't need "description", "visibility", "prereqs", or anything else. If you want to use such fields and
|
|
127
|
+
# expose them to users, you'll need to supplement this with additional data/models as mentioned in the docstring.
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def language_short(self) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get the language code used by this catalog course, without locale.
|
|
133
|
+
This is always a two-digit code, except for Mandarin and Cantonese.
|
|
134
|
+
(This should be a value from settings.ALL_LANGUAGES, and should match
|
|
135
|
+
the CourseOverview.language field.)
|
|
136
|
+
"""
|
|
137
|
+
if self.language == "zh-cn": # Chinese (Mainland China)
|
|
138
|
+
return "zh_HANS" # Mandarin / Simplified
|
|
139
|
+
elif self.language == "zh-hk": # Chinese (Hong Kong)
|
|
140
|
+
return "zh_HANT" # Cantonese / Traditional
|
|
141
|
+
return self.language[:2] # Strip locale
|
|
142
|
+
|
|
143
|
+
@language_short.setter
|
|
144
|
+
def language_short(self, legacy_code: str) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Set the language code used by this catalog course, without locale.
|
|
147
|
+
This is always a two-digit code, except for Mandarin and Cantonese.
|
|
148
|
+
(This should be a value from settings.ALL_LANGUAGES, and should match
|
|
149
|
+
the CourseOverview.language field.)
|
|
150
|
+
"""
|
|
151
|
+
if hasattr(settings, "ALL_LANGUAGES"):
|
|
152
|
+
assert legacy_code in [code for (code, _name) in settings.ALL_LANGUAGES] # type: ignore
|
|
153
|
+
if legacy_code == "zh_HANS": # Mandarin / Simplified
|
|
154
|
+
self.language = "zh-cn" # Chinese (Mainland China)
|
|
155
|
+
elif legacy_code == "zh_HANT": # Cantonese / Traditional
|
|
156
|
+
self.language = "zh-hk" # Chinese (Hong Kong)
|
|
157
|
+
else:
|
|
158
|
+
self.language = legacy_code
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
@admin.display(ordering="org__short_name")
|
|
162
|
+
def org_code(self) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Get the org code (Organization short_name) of this course, e.g. "MITx"
|
|
165
|
+
|
|
166
|
+
⚠️ This is the org's canonical short_name and may differ in case from
|
|
167
|
+
the actual org code used in the course keys of the runs of this course,
|
|
168
|
+
especially with older catalog courses.
|
|
169
|
+
"""
|
|
170
|
+
return self.org.short_name
|
|
171
|
+
|
|
172
|
+
@org_code.setter
|
|
173
|
+
def org_code(self, org_code: str) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Convenience method to set the related organization using its short_name.
|
|
176
|
+
"""
|
|
177
|
+
# We don't use the Organizations API `get_organization_by_short_name`
|
|
178
|
+
# method because it filters for only active organizations.
|
|
179
|
+
# To support historical data, backfilling, etc., we need to allow inactive orgs here.
|
|
180
|
+
self.org = Organization.objects.get(short_name__iexact=org_code)
|
|
181
|
+
# Note: on SQLite, it's possible for multiple orgs with the same short_name to exist,
|
|
182
|
+
# and the above query could raise `Organization.MultipleObjectsReturned`, but it's
|
|
183
|
+
# not possible on MySQL, using the default collation used by the Organizations app.
|
|
184
|
+
# So we do not worry about that possibility here.
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def key_str(self) -> str:
|
|
188
|
+
"""
|
|
189
|
+
A string key that can be used to identify this catalog course in URLs or
|
|
190
|
+
APIs.
|
|
191
|
+
In the future, this may be based on an editable SlugField, so don't
|
|
192
|
+
assume that it never changes. We may also make this an opaque key, in
|
|
193
|
+
which case you'd get it via `.key` and this `.key_str` would be
|
|
194
|
+
deprecated. The format returned by this function is designed to be
|
|
195
|
+
implementable as an opaque key.
|
|
196
|
+
"""
|
|
197
|
+
return f"catalog-course:{self.org_code}:{self.course_code}"
|
|
198
|
+
|
|
199
|
+
def clean(self) -> None:
|
|
200
|
+
"""Validate/normalize fields when edited via Django admin"""
|
|
201
|
+
# Set a default value for title:
|
|
202
|
+
if not self.title:
|
|
203
|
+
self.title = self.course_code
|
|
204
|
+
# Normalize language codes to match settings.LANGUAGES.
|
|
205
|
+
# It's safe to assume language is lowercase here, because if it's not the DB will reject its CHECK constraint.
|
|
206
|
+
if self.language == "zh-hans":
|
|
207
|
+
self.language = "zh-cn"
|
|
208
|
+
if self.language == "zh-hant":
|
|
209
|
+
self.language = "zh-hk"
|
|
210
|
+
|
|
211
|
+
def save(self, *args, **kwargs):
|
|
212
|
+
"""Save the model, with some defaults and validation."""
|
|
213
|
+
self.clean()
|
|
214
|
+
super().save(*args, **kwargs)
|
|
215
|
+
|
|
216
|
+
def __str__(self) -> str:
|
|
217
|
+
return f"{self.title} ({self.org_code} {self.course_code})"
|
|
218
|
+
|
|
219
|
+
class Meta:
|
|
220
|
+
verbose_name = _("Catalog Course")
|
|
221
|
+
verbose_name_plural = _("Catalog Courses")
|
|
222
|
+
ordering = ("-created",)
|
|
223
|
+
indexes = [
|
|
224
|
+
# We need fast lookups by (org, course_code) pairs. We generally want this lookup to be case sensitive.
|
|
225
|
+
models.Index(fields=["org", "course_code"]),
|
|
226
|
+
]
|
|
227
|
+
constraints = [
|
|
228
|
+
# The course_code must be case-insensitively unique per org:
|
|
229
|
+
models.UniqueConstraint("org", Lower("course_code"), name="oex_catalog_catalogcourse_org_code_uniq_ci"),
|
|
230
|
+
# Enforce at the DB level that these required fields are not blank:
|
|
231
|
+
models.CheckConstraint(
|
|
232
|
+
condition=models.Q(course_code__length__gt=0), name="oex_catalog_catalogcourse_course_code_not_blank"
|
|
233
|
+
),
|
|
234
|
+
models.CheckConstraint(
|
|
235
|
+
condition=models.Q(title__length__gt=0), name="oex_catalog_catalogcourse_title_not_blank"
|
|
236
|
+
),
|
|
237
|
+
# Language code must be lowercase, and locale codes separated by "-" (django convention) not "_"
|
|
238
|
+
models.CheckConstraint(
|
|
239
|
+
condition=Regex(models.F("language"), r"^[a-z][a-z](\-[a-z0-9]+)*$"),
|
|
240
|
+
name="oex_catalog_catalogcourse_language_regex",
|
|
241
|
+
violation_error_message=_(
|
|
242
|
+
'The language code must be lowercase, e.g. "en". If a country/locale code is provided, '
|
|
243
|
+
'it must be separated by a hyphen, e.g. "en-us", "zh-hk". '
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
]
|