@@ -146,16 +146,47 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
146
146
If link exists, is supported, and is followable, returns UpstreamLink.
147
147
Otherwise, raises an UpstreamLinkException.
148
148
"""
149
- if not downstream .upstream :
149
+ if downstream .upstream :
150
+ if not isinstance (downstream .usage_key .context_key , CourseKey ):
151
+ raise BadDownstream (_ ("Cannot update content because it does not belong to a course." ))
152
+ if downstream .has_children :
153
+ raise BadDownstream (_ ("Updating content with children is not yet supported." ))
154
+
155
+ # We need to determine the usage key of this block's upstream.
156
+ upstream_key : LibraryUsageLocatorV2
157
+ version_synced : int | None
158
+ version_available : int | None
159
+ # A few different scenarios...
160
+
161
+ # Do we have an upstream explicitly defined on the block? If so, use that.
162
+ if downstream .upstream :
163
+ try :
164
+ upstream_key = LibraryUsageLocatorV2 .from_string (downstream .upstream )
165
+ except InvalidKeyError as exc :
166
+ raise BadUpstream (_ ("Reference to linked library item is malformed" )) from exc
167
+ version_synced = downstream .upstream_version
168
+ version_declined = downstream .upstream_version_declined
169
+
170
+ # Otherwise, is this the child of a LegacyLibraryContentBlock?
171
+ # If so, then we know that this block was derived from block in a legacy (v1) content library.
172
+ # Try to get that block's migrated (v2) content library equivalent and use it as our upstream.
173
+ elif downstream .parent and downstream .parent .block_type == "library_content" :
174
+ from xmodule .library_content_block import LegacyLibraryContentBlock
175
+ parent : LegacyLibraryContentBlock = downstream .get_parent ()
176
+ # Next line will raise UpstreamLinkException if no matching V2 library block.
177
+ upstream_key = parent .get_migrated_upstream_for_child (downstream .usage_key .block_id )
178
+ # If we are here, then there is indeed a migrated V2 library block, but we have not yet synced from it
179
+ # (otherwise `.upstream` would have been explicitly set). So, it is fair to set the version information
180
+ # to "None". That way, as soon as an updated version of the migrated upstream is published, it will be
181
+ # available to the course author.
182
+ version_synced = None
183
+ version_declined = None
184
+
185
+ # Otherwise, we don't have an upstream. Raise.
186
+ else :
150
187
raise NoUpstream ()
151
- if not isinstance (downstream .usage_key .context_key , CourseKey ):
152
- raise BadDownstream (_ ("Cannot update content because it does not belong to a course." ))
153
- if downstream .has_children :
154
- raise BadDownstream (_ ("Updating content with children is not yet supported." ))
155
- try :
156
- upstream_key = LibraryUsageLocatorV2 .from_string (downstream .upstream )
157
- except InvalidKeyError as exc :
158
- raise BadUpstream (_ ("Reference to linked library item is malformed" )) from exc
188
+
189
+ # Ensure that the upstream block is of a compatible type.
159
190
downstream_type = downstream .usage_key .block_type
160
191
if upstream_key .block_type != downstream_type :
161
192
# Note: Currently, we strictly enforce that the downstream and upstream block_types must exactly match.
@@ -178,8 +209,8 @@ def get_for_block(cls, downstream: XBlock) -> t.Self:
178
209
except XBlockNotFoundError as exc :
179
210
raise BadUpstream (_ ("Linked library item was not found in the system" )) from exc
180
211
return cls (
181
- upstream_ref = downstream . upstream ,
182
- version_synced = downstream .upstream_version ,
212
+ upstream_ref = str ( upstream_key ) ,
213
+ version_synced = downstream .upstream_version if downstream . upstream else 0 ,
183
214
version_available = (lib_meta .published_version_num if lib_meta else None ),
184
215
version_declined = downstream .upstream_version_declined ,
185
216
error_message = None ,
@@ -201,6 +232,13 @@ def sync_from_upstream(downstream: XBlock, user: User) -> None:
201
232
_update_tags (upstream = upstream , downstream = downstream )
202
233
downstream .upstream_version = link .version_available
203
234
235
+ # Explicitly set the `upstream` setting of the downstream block from the upstream's usage key.
236
+ # In most cases, this is a no-op, since that is normally how we'd spefically an upstream.
237
+ # However, it is also possible for a block to have implicitly-defined upstream-- particularly, if it is the child of
238
+ # a LegacyLibraryContentBlock, whose source library was recently migrated from a V1 library to a V2 library.
239
+ # In that case, we want to "migrate" the downstream to the new schema by explicitly setting its `upstream` setting.
240
+ downstream .upstream = str (upstream .usage_key )
241
+
204
242
205
243
def fetch_customizable_fields (* , downstream : XBlock , user : User , upstream : XBlock | None = None ) -> None :
206
244
"""
@@ -213,6 +251,9 @@ def fetch_customizable_fields(*, downstream: XBlock, user: User, upstream: XBloc
213
251
_link , upstream = _load_upstream_link_and_block (downstream , user )
214
252
_update_customizable_fields (upstream = upstream , downstream = downstream , only_fetch = True )
215
253
254
+ # (see comment in sync_from_upstream)
255
+ downstream .upstream = str (upstream .usage_key )
256
+
216
257
217
258
def _load_upstream_link_and_block (downstream : XBlock , user : User ) -> tuple [UpstreamLink , XBlock ]:
218
259
"""
@@ -227,14 +268,16 @@ def _load_upstream_link_and_block(downstream: XBlock, user: User) -> tuple[Upstr
227
268
# We import load_block here b/c UpstreamSyncMixin is used by cms/envs, which loads before the djangoapps are ready.
228
269
from openedx .core .djangoapps .xblock .api import load_block , CheckPerm , LatestVersion # pylint: disable=wrong-import-order
229
270
try :
271
+ # We know that upstream_ref cannot be None, since get_for_block returned successfully.
272
+ upstream_ref : str = link .upstream_ref # type: ignore[assignment]
230
273
lib_block : XBlock = load_block (
231
- LibraryUsageLocatorV2 .from_string (downstream . upstream ),
274
+ LibraryUsageLocatorV2 .from_string (upstream_ref ),
232
275
user ,
233
276
check_permission = CheckPerm .CAN_READ_AS_AUTHOR ,
234
277
version = LatestVersion .PUBLISHED ,
235
278
)
236
279
except (NotFound , PermissionDenied ) as exc :
237
- raise BadUpstream (_ ("Linked library item could not be loaded: {}" ).format (downstream . upstream )) from exc
280
+ raise BadUpstream (_ ("Linked library item could not be loaded: {}" ).format (link . upstream_ref )) from exc
238
281
return link , lib_block
239
282
240
283
0 commit comments