Skip to content

feat!: Make Timestamp, Author params consistent#566

Draft
kdmccormick wants to merge 10 commits intokdmccormick/core-1from
kdmccormick/core-1-timestamp-author
Draft

feat!: Make Timestamp, Author params consistent#566
kdmccormick wants to merge 10 commits intokdmccormick/core-1from
kdmccormick/core-1-timestamp-author

Conversation

@kdmccormick
Copy link
Copy Markdown
Member

@kdmccormick kdmccormick commented Apr 28, 2026

Description

This PR aims to unify handling of Timestamp and Author parameters across all API functions. It ensures that the default behavior is "figure out a reasonable Timestamp and Author" rather than "assume that they are None". Timestamps are forbidden for being None, and Authors can only be None if explicitly specified as such using the special AUTHOR_NONE sentinel.

BREAKING CHANGE: Various Timestamp params (named <verb>ed, <verb>ed_at) and Author params (named <verb>ed_by) have stricter rules. For example, a concrete User or explicit AUTHOR_NONE must always be specified outside of a bulk_draft_changes_forcontext. See resolve_timestamp and resolve_author functions for details. I will make this breaking change message more specific before merging.

Background

See:

Concerns

I haven't mapped it out exactly, but I believe this will add additional queries to several functions, where we need to get the active DraftChangeLogContext (for a given learning_package_id) in order to resolve TIMESTAMP_AUTO and AUTHOR_AUTO. Additionally, functions which only take an entity_id (or version_id, etc.) will need to perform an extra extra query in order to get the operation's learning_package_id.

Status

I'm just looking for feedback on the approach currently.

Done:

  • Implemented POC, just for src/openedx_content/applets/publishing.
  • mypy passes in src/openedx_content/applets/publishing.

Todo:

  • Unit tests
  • Implement it in the rest of the applets.
  • Manual tests
  • Write up the BREAKING CHANGEs in more detail
  • openedx-platform PR

AI

This code is all written by hand, but I used Claude Opus 4.7 to help me plan it. In particular, it suggested that:

Log-only draft APIs raise inside a context. set_draft_version, soft_delete_draft, and reset_drafts_to_published raise ValueError if called inside a bulk_draft_changes_for block with a non-AUTO_* author or timestamp, because the underlying DraftChangeLog records those fields at the log level (not per record). Outside a context the explicit values are honored.

which led me to add the can_override_context parameters.

@kdmccormick kdmccormick force-pushed the kdmccormick/core-1-timestamp-author branch from 3f81533 to 8b2b362 Compare April 28, 2026 15:55
@kdmccormick kdmccormick changed the title feat!: Make Timestamp and Author params consistent across Publishing … feat!: Make Timestamp and Author params consistent across Publishing app (WIP) Apr 28, 2026
@kdmccormick kdmccormick changed the title feat!: Make Timestamp and Author params consistent across Publishing app (WIP) feat!: Make Timestamp, Author params consistent across Publishing app (WIP) Apr 28, 2026
@kdmccormick kdmccormick force-pushed the kdmccormick/core-1-timestamp-author branch from 8b2b362 to 58a9ef1 Compare April 28, 2026 16:12
@kdmccormick kdmccormick requested review from bradenmacdonald and ormsbee and removed request for bradenmacdonald April 28, 2026 16:42
@kdmccormick
Copy link
Copy Markdown
Member Author

kdmccormick commented Apr 28, 2026

@ormsbee @bradenmacdonald Let me know what you think about this approach, and whether you feel confident enough in it that I should rush it in for Verawood or wait until post-1.0.

@kdmccormick kdmccormick changed the title feat!: Make Timestamp, Author params consistent across Publishing app (WIP) feat!: Make Timestamp, Author params consistent Apr 28, 2026
@kdmccormick kdmccormick force-pushed the kdmccormick/core-1-timestamp-author branch 2 times, most recently from 22d0150 to 328ce33 Compare April 28, 2026 17:27
@kdmccormick kdmccormick force-pushed the kdmccormick/core-1-timestamp-author branch from 328ce33 to c8e6187 Compare April 28, 2026 17:29

A UserID is meant to hold a primary key of a User object, but this is not
enforced by the type checker. Future versions of openedx-core may enforce
this more strictly by making UserID a NewType (subclass) of int.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, we could add a UserID type here, and/or move these types to openedx-core: https://github.com/openedx/openedx-platform/blob/master/openedx/core/types/user.py

I suspect that without modifying Django itself, aliasing UserID = int is the best we can do.

"""
Attribute the operation to the same user as the enclosing `bulk_draft_changes_for` context.

Outside of a context, using `AUTHOR_AUTO` is meaningless and will result in a `ValueError`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use djngo-crum or something to try to auto-detect the user, but I think it's better to just explicitly pass it in.

if changed_at is TIMESTAMP_AUTO:
changed_at_dt = datetime.now(tz=timezone.utc)
else:
changed_at_dt = cast(datetime, changed_at)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this cast? If changed_at is a Timestamp and it's not TIMESTAMP_AUTO then it must be a datetime already right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I couldn't get mypy to believe it.

@bradenmacdonald
Copy link
Copy Markdown
Contributor

I believe this will add additional queries to several functions, where we need to get the active DraftChangeLogContext (for a given learning_package_id) in order to resolve TIMESTAMP_AUTO and AUTHOR_AUTO

Getting the active DraftChangeLogContext doesn't use any queries, AFAIK.

Additionally, functions which only take an entity_id (or version_id, etc.) will need to perform an extra extra query in order to get the operation's learning_package_id.

Yes, that would be a an extra query.

return LearningPackage.objects.filter(package_ref=package_ref).exists()


def resolve_timestamp(
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure to add test data where datetime is None

"""


type Timestamp = datetime | Literal["TIMESTAMP_AUTO"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe DATETIME_AUTO? In Python, "timestamp" specifically means a Unix float timestamp, i.e. seconds since the epoch. There's even a timestamp() method on datetime objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants