Emit events when a learning package is modified [FC-0117]#543
Emit events when a learning package is modified [FC-0117]#543bradenmacdonald wants to merge 34 commits intomainfrom
Conversation
|
Thanks for the pull request, @bradenmacdonald! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
b92d224 to
67cb958
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19b76fa to
4aa66f5
Compare
|
|
||
| old_version: int | None | ||
| """ | ||
| The old version_num of this entity (not the PublishableEntityVersion ID). |
There was a problem hiding this comment.
Working on this PR has made me want to add fully-typed IDs for PublishableEntityVersion and some kind of VersionNum type, because we do mix and match those a bit in the code, but they're very different things.
(I made PublishableEntity.id fully-typed but hadn't yet done ___Version)
| assert event.signal is api.signals.LEARNING_PACKAGE_ENTITIES_CHANGED | ||
| assert event.kwargs["learning_package"].id == learning_package.id | ||
| assert event.kwargs["learning_package"].title == "Test LP 📦" | ||
| assert event.kwargs["changed_by"].user_id is None | ||
| assert event.kwargs["change_log"].draft_change_log_id == expected_draft_change_log_id | ||
| assert event.kwargs["change_log"].changes == [ | ||
| api.signals.ChangeLogRecord(entity_id=entity.id, old_version=None, new_version=NEW_VERSION_NUM), | ||
| ] | ||
| assert event.kwargs["metadata"].time == now_time |
There was a problem hiding this comment.
Unfortunately, openedx-events doesn't yet support typing so event.kwargs is just a generic dict.
4aa66f5 to
296b74c
Compare
716354e to
88f3791
Compare
5b7e2ab to
eb7b160
Compare
b5f54fa to
7b62c3e
Compare
Co-Authored-By: Claude <noreply@anthropic.com>
6ea8837 to
d3537e4
Compare
| entities_removed: list[PublishableEntity.ID] | None = None, | ||
| user_id: int | None = None, | ||
| ) -> None: | ||
| """Helper for emitting the event when a collection has changed.""" |
There was a problem hiding this comment.
This helper would not be necessary if openedx/openedx-events#570 is accepted, and then we could just use that.
| "learning_package": LearningPackageEventData, | ||
| "changed_by": UserAttributionEventData, | ||
| "change": CollectionChangeData, |
There was a problem hiding this comment.
I am using multiple x: XData values in the data dict per event. I think this is a much cleaner and DRYer pattern than forcing all events to have a single key-value pair in their data dictionaries, but most (all?) event definitions in openedx-events have only a single key-value pair in the dict even when it doesn't make sense, and I don't know why.
Edit: based on the additional info I posted there, it should be completely fine. But any automatic parsing of the signal documentation may have incomplete type info (or rather, slightly more incomplete than it already is, since it already lacks the dict key info for all events).
ormsbee
left a comment
There was a problem hiding this comment.
Trying to do this in pieces to give a quicker feedback cycle. This is for Collections.
ormsbee
left a comment
There was a problem hiding this comment.
Minor comments on the rest of the app code. I don't consider any of these blocking. I'm going to look through the rest of the tests now.
| collection_code="col1", | ||
| title="Collection 1", | ||
| created_by=None, | ||
| enabled=False, |
There was a problem hiding this comment.
Nit: So technically, this means that you can get a deletion event for a collection that never emitted a create event, right?
There was a problem hiding this comment.
Hmm, I should add a test case for that but technically no, having enabled=False should be the same as never being created at all as far as events are concerned, and then actually deleting it should not emit any further events. But enabling it will emit a CREATED event and then re-disabling it ("soft deleting") should emit a deleted event. Let me know if you disagree.
There was a problem hiding this comment.
Added a fix+test for that edge case: 5ff0496
| """ | ||
|
|
||
|
|
||
| LEARNING_PACKAGE_DELETED = OpenEdxPublicSignal( |
There was a problem hiding this comment.
@bmtcril, @mariajgrimaldi, @robrap: We're declaring an OpenEdxPublicSignal outside of openedx-events here. A couple of questions:
- Is this going to have any weird consequences w.r.t. the message bus? Should we be making it a simple Django signal for now?
- @bradenmacdonald is introducing a Sphinx doc convention of doing docstring-style comments for module variables. Will this cause any problems? Should we also be putting the normal annotations above the signal? (I'm not even clear if the normal annotations for signals are picked up if they're defined outside of openedx-events.)
| by ID — they only get the ``id`` and ``title`` that are captured in the | ||
| ``LearningPackageEventData`` payload. | ||
|
|
||
| 🗑️ Unlike other ``publishing`` events, the effects of this deletion are |
There was a problem hiding this comment.
I'm guessing that we'll eventually want a pre-delete hook as well, but that's definitely puntable.
| """ | ||
|
|
||
|
|
||
| LEARNING_PACKAGE_ENTITIES_CHANGED = OpenEdxPublicSignal( |
There was a problem hiding this comment.
Nit: Is it worth calling out in the name that it's more specifically when the Entities' draft state changes?
There was a problem hiding this comment.
Yeah, you're right - the more explicit the better. Some of the other openedx-events have quite ambiguous names and documentation and that's been a pain.
Do you like LEARNING_PACKAGE_ENTITIES_DRAFTS_CHANGED? Or since that's getting rather long we could consider ENTITIES_DRAFTS_CHANGED. (Or _DRAFT_CHANGED singular?)
There was a problem hiding this comment.
I have a mild preference for ENTITIES_DRAFT_CHANGED with a corresponding ENTITIES_PUBLISHED.
There was a problem hiding this comment.
Or maybe DRAFT_ENTITIES_CHANGED and ENTITIES_PUBLISHED ? I think that reads better.
There was a problem hiding this comment.
I'd prefer ENTITIES_DRAFT_CHANGED to keep it closer to DraftChangeLog and PublishLog.
There was a problem hiding this comment.
Signals renamed in 26683ff:
ENTITIES_DRAFT_CHANGEDENTITIES_PUBLISHEDCOLLECTION_CHANGED
| # pylint: disable=wildcard-import,unused-import | ||
|
|
||
| # Signals are kept in a separate namespace: | ||
| from . import api_signals as signals |
There was a problem hiding this comment.
Nit: Why do this import? The signal receiver import happens in apps.py anyway, no?
There was a problem hiding this comment.
This is for the public API, e.g. in my platform PR:
from openedx_content.api import signals as content_signals
...
@receiver(content_signals.LEARNING_PACKAGE_ENTITIES_CHANGED)
def entities_updated(...):
...or even in this PR:
There was a problem hiding this comment.
Oh, I see. I thought it would be expected that clients import api_signals. But if they're expected to import api.signals, it seems like the aggregating thing should be just signals.py to match what we do with models.py and admin.py, and then api.py should re-export that?
There was a problem hiding this comment.
I already find it annoying to almost always need a second import of api_models in addition to api everywhere we use these APIs, so I do prefer including signals in the api instead of a separate api_signals. But if you think there's a good argument for not always loading them and having them in a separate import, let me know.
I definitely don't mind renaming api_signals to signals; I just wanted to signal that things in it were public, but maybe that's obvious if you look at it.
There was a problem hiding this comment.
Yeah, I think it's fine to:
- Do the aggregation in
signals.pyand then; - Import
signals.pyintoapi.pywith the expectation that all external clients will import fromapi.signals
I guess that if we ever do have internal signals for some reason, we can tweak things with an intermediate module later before we re-export.
There was a problem hiding this comment.
Updated api_signals -> signals with a lot more explanation in e42144b
ormsbee
left a comment
There was a problem hiding this comment.
No blockers. I would prefer the signal name be changed and the signal aggregation be done in signals.py instead of api_signals.py before being re-exported, but I have no additional concerns. Thank you.
|
@bradenmacdonald I won't review this unless you'd like me to. |
Implements #462 .
This updates openedx-code to emit the following events:
1.
LEARNING_PACKAGE_ENTITIES_CHANGEDopenedx-core/src/openedx_content/applets/publishing/signals.py
Lines 90 to 95 in 7ad1e79
2.
LEARNING_PACKAGE_ENTITIES_PUBLISHEDopenedx-core/src/openedx_content/applets/publishing/signals.py
Lines 125 to 129 in 7ad1e79
3.
LEARNING_PACKAGE_COLLECTION_CHANGEDopenedx-core/src/openedx_content/applets/collections/signals.py
Lines 48 to 49 in 7ad1e79
LEARNING_PACKAGE_CREATED/LEARNING_PACKAGE_UPDATED/LEARNING_PACKAGE_DELETED. Not sure we have much use case for these yet, but they seem like important low-level hooks.TODOs
Test that the performance of event handlers responding to
set_collectionsis OK in practice and change it to async if needed.Notes
Tagging events are much more complicated so will be in their own PR. The proposed spec is at #557
While working on this, I also opened openedx/openedx-events#570 which would make working with these kind of events easier in the future, and let us slightly simplify this code in a few places.
These events are defined locally in
openedx_contentfor now but we could move them to openedx-events in the future?Platform PR
openedx/openedx-platform#38437
The platform PR is approx -500 lines of non-test code, and +265 lines of test code, and I think it's much cleaner, so I'm really happy with the downstream effects of this PR. Part of this is being able to take advantage of the "dependencies" / "side effect" tracking properly, which the original library events weren't able to do at the time.