21
21
22
22
from .data import (
23
23
CLIPBOARD_PURPOSE ,
24
- StagedContentData , StagedContentFileData , StagedContentStatus , UserClipboardData
24
+ LIBRARY_SYNC_PURPOSE ,
25
+ StagedContentData , StagedContentFileData , StagedContentStatus , UserClipboardData , UserLibrarySyncData ,
25
26
)
26
27
from .models import (
27
28
UserClipboard as _UserClipboard ,
28
29
StagedContent as _StagedContent ,
29
30
StagedContentFile as _StagedContentFile ,
31
+ UserLibrarySync as _UserLibrarySync ,
32
+ )
33
+ from .serializers import (
34
+ UserClipboardSerializer as _UserClipboardSerializer ,
35
+ UserLibrarySyncSerializer as _UserLibrarySyncSerializer ,
30
36
)
31
- from .serializers import UserClipboardSerializer as _UserClipboardSerializer
32
37
from .tasks import delete_expired_clipboards
33
38
34
39
log = logging .getLogger (__name__ )
35
40
36
41
37
- def save_xblock_to_user_clipboard (block : XBlock , user_id : int , version_num : int | None = None ) -> UserClipboardData :
42
+ def _save_xblock_to_staged_content (
43
+ block : XBlock ,
44
+ user_id : int ,
45
+ purpose : str ,
46
+ version_num : int | None = None
47
+ ) -> _StagedContent :
38
48
"""
39
- Copy an XBlock's OLX to the user's clipboard.
49
+ Generic function to save an XBlock's OLX to staged content.
50
+ Used by both clipboard and library sync functionality.
40
51
"""
41
52
block_data = XBlockSerializer (
42
53
block ,
@@ -49,7 +60,7 @@ def save_xblock_to_user_clipboard(block: XBlock, user_id: int, version_num: int
49
60
# Mark all of the user's existing StagedContent rows as EXPIRED
50
61
to_expire = _StagedContent .objects .filter (
51
62
user_id = user_id ,
52
- purpose = CLIPBOARD_PURPOSE ,
63
+ purpose = purpose ,
53
64
).exclude (
54
65
status = StagedContentStatus .EXPIRED ,
55
66
)
@@ -60,7 +71,7 @@ def save_xblock_to_user_clipboard(block: XBlock, user_id: int, version_num: int
60
71
# Insert a new StagedContent row for this
61
72
staged_content = _StagedContent .objects .create (
62
73
user_id = user_id ,
63
- purpose = CLIPBOARD_PURPOSE ,
74
+ purpose = purpose ,
64
75
status = StagedContentStatus .READY ,
65
76
block_type = usage_key .block_type ,
66
77
olx = block_data .olx_str ,
@@ -69,38 +80,32 @@ def save_xblock_to_user_clipboard(block: XBlock, user_id: int, version_num: int
69
80
tags = block_data .tags or {},
70
81
version_num = (version_num or 0 ),
71
82
)
72
- (clipboard , _created ) = _UserClipboard .objects .update_or_create (user_id = user_id , defaults = {
73
- "content" : staged_content ,
74
- "source_usage_key" : usage_key ,
75
- })
76
83
77
84
# Log an event so we can analyze how this feature is used:
78
- log .info (f"Copied { usage_key .block_type } component \" { usage_key } \" to their clipboard ." )
85
+ log .info (f"Saved { usage_key .block_type } component \" { usage_key } \" to staged content for { purpose } ." )
79
86
80
- # Try to copy the static files. If this fails, we still consider the overall copy attempt to have succeeded,
81
- # because intra-course pasting will still work fine, and in any case users can manually resolve the file issue .
87
+ # Try to copy the static files. If this fails, we still consider the overall save attempt to have succeeded,
88
+ # because intra-course operations will still work fine, and users can manually resolve file issues .
82
89
try :
83
- _save_static_assets_to_user_clipboard (block_data .static_files , usage_key , staged_content )
90
+ _save_static_assets_to_staged_content (block_data .static_files , usage_key , staged_content )
84
91
except Exception : # pylint: disable=broad-except
85
- # Regardless of what happened, with get_asset_key_from_path or contentstore or run_filter, we don't want the
86
- # whole "copy to clipboard" operation to fail, which would be a bad user experience. For copying and pasting
87
- # within a single course, static assets don't even matter. So any such errors become warnings here.
88
- log .exception (f"Unable to copy static files to clipboard for component { usage_key } " )
92
+ log .exception (f"Unable to copy static files to staged content for component { usage_key } " )
89
93
90
94
# Enqueue a (potentially slow) task to delete the old staged content
91
95
try :
92
96
delete_expired_clipboards .delay (expired_ids )
93
97
except Exception : # pylint: disable=broad-except
94
98
log .exception (f"Unable to enqueue cleanup task for StagedContents: { ',' .join (str (x ) for x in expired_ids )} " )
95
99
96
- return _user_clipboard_model_to_data ( clipboard )
100
+ return staged_content
97
101
98
102
99
- def _save_static_assets_to_user_clipboard (
103
+ def _save_static_assets_to_staged_content (
100
104
static_files : list [StaticFile ], usage_key : UsageKey , staged_content : _StagedContent
101
105
):
102
106
"""
103
- Helper method for save_xblock_to_user_clipboard endpoint. This deals with copying static files into the clipboard.
107
+ Helper method for saving static files into staged content.
108
+ Used by both clipboard and library sync functionality.
104
109
"""
105
110
for f in static_files :
106
111
source_key = (
@@ -144,6 +149,46 @@ def _save_static_assets_to_user_clipboard(
144
149
log .exception (f"Unable to copy static file { f .name } to clipboard for component { usage_key } " )
145
150
146
151
152
+ def save_xblock_to_user_clipboard (block : XBlock , user_id : int , version_num : int | None = None ) -> UserClipboardData :
153
+ """
154
+ Copy an XBlock's OLX to the user's clipboard.
155
+ """
156
+ staged_content = _save_xblock_to_staged_content (block , user_id , CLIPBOARD_PURPOSE , version_num )
157
+ usage_key = block .usage_key
158
+
159
+ # Create/update the clipboard entry
160
+ (clipboard , _created ) = _UserClipboard .objects .update_or_create (
161
+ user_id = user_id ,
162
+ defaults = {
163
+ "content" : staged_content ,
164
+ "source_usage_key" : usage_key ,
165
+ }
166
+ )
167
+
168
+ return _user_clipboard_model_to_data (clipboard )
169
+
170
+ def save_xblock_to_user_library_sync (
171
+ block : XBlock ,
172
+ user_id : int ,
173
+ version_num : int | None = None
174
+ ) -> UserLibrarySyncData :
175
+ """
176
+ Save an XBlock's OLX for library sync.
177
+ """
178
+ staged_content = _save_xblock_to_staged_content (block , user_id , LIBRARY_SYNC_PURPOSE , version_num )
179
+ usage_key = block .usage_key
180
+
181
+ # Create/update the library sync entry
182
+ (sync , _created ) = _UserLibrarySync .objects .update_or_create (
183
+ user_id = user_id ,
184
+ defaults = {
185
+ "content" : staged_content ,
186
+ "source_usage_key" : usage_key ,
187
+ }
188
+ )
189
+
190
+ return _user_library_sync_model_to_data (sync )
191
+
147
192
def get_user_clipboard (user_id : int , only_ready : bool = True ) -> UserClipboardData | None :
148
193
"""
149
194
Get the details of the user's clipboard.
@@ -190,32 +235,72 @@ def get_user_clipboard_json(user_id: int, request: HttpRequest | None = None):
190
235
return serializer .data
191
236
192
237
238
+ def get_user_library_sync_json (user_id : int , request : HttpRequest | None = None ):
239
+ """
240
+ Get the detailed status of the user's library sync, in exactly the same format
241
+ as returned from the
242
+ /api/content-staging/v1/library-sync/
243
+ REST API endpoint. This version of the API is meant for "preloading" that
244
+ REST API endpoint so it can be embedded in a larger response sent to the
245
+ user's browser.
246
+
247
+ (request is optional; including it will make the "olx_url" absolute instead
248
+ of relative.)
249
+ """
250
+ try :
251
+ sync = _UserLibrarySync .objects .get (user_id = user_id )
252
+ except _UserLibrarySync .DoesNotExist :
253
+ # This user does not have any library sync content.
254
+ return {
255
+ "content" : None ,
256
+ "source_usage_key" : "" ,
257
+ "source_context_title" : "" ,
258
+ "source_edit_url" : ""
259
+ }
260
+
261
+ serializer = _UserLibrarySyncSerializer (
262
+ _user_library_sync_model_to_data (sync ),
263
+ context = {'request' : request },
264
+ )
265
+ return serializer .data
266
+
267
+
268
+ def _staged_content_to_data (content : _StagedContent ) -> StagedContentData :
269
+ """
270
+ Convert a StagedContent model instance to an immutable data object.
271
+ """
272
+ return StagedContentData (
273
+ id = content .id ,
274
+ user_id = content .user_id ,
275
+ created = content .created ,
276
+ purpose = content .purpose ,
277
+ status = content .status ,
278
+ block_type = content .block_type ,
279
+ display_name = content .display_name ,
280
+ tags = content .tags or {},
281
+ version_num = content .version_num ,
282
+ )
283
+
193
284
def _user_clipboard_model_to_data (clipboard : _UserClipboard ) -> UserClipboardData :
194
285
"""
195
286
Convert a UserClipboard model instance to an immutable data object.
196
287
"""
197
- content = clipboard .content
198
- source_context_key = clipboard .source_usage_key .context_key
199
- if source_context_key .is_course and (course_overview := get_course_overview_or_none (source_context_key )):
200
- source_context_title = course_overview .display_name_with_default
201
- else :
202
- source_context_title = str (source_context_key ) # Fall back to stringified context key as a title
203
288
return UserClipboardData (
204
- content = StagedContentData (
205
- id = content .id ,
206
- user_id = content .user_id ,
207
- created = content .created ,
208
- purpose = content .purpose ,
209
- status = content .status ,
210
- block_type = content .block_type ,
211
- display_name = content .display_name ,
212
- tags = content .tags or {},
213
- version_num = content .version_num ,
214
- ),
289
+ content = _staged_content_to_data (clipboard .content ),
215
290
source_usage_key = clipboard .source_usage_key ,
216
291
source_context_title = clipboard .get_source_context_title (),
217
292
)
218
293
294
+ def _user_library_sync_model_to_data (sync : _UserLibrarySync ) -> UserLibrarySyncData :
295
+ """
296
+ Convert a UserLibrarySync model instance to an immutable data object.
297
+ """
298
+ return UserLibrarySyncData (
299
+ content = _staged_content_to_data (sync .content ),
300
+ source_usage_key = sync .source_usage_key ,
301
+ source_context_title = sync .get_source_context_title (),
302
+ )
303
+
219
304
220
305
def get_staged_content_olx (staged_content_id : int ) -> str | None :
221
306
"""
0 commit comments