@@ -327,7 +327,7 @@ def rename_document(
327327 new_name : str ,
328328 * ,
329329 request : Any = None ,
330- ) -> tuple [bool , str , str | None ]:
330+ ) -> tuple [bool , str , str | None , bool ]:
331331 """Rename a document's file within a corpus (change the filename only).
332332
333333 Creates a new immutable :class:`DocumentPath` history node whose
@@ -358,10 +358,14 @@ def rename_document(
358358 new_name: Desired new filename (last path segment only).
359359
360360 Returns:
361- ``(success, error_message, new_path)``. ``new_path`` is the
361+ ``(success, error_message, new_path, changed )``. ``new_path`` is the
362362 resulting path string on success (possibly disambiguated, e.g.
363- ``/Legal/Q3_Summary_1.pdf``) and ``None`` on failure. A no-op
364- rename (the name is unchanged) returns ``(True, "", <current path>)``.
363+ ``/Legal/Q3_Summary_1.pdf``) and ``None`` on failure. ``changed`` is
364+ ``True`` only when the filename actually changed and ``False`` for a
365+ no-op (the sanitised name already matched the current filename) or
366+ any failure — letting callers distinguish a real rename from a no-op
367+ without re-reading the pre-rename path. A no-op rename returns
368+ ``(True, "", <current path>, False)``.
365369
366370 Validations:
367371 - User has corpus UPDATE permission
@@ -377,14 +381,15 @@ def rename_document(
377381 False ,
378382 "Permission denied: You do not have write access to this corpus" ,
379383 None ,
384+ False ,
380385 )
381386
382387 # Validate document belongs to corpus
383388 if not CorpusDocumentService ._check_document_in_corpus (document , corpus ):
384- return False , "Document does not belong to this corpus" , None
389+ return False , "Document does not belong to this corpus" , None , False
385390
386391 if not new_name or not new_name .strip ():
387- return False , "New name must not be empty" , None
392+ return False , "New name must not be empty" , None , False
388393
389394 sanitized = sanitize_corpus_filename (new_name .strip ())
390395
@@ -407,7 +412,7 @@ def rename_document(
407412 )
408413
409414 if not current :
410- return False , "No active document path found" , None
415+ return False , "No active document path found" , None , False
411416
412417 # Split the current path into "<directory>/" + "<filename>".
413418 # Stored paths always start with "/", so rpartition yields a
@@ -423,7 +428,7 @@ def rename_document(
423428
424429 # No-op: the file already has this name.
425430 if new_filename == current_filename :
426- return True , "" , current .path
431+ return True , "" , current .path , False
427432
428433 base_path = f"{ directory } { new_filename } "
429434
@@ -436,7 +441,7 @@ def rename_document(
436441 user = user ,
437442 )
438443 except ValueError as exc :
439- return False , str (exc ), None
444+ return False , str (exc ), None , False
440445 except IntegrityError as exc :
441446 logger .warning (
442447 "IntegrityError renaming document %s in corpus %s after "
@@ -445,7 +450,7 @@ def rename_document(
445450 corpus .id ,
446451 exc ,
447452 )
448- return False , f"{ PATH_CONFLICT_MSG } , please retry: { exc } " , None
453+ return False , f"{ PATH_CONFLICT_MSG } , please retry: { exc } " , None , False
449454
450455 logger .info (
451456 "Renamed document %s in corpus %s from %r to %r by user %s" ,
@@ -455,7 +460,7 @@ def rename_document(
455460 chosen_path ,
456461 user .id ,
457462 )
458- return True , "" , chosen_path
463+ return True , "" , chosen_path , True
459464
460465 @classmethod
461466 def move_documents_to_folder (
0 commit comments