[FC-0009] New django app for copying and pasting OLX content in Studio#31904
[FC-0009] New django app for copying and pasting OLX content in Studio#31904bradenmacdonald merged 19 commits intoopenedx:masterfrom
Conversation
|
Thanks for the pull request, @bradenmacdonald! As a core committer in this repo, you can merge this once the pull request is approved per the core committer reviewer requirements and according to the agreement with your edX Champion. |
|
@Agrendalath @ormsbee This isn't quite working yet but if you want to see how it's shaping up, feel free :) |
fb9b58e to
ba2ae09
Compare
6649939 to
2a4c118
Compare
2a4c118 to
e2184e0
Compare
e2184e0 to
0afd952
Compare
b01999f to
73ba7ab
Compare
|
@Agrendalath @ormsbee This is ready for review now. I would still like to consolidate the two similar |
Agrendalath
left a comment
There was a problem hiding this comment.
The Swagger is broken, but everything else works perfectly.
9eed2ed to
a42886d
Compare
Agrendalath
left a comment
There was a problem hiding this comment.
👍
- I tested this: tested the functionality on the devstack
- I read through the code
- I checked for accessibility issues: n/a
- Includes documentation
- I made sure any change in configuration variables is reflected in the corresponding client's
configuration-securerepository: n/a
|
|
||
| def get_source_context_title(self, obj): | ||
| return obj.get_source_context_title() | ||
| get_source_context_title.short_description = 'Source Context Title' |
There was a problem hiding this comment.
Nit: should we mark this (and 'Content') as translatable?
There was a problem hiding this comment.
I don't think it's worth translator's efforts to translate the Django admin, especially since the only use case I see here is for developers. I don't think operators would ever use this Django admin, and we may even remove it once the feature is stable.
| """ Admin config for UserClipboard """ | ||
| list_display = ('user', 'content_link', 'source_usage_key', 'get_source_context_title') | ||
| search_fields = ('user__username', 'source_usage_key', 'content__display_name') | ||
| readonly_fields = ('source_context_key', 'get_source_context_title') |
There was a problem hiding this comment.
Nit: is it intended that User, Content, and Source usage key are editable in the admin detail view?
There was a problem hiding this comment.
not really; I didn't worry too much about this. I can make everything read only before I merge this.
|
@ormsbee Can you give this a final review? |
ormsbee
left a comment
There was a problem hiding this comment.
Some immediate questions on deletion and longer term mulling over how much StagedContent has to understand.
|
|
||
|
|
||
| @shared_task(base=LoggedTask) | ||
| def delete_expired_staged_content(): |
There was a problem hiding this comment.
We've had some painful operational experiences with select_for_update, to the point where I'm pretty skittish about code using it, even where skip_locked=True.
Do we really need this at the moment? The code already disconnects the StagedContent from the UserClipboard when there's another copy. Can't we spin off a task to delete that old entry at that time? (I guess we'd need to be careful about transactions to make sure there's no weird race condition, but still...)
It looks like the current philosophy of cleanup is to centralize it at the StagedContent model, and to cascade outwards to things that build on top of it like UserClipboard. But given the differing policies that might come up between things like UserClipboard and partial import, I think it might be easier to trigger the deletion from the other direction–i.e. "disconnect your extended thing (fast operation), queue task to delete StagedContent (potentially slow)"
There was a problem hiding this comment.
OK, I'll refactor along those lines. Makes total sense.
There was a problem hiding this comment.
@ormsbee OK, I've made that change. Can you give this another look?
| class Purpose(models.TextChoices): | ||
| """ The purpose of this staged content. """ | ||
| CLIPBOARD = "clipboard", _("Clipboard") |
There was a problem hiding this comment.
Does StagedContent really need this? StagedContent is otherwise a lower-level model that UserClipboard makes use of. It seems odd for it to have this kind of knowledge.
There was a problem hiding this comment.
I know, but otherwise it's hard to tell what the StagedContent was for. UserClipboard has a OneToOne relationship to User, which means that when a user copies something new, there is no "old" UserClipboard entry. So when a user copies something then copies something else, you end up with"stranded" StagedContent entries that aren't pointed to by UserClipboard and so without this field, there is no information about their purpose. It's impossible to tell from the database if they were used for clipboard or for some other thing (partial import, template, ... ?)
Now, if I make it so that UserClipboard always immediately deletes its old StagedContent rows, that would solve the problem, but that makes copying slower unless it's done asynchronously. And if it's done asynchronously, it might fail and then you'd get "stranded" StagedContent rows with no information about what they were used for. At least, that was my thought process. Let me know if you think that's not important.
Another option is to leave the purpose field but don't make it an enum, so that it can be flexibly used without knowing all the possible apps that will use it. Or I can just delete it any rely on the fact that for now all stagedcontent is clipboard content?
There was a problem hiding this comment.
Another option is to leave the purpose field but don't make it an enum, so that it can be flexibly used without knowing all the possible apps that will use it. Or I can just delete it any rely on the fact that for now all stagedcontent is clipboard content?
I do think we should kick off the async deletion of the old StagedContent as soon as a new thing has started copying, but I take the point that deletion could always fail. I have a mild preference to just leave it a freeform field then (ditching the Enum as you suggest here). But I don't feel strongly about it.
| # The user that created and owns this staged content. Only this user can read it. | ||
| user = models.ForeignKey(User, null=False, on_delete=models.CASCADE) |
There was a problem hiding this comment.
Not something for this PR, but I'm starting to wonder if StagedContent might be better as a lower-level primitive that doesn't even necessarily have a REST API or user information–because it's not exposed directly and relies on higher level constructs for things like permissions. For instance, I can imagine things like Unit templates (e.g. "Case Study": html, video, question) that are StagedContent but don't belong to a particular user.
In that situation StagedContent isn't responsible for figuring how who owns content or how it should be used. But only storing a consistent little set of content + assets together.
ormsbee
left a comment
There was a problem hiding this comment.
Minor question and suggestion w.r.t. transaction handling.
| @@ -0,0 +1,22 @@ | |||
| """ | |||
There was a problem hiding this comment.
I missed this the first time around, but please rename this to handlers.py. I personally prefer signal_handlers.py, but it handlers.py is the common convention in edx-platform and OEP-49.
There was a problem hiding this comment.
Whoops, I don't actually want the signal handlers there anymore anyways since I'm now dispatching the cleanup for individual rows at copy time as you suggested. Fixed by deleting the whole file.
| e.g. "video" if a video is staged, or "vertical" for a unit. | ||
| """), | ||
| ) | ||
| olx = models.TextField(null=False, blank=False) |
There was a problem hiding this comment.
No action required, but just for future consideration: Can we imagine using the clipboard to do a straight copy of course assets without any specific XBlock OLX?
There was a problem hiding this comment.
Predicting future requirements is a risky business :p I can imagine it, sure, but I have no idea if we'll think it's a good idea at some point in the future or not. I can also imagine StagedContent being a mutable work area instead of a read-only staging area, but I'm also unsure if that will be a good idea or not.
| clipboard or to POST some content to the clipboard. | ||
| """ | ||
|
|
||
| @atomic |
There was a problem hiding this comment.
Is anything actually being written in this request?
There was a problem hiding this comment.
Whoops, no. Probably copy-pasta.
| }) | ||
|
|
||
|
|
||
| @view_auth_classes(is_authenticated=True) |
There was a problem hiding this comment.
You might want to decorate this to prevent Django from ever enclosing this in a view-wide implicit transaction (which could later cause concurrency issues with cleanup):
@method_decorator(transaction.non_atomic_requests, name='dispatch')There was a problem hiding this comment.
Thanks, done.
|
@ormsbee @Agrendalath Is this good to merge? |
Agrendalath
left a comment
There was a problem hiding this comment.
I didn't re-test it, but the code changes look good.
|
@bradenmacdonald 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
|
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production. |
|
EdX Release Notice: This PR has been deployed to the production environment. |
Description
This implements openedx/modular-learning#51 and openedx/modular-learning#10
It is implemented as a more generic "staged content" area, where "staged content" is any content that isn't part of a particular course or library at the moment. Staged content is not editable; you have to paste it somewhere to edit it. We anticipate that "partial import" functionality may be another use case for staged content in addition to this clipboard functionality.
It's currently implemented as a django app plugin within edx-platform because it's highly coupled to platform internals and we want it tested as part of the test suite. However, if preferable we could move it to an external repo.
I am hoping to use #31903 to enforce that no other code imports the private API of this new app.
Supporting information
See GitHub issue linked above.
Analytics
When a user copies something to their clipboard, the CMS will log something like this:
TODOs:
Testing instructions
contentstore.enable_copy_paste_featureand enable it for Everyoneolx_urlthat you can go to in turn to download the OLX as an .xml file.How to test asynchronous copying
Copying is currently done synchronously, but the backend and frontend code do already support long-running asynchronous copy operations. To simulate this for testing:
Click to view instructions
First, make the following edit:
Then copy an XBlock. The copying process will continue until you retrieve the OLX. To retrieve the OLX, open the Network inspector and in one of the recent "clipboard/" responses, you'll see the URL - go to the highlighted URL:
You will see that the "Copying..." toast stays until you access that URL, which is how we signal the completion of our (faked) async operation.
Deadline
None
Other information
Private ref - MNG-3572