Skip to content

Commit c1307ef

Browse files
committed
fix: Findings from Claude's audit
1 parent 80d249a commit c1307ef

6 files changed

Lines changed: 44 additions & 42 deletions

File tree

src/openedx_content/applets/backup_restore/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ..publishing.api import get_learning_package_by_ref
99
from .zipper import LearningPackageUnzipper, LearningPackageZipper
1010

11+
1112
# The public API that will be re-exported by openedx_content.api
1213
# is listed in the __all__ entries below. Internal helper functions that are
1314
# private to this module should start with an underscore. If a function does not

src/openedx_content/applets/backup_restore/zipper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ def _save(
780780
self._save_subsections(learning_package_obj, containers)
781781
self._save_sections(learning_package_obj, containers)
782782
self._save_collections(learning_package_obj, collections)
783-
publishing_api.publish_all_drafts(learning_package_obj.id)
783+
publishing_api.publish_all_drafts(learning_package_obj.id, self.user)
784784

785785
with publishing_api.draft_changes_for(learning_package_obj.id, self.user):
786786
self._save_draft_versions(components, containers, component_static_files)

src/openedx_content/applets/components/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ def create_component(
9090
The ``entity_ref`` is conventionally derived as
9191
``"{namespace}:{type_name}:{component_code}"``, although callers should not assume
9292
that this will always be true.
93+
94+
You must specify `created_by=` unless you're inside a `draft_changes_for` context.
9395
"""
9496
entity_ref = f"{component_type.namespace}:{component_type.name}:{component_code}"
9597
with atomic():

src/openedx_content/applets/containers/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def _create_container_version(
246246
entity_list: EntityList,
247247
) -> ContainerVersion:
248248
"""
249-
Private internal method for logic shared juby create_container_version() and
249+
Private internal method for logic shared by create_container_version() and
250250
create_next_container_version().
251251
"""
252252
# validate entity_list using the type implementation:

src/openedx_content/applets/publishing/api.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"DATETIME_AUTO",
4949
"DatetimeOrAuto",
5050
"UserID",
51+
"Author",
5152
"AUTHOR_AUTO",
5253
"AuthorOrAuto",
5354
"get_learning_package",
@@ -120,25 +121,26 @@
120121
"""
121122

122123

123-
type AuthorOrAuto = (
124-
# Attribute to a specific user:
125-
UserID | AbstractUser
126-
# Attribue to nobody:
127-
| None | AnonymousUser
128-
# Attribute to the same user as the enclosing draft_changes_for:
129-
| Literal["AUTHOR_AUTO"]
130-
)
124+
type Author = UserID | AbstractUser | AnonymousUser | None
131125
"""
132-
How to attribute an operation (creation, edit, deletion, change) to a user.
126+
A user attribution for a content operation (creation, edit, deletion, change).
133127
134128
For attributable changes, this could be a User, or its database ID.
135129
136130
For changes without any attributable author (e.g. backfills), this could be None
137131
or an AnonymousUser, both of which map to NULL in the database. This should be
138132
reserved for special cases--most changes have a concrete author!
133+
"""
134+
135+
136+
type AuthorOrAuto = Author | Literal["AUTHOR_AUTO"]
137+
"""
138+
Either an Author (which could be None) or AUTHOR_AUTO.
139139
140-
Finally, this could be AUTHOR_AUTO, which means "use the same author (or lack
141-
thereof) that the draft change context is using." Most functions default to AUTHOR_AUTO.
140+
Most functions default to AUTHOR_AUTO. Please note that `None` is not an
141+
appopriate default author--only certain special operations are author-less;
142+
by default, users should supply an author user or use AUTHOR_AUTO in order to
143+
inherit the context's author user.
142144
"""
143145

144146

@@ -147,7 +149,7 @@ def resolve_datetime(
147149
dt: DatetimeOrAuto,
148150
) -> datetime:
149151
"""
150-
Convert a Timestamp specifier into a concerete datetime to be saved to the DB.
152+
Convert a Timestamp specifier into a concrete datetime to be saved to the DB.
151153
152154
If `timestamp is DATETIME_AUTO`, then return either the datetime of the
153155
enclosing `draft_changes_for` context, or `datetime.now` if there is
@@ -168,7 +170,7 @@ def resolve_author(
168170
author: AuthorOrAuto,
169171
) -> UserID | None:
170172
"""
171-
Convert a Author specifier into a concerete user ID (or None) to be saved to the DB.
173+
Convert a Author specifier into a concrete user ID (or None) to be saved to the DB.
172174
173175
If `author is AUTHOR_AUTO`, then return either the author of the enclosing
174176
`draft_changes_for` context. Raises `ValueError` if there is no
@@ -184,7 +186,14 @@ def resolve_author(
184186
"An author (other than AUTHOR_AUTO) must be specified when changing content "
185187
"outside of a `draft_changes_for` context."
186188
)
187-
elif isinstance(author, AnonymousUser):
189+
return _normalize_author_to_user_id(author) # type: ignore[arg-type]
190+
191+
192+
def _normalize_author_to_user_id(author: Author) -> UserID | None:
193+
"""
194+
Given a user (object or ID) or lack thereof (AnonymousUser or None), return the ID or None.
195+
"""
196+
if isinstance(author, AnonymousUser):
188197
return None
189198
elif isinstance(author, AbstractUser):
190199
assert isinstance(author.pk, int)
@@ -318,7 +327,7 @@ def create_publishable_entity(
318327
You'd typically want to call this right before creating your own content
319328
model that points to it.
320329
321-
Must be called inside `with draft_changes_for(...):`
330+
You must specify `created_by=` unless you're inside a `draft_changes_for` context.
322331
"""
323332

324333
return PublishableEntity.objects.create(
@@ -520,9 +529,10 @@ def get_entities_with_unpublished_deletes(learning_package_id: LearningPackage.I
520529
def publish_all_drafts(
521530
learning_package_id: LearningPackage.ID,
522531
/,
532+
published_by: Author,
533+
*,
523534
message="",
524535
published_at: DatetimeOrAuto = DATETIME_AUTO,
525-
published_by: AuthorOrAuto = AUTHOR_AUTO,
526536
) -> PublishLog:
527537
"""
528538
Publish everything that is a Draft and is not already published.
@@ -533,7 +543,11 @@ def publish_all_drafts(
533543
.with_unpublished_changes()
534544
)
535545
return publish_from_drafts(
536-
learning_package_id, draft_qset, message, published_at, published_by
546+
learning_package_id,
547+
draft_qset,
548+
published_by=published_by,
549+
message=message,
550+
published_at=published_at,
537551
)
538552

539553

@@ -574,10 +588,10 @@ def publish_from_drafts(
574588
learning_package_id: LearningPackage.ID,
575589
/,
576590
draft_qset: QuerySet[Draft],
591+
published_by: Author,
592+
*,
577593
message: str = "",
578594
published_at: DatetimeOrAuto = DATETIME_AUTO,
579-
published_by: AuthorOrAuto = AUTHOR_AUTO,
580-
*,
581595
publish_dependencies: bool = True,
582596
) -> PublishLog:
583597
"""
@@ -589,7 +603,7 @@ def publish_from_drafts(
589603
if DraftChangeLogContext.get_active_draft_change_log(learning_package_id) is not None:
590604
raise ValidationError("Cannot publish while in draft_changes_for().")
591605
published_at = resolve_datetime(learning_package_id, published_at)
592-
published_by = resolve_author(learning_package_id, published_by)
606+
published_by = _normalize_author_to_user_id(published_by)
593607
with atomic():
594608
if publish_dependencies:
595609
dependency_drafts_qsets = _get_dependencies_with_unpublished_changes(draft_qset)
@@ -1015,13 +1029,6 @@ def set_draft_version(
10151029
Calling this function attaches a new DraftChangeLogRecord and attaches it to
10161030
a DraftChangeLog.
10171031
1018-
This function will create DraftSideEffect entries and properly add any
1019-
containers that may have been affected by this draft update, UNLESS it is
1020-
called from within a draft_changes_for block. If it is called from
1021-
inside a draft_changes_for block, it will not add side-effects for
1022-
containers, as draft_changes_for will automatically do that when the
1023-
block exits. @@TODO update this docstring
1024-
10251032
Must be called inside `with draft_changes_for(...):`
10261033
"""
10271034
with atomic(savepoint=False):
@@ -1797,7 +1804,7 @@ def get_published_version_as_of(
17971804

17981805
def draft_changes_for(
17991806
learning_package_id: LearningPackage.ID,
1800-
changed_by: UserID | AbstractUser | AnonymousUser | None,
1807+
changed_by: Author,
18011808
changed_at: DatetimeOrAuto = DATETIME_AUTO,
18021809
) -> DraftChangeLogContext:
18031810
"""
@@ -1824,15 +1831,7 @@ def draft_changes_for(
18241831
else:
18251832
assert isinstance(changed_at, datetime)
18261833
changed_at_dt = changed_at
1827-
changed_by_id: UserID | None
1828-
if isinstance(changed_by, AnonymousUser):
1829-
changed_by_id = None
1830-
elif isinstance(changed_by, AbstractUser):
1831-
assert isinstance(changed_by.pk, int)
1832-
changed_by_id = changed_by.pk
1833-
elif changed_by:
1834-
assert isinstance(changed_by, int)
1835-
changed_by_id = changed_by
1834+
changed_by_id = _normalize_author_to_user_id(changed_by)
18361835
return DraftChangeLogContext(
18371836
learning_package_id,
18381837
changed_at=changed_at_dt,

tests/openedx_content/applets/publishing/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def inner_reset_drafts_to_published(self, bulk: bool) -> None:
420420

421421
def test_get_entities_with_unpublished_changes(self) -> None:
422422
"""Test fetching entities with unpublished changes after soft deletes."""
423-
with publishing_api.draft_changes_for(learning_package_id, None):
423+
with publishing_api.draft_changes_for(learning_package.id, None):
424424
entity = publishing_api.create_publishable_entity(
425425
self.learning_package_1.id,
426426
"my_entity",
@@ -461,7 +461,7 @@ def test_filter_publishable_entities(self) -> None:
461461
count_drafts = 6
462462
count_no_drafts = 3
463463

464-
with publishing_api.draft_changes_for(learning_package_id, None):
464+
with publishing_api.draft_changes_for(self.learning_package_1.id, None):
465465
for index in range(count_published):
466466
# Create entities to publish
467467
entity = publishing_api.create_publishable_entity(
@@ -476,7 +476,7 @@ def test_filter_publishable_entities(self) -> None:
476476

477477
publishing_api.publish_all_drafts(self.learning_package_1.id)
478478

479-
with publishing_api.draft_changes_for(learning_package_id, None):
479+
with publishing_api.draft_changes_for(self.learning_package_1.id, None):
480480
for index in range(count_drafts):
481481
# Create entities with drafts
482482
entity = publishing_api.create_publishable_entity(

0 commit comments

Comments
 (0)