@@ -106,6 +106,34 @@ class EntityLinkBase(models.Model):
106106 class Meta :
107107 abstract = True
108108
109+
110+ class ComponentLink (EntityLinkBase ):
111+ """
112+ This represents link between any two publishable entities or link between publishable entity and a course
113+ XBlock. It helps in tracking relationship between XBlocks imported from libraries and used in different courses.
114+ """
115+ upstream_block = models .ForeignKey (
116+ Component ,
117+ on_delete = models .SET_NULL ,
118+ related_name = "links" ,
119+ null = True ,
120+ blank = True ,
121+ )
122+ upstream_usage_key = UsageKeyField (
123+ max_length = 255 ,
124+ help_text = _ (
125+ "Upstream block usage key, this value cannot be null"
126+ " and useful to track upstream library blocks that do not exist yet"
127+ )
128+ )
129+
130+ class Meta :
131+ verbose_name = _ ("Component Link" )
132+ verbose_name_plural = _ ("Component Links" )
133+
134+ def __str__ (self ):
135+ return f"ComponentLink<{ self .upstream_usage_key } ->{ self .downstream_usage_key } >"
136+
109137 @property
110138 def upstream_version_num (self ) -> int | None :
111139 """
@@ -177,34 +205,6 @@ def summarize_by_downstream_context(cls, downstream_context_key: CourseKey) -> Q
177205 )
178206 return result
179207
180-
181- class ComponentLink (EntityLinkBase ):
182- """
183- This represents link between any two publishable entities or link between publishable entity and a course
184- XBlock. It helps in tracking relationship between XBlocks imported from libraries and used in different courses.
185- """
186- upstream_block = models .ForeignKey (
187- Component ,
188- on_delete = models .SET_NULL ,
189- related_name = "links" ,
190- null = True ,
191- blank = True ,
192- )
193- upstream_usage_key = UsageKeyField (
194- max_length = 255 ,
195- help_text = _ (
196- "Upstream block usage key, this value cannot be null"
197- " and useful to track upstream library blocks that do not exist yet"
198- )
199- )
200-
201- class Meta :
202- verbose_name = _ ("Component Link" )
203- verbose_name_plural = _ ("Component Links" )
204-
205- def __str__ (self ):
206- return f"ComponentLink<{ self .upstream_usage_key } ->{ self .downstream_usage_key } >"
207-
208208 @classmethod
209209 def update_or_create (
210210 cls ,
@@ -232,25 +232,15 @@ def update_or_create(
232232 'version_declined' : version_declined ,
233233 }
234234 if upstream_block :
235- new_values .update (
236- {
237- 'upstream_block' : upstream_block ,
238- }
239- )
235+ new_values ['upstream_block' ] = upstream_block
240236 try :
241237 link = cls .objects .get (downstream_usage_key = downstream_usage_key )
242- # TODO: until we save modified datetime for course xblocks in index, the modified time for links are updated
243- # everytime a downstream/course block is updated. This allows us to order links[1] based on recently
244- # modified downstream version.
245- # pylint: disable=line-too-long
246- # 1. https://github.com/open-craft/frontend-app-course-authoring/blob/0443d88824095f6f65a3a64b77244af590d4edff/src/course-libraries/ReviewTabContent.tsx#L222-L233
247- has_changes = True # change to false once above condition is met.
248- for key , value in new_values .items ():
249- prev = getattr (link , key )
250- # None != None is True, so we need to check for it specially
251- if prev != value and ~ (prev is None and value is None ):
238+ has_changes = False
239+ for key , new_value in new_values .items ():
240+ prev_value = getattr (link , key )
241+ if prev_value != new_value :
252242 has_changes = True
253- setattr (link , key , value )
243+ setattr (link , key , new_value )
254244 if has_changes :
255245 link .updated = created
256246 link .save ()
@@ -290,6 +280,77 @@ class Meta:
290280 def __str__ (self ):
291281 return f"ContainerLink<{ self .upstream_container_key } ->{ self .downstream_usage_key } >"
292282
283+ @property
284+ def upstream_version_num (self ) -> int | None :
285+ """
286+ Returns upstream container version number if available.
287+ """
288+ published_version = get_published_version (self .upstream_container .publishable_entity .id )
289+ return published_version .version_num if published_version else None
290+
291+ @property
292+ def upstream_context_title (self ) -> str :
293+ """
294+ Returns upstream context title.
295+ """
296+ return self .upstream_container .publishable_entity .learning_package .title
297+
298+ @classmethod
299+ def filter_links (
300+ cls ,
301+ ** link_filter ,
302+ ) -> QuerySet ["EntityLinkBase" ]:
303+ """
304+ Get all links along with sync flag, upstream context title and version, with optional filtering.
305+ """
306+ ready_to_sync = link_filter .pop ('ready_to_sync' , None )
307+ result = cls .objects .filter (** link_filter ).select_related (
308+ "upstream_container__publishable_entity__published__version" ,
309+ "upstream_container__publishable_entity__learning_package"
310+ ).annotate (
311+ ready_to_sync = (
312+ GreaterThan (
313+ Coalesce ("upstream_container__publishable_entity__published__version__version_num" , 0 ),
314+ Coalesce ("version_synced" , 0 )
315+ ) & GreaterThan (
316+ Coalesce ("upstream_container__publishable_entity__published__version__version_num" , 0 ),
317+ Coalesce ("version_declined" , 0 )
318+ )
319+ )
320+ )
321+ if ready_to_sync is not None :
322+ result = result .filter (ready_to_sync = ready_to_sync )
323+ return result
324+
325+ @classmethod
326+ def summarize_by_downstream_context (cls , downstream_context_key : CourseKey ) -> QuerySet :
327+ """
328+ Returns a summary of links by upstream context for given downstream_context_key.
329+ Example:
330+ [
331+ {
332+ "upstream_context_title": "CS problems 3",
333+ "upstream_context_key": "lib:OpenedX:CSPROB3",
334+ "ready_to_sync_count": 11,
335+ "total_count": 14
336+ },
337+ {
338+ "upstream_context_title": "CS problems 2",
339+ "upstream_context_key": "lib:OpenedX:CSPROB2",
340+ "ready_to_sync_count": 15,
341+ "total_count": 24
342+ },
343+ ]
344+ """
345+ result = cls .filter_links (downstream_context_key = downstream_context_key ).values (
346+ "upstream_context_key" ,
347+ upstream_context_title = F ("upstream_container__publishable_entity__learning_package__title" ),
348+ ).annotate (
349+ ready_to_sync_count = Count ("id" , Q (ready_to_sync = True )),
350+ total_count = Count ('id' )
351+ )
352+ return result
353+
293354 @classmethod
294355 def update_or_create (
295356 cls ,
@@ -317,25 +378,15 @@ def update_or_create(
317378 'version_declined' : version_declined ,
318379 }
319380 if upstream_container :
320- new_values .update (
321- {
322- 'upstream_container' : upstream_container ,
323- }
324- )
381+ new_values ['upstream_container' ] = upstream_container
325382 try :
326383 link = cls .objects .get (downstream_usage_key = downstream_usage_key )
327- # TODO: until we save modified datetime for course xblocks in index, the modified time for links are updated
328- # everytime a downstream/course block is updated. This allows us to order links[1] based on recently
329- # modified downstream version.
330- # pylint: disable=line-too-long
331- # 1. https://github.com/open-craft/frontend-app-course-authoring/blob/0443d88824095f6f65a3a64b77244af590d4edff/src/course-libraries/ReviewTabContent.tsx#L222-L233
332- has_changes = True # change to false once above condition is met.
333- for key , value in new_values .items ():
334- prev = getattr (link , key )
335- # None != None is True, so we need to check for it specially
336- if prev != value and ~ (prev is None and value is None ):
384+ has_changes = False
385+ for key , new_value in new_values .items ():
386+ prev_value = getattr (link , key )
387+ if prev_value != new_value :
337388 has_changes = True
338- setattr (link , key , value )
389+ setattr (link , key , new_value )
339390 if has_changes :
340391 link .updated = created
341392 link .save ()
0 commit comments