Skip to content

Commit fe9c92a

Browse files
feat: add typed primary keys for core models like PublishableEntity
feat: added typed primary keys for Container feat: added typed primary keys for Component feat: added typed primary keys for PublishableEntity feat: added typed primary keys for LearningPackage feat: added typed primary keys for CatalogCourse and CourseRun feat: added typed primary keys for Units/Subsections/Sections refactor: change typed ID fields to use 'id' instead of 'pk' feat: add a default 'id' accessor to PublishableEntityMixin
1 parent 2eebc7b commit fe9c92a

39 files changed

Lines changed: 545 additions & 267 deletions

File tree

src/openedx_catalog/api_impl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ def get_catalog_course(*, org_code: str, course_code: str) -> CatalogCourse: ...
3030
@overload
3131
def get_catalog_course(*, key_str: str) -> CatalogCourse: ...
3232
@overload
33-
def get_catalog_course(*, pk: int) -> CatalogCourse: ...
33+
def get_catalog_course(*, pk: CatalogCourse.ID) -> CatalogCourse: ...
3434

3535

3636
def get_catalog_course(
37-
pk: int | None = None,
37+
pk: CatalogCourse.ID | None = None,
3838
key_str: str = "",
3939
org_code: str = "",
4040
course_code: str = "",
@@ -61,7 +61,7 @@ def get_catalog_course(
6161

6262

6363
def update_catalog_course(
64-
catalog_course: CatalogCourse | int,
64+
catalog_course: CatalogCourse | CatalogCourse.ID,
6565
*,
6666
title: str | None = None, # Specify a string to change the title (display name).
6767
# The short language code (one of settings.ALL_LANGUAGES), e.g. "en", "es", "zh_HANS"
@@ -88,7 +88,7 @@ def update_catalog_course(
8888
cc.save(update_fields=update_fields)
8989

9090

91-
def delete_catalog_course(catalog_course: CatalogCourse | int) -> None:
91+
def delete_catalog_course(catalog_course: CatalogCourse | CatalogCourse.ID) -> None:
9292
"""
9393
Delete a `CatalogCourse`. This will fail with a `ProtectedError` if any runs exist.
9494

src/openedx_catalog/models/catalog_course.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import logging
6+
from typing import NewType
67

78
from django.conf import settings
89
from django.contrib import admin
@@ -12,7 +13,7 @@
1213
from django.utils.translation import gettext_lazy as _
1314
from organizations.models import Organization # type: ignore[import]
1415

15-
from openedx_django_lib.fields import case_insensitive_char_field, case_sensitive_char_field
16+
from openedx_django_lib.fields import TypedBigAutoField, case_insensitive_char_field, case_sensitive_char_field
1617
from openedx_django_lib.validators import validate_utc_datetime
1718

1819
log = logging.getLogger(__name__)
@@ -59,7 +60,13 @@ class CatalogCourse(models.Model):
5960
courses in all instances of Open edX will need.)
6061
"""
6162

62-
id = models.BigAutoField(
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(
6370
primary_key=True,
6471
verbose_name=_("Primary Key"),
6572
help_text=_("The internal database ID for this catalog course. Should not be exposed to users nor in APIs."),

src/openedx_catalog/models/course_run.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import logging
6+
from typing import NewType
67

78
from django.contrib import admin
89
from django.core.exceptions import ValidationError
@@ -15,7 +16,7 @@
1516
from opaque_keys.edx.django.models import CourseKeyField
1617
from opaque_keys.edx.locator import CourseLocator
1718

18-
from openedx_django_lib.fields import case_insensitive_char_field, case_sensitive_char_field
19+
from openedx_django_lib.fields import TypedBigAutoField, case_insensitive_char_field, case_sensitive_char_field
1920
from openedx_django_lib.validators import validate_utc_datetime
2021

2122
from .catalog_course import CatalogCourse
@@ -79,8 +80,14 @@ class CourseRun(models.Model):
7980
learning package.
8081
"""
8182

83+
CourseRunID = NewType("CourseRunID", int)
84+
type ID = CourseRunID
85+
86+
class IDField(TypedBigAutoField[ID]): # Boilerplate for fully-typed ID field.
87+
pass
88+
8289
# Use this field for relationships within the database:
83-
id = models.BigAutoField(
90+
id = IDField(
8491
primary_key=True,
8592
verbose_name=_("Primary Key"),
8693
help_text=_("The internal database ID for this course. Should not be exposed to users nor in APIs."),

src/openedx_content/applets/backup_restore/zipper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def get_publishable_entities(self) -> QuerySet[PublishableEntity]:
176176
Retrieve the publishable entities associated with the learning package.
177177
Prefetches related data for efficiency.
178178
"""
179-
lp_id = self.learning_package.pk
179+
lp_id = self.learning_package.id
180180
publishable_entities: QuerySet[PublishableEntity] = publishing_api.get_publishable_entities(lp_id)
181181
return (
182182
publishable_entities # type: ignore[no-redef]
@@ -210,7 +210,7 @@ def get_collections(self) -> QuerySet[Collection]:
210210
Get the collections associated with the learning package.
211211
"""
212212
return (
213-
collections_api.get_collections(self.learning_package.pk)
213+
collections_api.get_collections(self.learning_package.id)
214214
.prefetch_related("entities")
215215
)
216216

@@ -512,7 +512,7 @@ def __init__(self, zipf: zipfile.ZipFile, key: str | None = None, user: UserType
512512
self.user = user
513513
self.user_id = getattr(self.user, "id", None)
514514
self.lp_key = key # If provided, use this key for the restored learning package
515-
self.learning_package_id: int | None = None # Will be set upon restoration
515+
self.learning_package_id: LearningPackage.ID | None = None # Will be set upon restoration
516516
self.utc_now: datetime = datetime.now(timezone.utc)
517517
self.component_types_cache: dict[tuple[str, str], ComponentType] = {}
518518
self.errors: list[dict[str, Any]] = []

src/openedx_content/applets/collections/api.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from ..publishing import api as publishing_api
1212
from ..publishing.models import PublishableEntity
13-
from .models import Collection, CollectionPublishableEntity
13+
from .models import Collection, CollectionPublishableEntity, LearningPackage
1414

1515
# The public API that will be re-exported by openedx_content.api
1616
# is listed in the __all__ entries below. Internal helper functions that are
@@ -33,7 +33,7 @@
3333

3434

3535
def create_collection(
36-
learning_package_id: int,
36+
learning_package_id: LearningPackage.ID,
3737
key: str,
3838
*,
3939
title: str,
@@ -55,15 +55,15 @@ def create_collection(
5555
return collection
5656

5757

58-
def get_collection(learning_package_id: int, collection_key: str) -> Collection:
58+
def get_collection(learning_package_id: LearningPackage.ID, collection_key: str) -> Collection:
5959
"""
6060
Get a Collection by ID
6161
"""
6262
return Collection.objects.get_by_key(learning_package_id, collection_key)
6363

6464

6565
def update_collection(
66-
learning_package_id: int,
66+
learning_package_id: LearningPackage.ID,
6767
key: str,
6868
*,
6969
title: str | None = None,
@@ -89,7 +89,7 @@ def update_collection(
8989

9090

9191
def delete_collection(
92-
learning_package_id: int,
92+
learning_package_id: LearningPackage.ID,
9393
key: str,
9494
*,
9595
hard_delete=False,
@@ -111,7 +111,7 @@ def delete_collection(
111111

112112

113113
def restore_collection(
114-
learning_package_id: int,
114+
learning_package_id: LearningPackage.ID,
115115
key: str,
116116
) -> Collection:
117117
"""
@@ -125,7 +125,7 @@ def restore_collection(
125125

126126

127127
def add_to_collection(
128-
learning_package_id: int,
128+
learning_package_id: LearningPackage.ID,
129129
key: str,
130130
entities_qset: QuerySet[PublishableEntity],
131131
created_by: int | None = None,
@@ -145,7 +145,7 @@ def add_to_collection(
145145
invalid_entity = entities_qset.exclude(learning_package_id=learning_package_id).first()
146146
if invalid_entity:
147147
raise ValidationError(
148-
f"Cannot add entity {invalid_entity.pk} in learning package {invalid_entity.learning_package_id} "
148+
f"Cannot add entity {invalid_entity.id} in learning package {invalid_entity.learning_package_id} "
149149
f"to collection {key} in learning package {learning_package_id}."
150150
)
151151

@@ -161,7 +161,7 @@ def add_to_collection(
161161

162162

163163
def remove_from_collection(
164-
learning_package_id: int,
164+
learning_package_id: LearningPackage.ID,
165165
key: str,
166166
entities_qset: QuerySet[PublishableEntity],
167167
) -> Collection:
@@ -183,7 +183,7 @@ def remove_from_collection(
183183
return collection
184184

185185

186-
def get_entity_collections(learning_package_id: int, entity_key: str) -> QuerySet[Collection]:
186+
def get_entity_collections(learning_package_id: LearningPackage.ID, entity_key: str) -> QuerySet[Collection]:
187187
"""
188188
Get all collections in the given learning package which contain this entity.
189189
@@ -196,7 +196,10 @@ def get_entity_collections(learning_package_id: int, entity_key: str) -> QuerySe
196196
return entity.collections.filter(enabled=True).order_by("pk")
197197

198198

199-
def get_collection_entities(learning_package_id: int, collection_key: str) -> QuerySet[PublishableEntity]:
199+
def get_collection_entities(
200+
learning_package_id: LearningPackage.ID,
201+
collection_key: str,
202+
) -> QuerySet[PublishableEntity]:
200203
"""
201204
Returns a QuerySet of PublishableEntities in a Collection.
202205
@@ -208,7 +211,7 @@ def get_collection_entities(learning_package_id: int, collection_key: str) -> Qu
208211
).order_by("pk")
209212

210213

211-
def get_collections(learning_package_id: int, enabled: bool | None = True) -> QuerySet[Collection]:
214+
def get_collections(learning_package_id: LearningPackage.ID, enabled: bool | None = True) -> QuerySet[Collection]:
212215
"""
213216
Get all collections for a given learning package
214217

src/openedx_content/applets/components/api.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from ..media import api as media_api
2727
from ..publishing import api as publishing_api
28-
from .models import Component, ComponentType, ComponentVersion, ComponentVersionMedia
28+
from .models import Component, ComponentType, ComponentVersion, ComponentVersionMedia, LearningPackage
2929

3030
# The public API that will be re-exported by openedx_content.api
3131
# is listed in the __all__ entries below. Internal helper functions that are
@@ -96,7 +96,7 @@ def get_or_create_component_type_by_entity_key(entity_key: str) -> tuple[Compone
9696

9797

9898
def create_component(
99-
learning_package_id: int,
99+
learning_package_id: LearningPackage.ID,
100100
/,
101101
component_type: ComponentType,
102102
local_key: str,
@@ -127,7 +127,7 @@ def create_component(
127127

128128

129129
def create_component_version(
130-
component_pk: int,
130+
component_id: Component.ID,
131131
/,
132132
version_num: int,
133133
title: str,
@@ -139,21 +139,21 @@ def create_component_version(
139139
"""
140140
with atomic():
141141
publishable_entity_version = publishing_api.create_publishable_entity_version(
142-
component_pk,
142+
component_id,
143143
version_num=version_num,
144144
title=title,
145145
created=created,
146146
created_by=created_by,
147147
)
148148
component_version = ComponentVersion.objects.create(
149149
publishable_entity_version=publishable_entity_version,
150-
component_id=component_pk,
150+
component_id=component_id,
151151
)
152152
return component_version
153153

154154

155155
def create_next_component_version(
156-
component_pk: int,
156+
component_id: Component.ID,
157157
/,
158158
media_to_replace: dict[str, int | None | bytes],
159159
created: datetime,
@@ -167,7 +167,7 @@ def create_next_component_version(
167167
Create a new ComponentVersion based on the most recent version.
168168
169169
Args:
170-
component_pk (int): The primary key of the Component to version.
170+
component_id (int): The primary key of the Component to version.
171171
media_to_replace (dict): Mapping of file keys to Media IDs,
172172
None (for deletion), or bytes (for new file media).
173173
created (datetime): The creation timestamp for the new version.
@@ -218,7 +218,7 @@ def create_next_component_version(
218218
# should pick up from the last edited version. Likewise, a Draft might get
219219
# reverted to an earlier version, but we want the latest version_num when
220220
# creating the next version.
221-
component = Component.objects.get(pk=component_pk)
221+
component = Component.objects.get(pk=component_id)
222222
last_version = component.versioning.latest
223223
if last_version is None:
224224
next_version_num = 1
@@ -233,15 +233,15 @@ def create_next_component_version(
233233

234234
with atomic():
235235
publishable_entity_version = publishing_api.create_publishable_entity_version(
236-
component_pk,
236+
component_id,
237237
version_num=next_version_num,
238238
title=title,
239239
created=created,
240240
created_by=created_by,
241241
)
242242
component_version = ComponentVersion.objects.create(
243243
publishable_entity_version=publishable_entity_version,
244-
component_id=component_pk,
244+
component_id=component_id,
245245
)
246246
# First copy the new stuff over...
247247
for key, media_pk_or_bytes in media_to_replace.items():
@@ -290,7 +290,7 @@ def create_next_component_version(
290290

291291

292292
def create_component_and_version( # pylint: disable=too-many-positional-arguments
293-
learning_package_id: int,
293+
learning_package_id: LearningPackage.ID,
294294
/,
295295
component_type: ComponentType,
296296
local_key: str,
@@ -313,7 +313,7 @@ def create_component_and_version( # pylint: disable=too-many-positional-argumen
313313
can_stand_alone=can_stand_alone,
314314
)
315315
component_version = create_component_version(
316-
component.pk,
316+
component.id,
317317
version_num=1,
318318
title=title,
319319
created=created,
@@ -322,17 +322,17 @@ def create_component_and_version( # pylint: disable=too-many-positional-argumen
322322
return (component, component_version)
323323

324324

325-
def get_component(component_pk: int, /) -> Component:
325+
def get_component(component_id: Component.ID, /) -> Component:
326326
"""
327327
Get Component by its primary key.
328328
329329
This is the same as the PublishableEntity's ID primary key.
330330
"""
331-
return Component.with_publishing_relations.get(pk=component_pk)
331+
return Component.with_publishing_relations.get(pk=component_id)
332332

333333

334334
def get_component_by_key(
335-
learning_package_id: int,
335+
learning_package_id: LearningPackage.ID,
336336
/,
337337
namespace: str,
338338
type_name: str,
@@ -367,7 +367,7 @@ def get_component_version_by_uuid(uuid: UUID) -> ComponentVersion:
367367

368368

369369
def component_exists_by_key(
370-
learning_package_id: int,
370+
learning_package_id: LearningPackage.ID,
371371
/,
372372
namespace: str,
373373
type_name: str,
@@ -392,7 +392,7 @@ def component_exists_by_key(
392392

393393

394394
def get_components( # pylint: disable=too-many-positional-arguments
395-
learning_package_id: int,
395+
learning_package_id: LearningPackage.ID,
396396
/,
397397
draft: bool | None = None,
398398
published: bool | None = None,
@@ -435,7 +435,7 @@ def get_components( # pylint: disable=too-many-positional-arguments
435435

436436

437437
def get_collection_components(
438-
learning_package_id: int,
438+
learning_package_id: LearningPackage.ID,
439439
collection_key: str,
440440
) -> QuerySet[Component]:
441441
"""

0 commit comments

Comments
 (0)