1
+ from __future__ import annotations # Necessary until Python 3.12
2
+
1
3
import datetime
2
4
import os .path
3
5
import time
4
6
import uuid
5
7
from itertools import chain
6
8
from logging import getLogger
9
+ from typing import Optional , Tuple , Union
7
10
8
11
import pytesseract
9
12
from django .conf import settings
@@ -726,7 +729,31 @@ def get_contributor_count(self):
726
729
def turn_off_ocr (self ):
727
730
return self .disable_ocr or self .item .turn_off_ocr ()
728
731
729
- def can_rollback (self ):
732
+ def can_rollback (
733
+ self ,
734
+ ) -> Tuple [bool , Union [str , Transcription ], Optional [Transcription ]]:
735
+ """
736
+ Determine whether the latest transcription on this asset can be rolled back.
737
+
738
+ This checks the transcription history for the most recent non-rolled-forward
739
+ transcription that precedes the current latest transcription, excluding any
740
+ transcriptions that are rollforwards or are sources of rollforwards.
741
+
742
+ Returns:
743
+ tuple:
744
+ - (True, target, original) if a rollback is possible, where:
745
+ * target is the transcription to roll back to.
746
+ * original is the current latest transcription.
747
+ - (False, reason, None) if a rollback is not possible, where:
748
+ * reason is a string explaining why.
749
+
750
+ A rollback is only possible if:
751
+ - There is more than one transcription.
752
+ - There is a prior transcription that is not a rollforward
753
+ or a source of one.
754
+
755
+ This method does not perform the rollback, only checks feasibility.
756
+ """
730
757
# original_latest_transcription holds the actual latest transcription
731
758
# latest_transcription starts by holding the actual latest transcription,
732
759
# but if it's a rolled forward or backward transcription, we use it to
@@ -741,6 +768,7 @@ def can_rollback(self):
741
768
"Can not rollback transcription on an asset "
742
769
"with no transcriptions"
743
770
),
771
+ None ,
744
772
)
745
773
# If the latest transcription has a source (i.e., is a rollback
746
774
# or rollforward transcription), we want the original transcription
@@ -769,11 +797,34 @@ def can_rollback(self):
769
797
"Can not rollback transcription on an asset "
770
798
"with no non-rollforward older transcriptions"
771
799
),
800
+ None ,
772
801
)
773
802
774
803
return True , transcription_to_rollback_to , original_latest_transcription
775
804
776
- def rollback_transcription (self , user ):
805
+ def rollback_transcription (self , user : User ) -> Transcription :
806
+ """
807
+ Perform a rollback of the latest transcription on this asset.
808
+
809
+ This creates a new transcription that copies the text of the most recent
810
+ eligible prior transcription (as determined by `can_rollback`) and marks it
811
+ as rolled back. It also updates the original latest transcription to reflect
812
+ that it has been superseded.
813
+
814
+ Args:
815
+ user (User): The user initiating the rollback.
816
+
817
+ Returns:
818
+ Transcription: The newly created rollback transcription.
819
+
820
+ Raises:
821
+ ValueError: If rollback is not possible (e.g. no eligible transcription).
822
+
823
+ The new transcription will:
824
+ - Have `rolled_back=True`.
825
+ - Set its `source` to the transcription it is rolled back to.
826
+ - Set `supersedes` to the current latest transcription.
827
+ """
777
828
results = self .can_rollback ()
778
829
if results [0 ] is not True :
779
830
raise ValueError (results [1 ])
@@ -793,12 +844,35 @@ def rollback_transcription(self, user):
793
844
new_transcription .save ()
794
845
return new_transcription
795
846
796
- def can_rollforward (self ):
847
+ def can_rollforward (
848
+ self ,
849
+ ) -> Tuple [bool , Union [str , Transcription ], Optional [Transcription ]]:
850
+ """
851
+ Determine whether a previous rollback on this asset can be rolled forward.
852
+
853
+ This checks whether the most recent transcription is a rollback transcription
854
+ and whether the transcription it replaced (its `supersedes`) can be restored.
855
+
856
+ Returns:
857
+ tuple:
858
+ - (True, target, original) if rollforward is possible, where:
859
+ * target is the transcription to roll forward to.
860
+ * original is the current latest transcription.
861
+ - (False, reason, None) if rollforward is not possible, where:
862
+ * reason is a string explaining why.
863
+
864
+ This method handles cases where multiple rollforwards were applied,
865
+ walking backward through the transcription chain to find the appropriate
866
+ rollback origin.
867
+
868
+ A rollforward is only possible if:
869
+ - The latest transcription is a rollback.
870
+ - The rollback's superseded transcription exists and can be restored.
871
+ """
797
872
# original_latest_transcription holds the actual latest transcription
798
873
# latest_transcription starts by holding the actual latest transcription,
799
874
# but if it's a rolled forward transcription, we use it to find the most
800
875
# recent non-rolled-forward transcription and store that in latest_transcription
801
-
802
876
original_latest_transcription = latest_transcription = (
803
877
self .latest_transcription ()
804
878
)
@@ -810,6 +884,7 @@ def can_rollforward(self):
810
884
"Can not rollforward transcription on an asset "
811
885
"with no transcriptions"
812
886
),
887
+ None ,
813
888
)
814
889
815
890
if latest_transcription .rolled_forward :
@@ -826,6 +901,7 @@ def can_rollforward(self):
826
901
"Can not rollforward transcription on an asset with no "
827
902
"non-rollforward transcriptions"
828
903
),
904
+ None ,
829
905
)
830
906
# latest_transcription is now the most recent non-rolled-forward
831
907
# transcription, but we need to go back fruther based on the number
@@ -853,6 +929,7 @@ def can_rollforward(self):
853
929
"transcriptions, which shouldn't be possible. Possibly "
854
930
"incorrectly modified transcriptions for this asset."
855
931
),
932
+ None ,
856
933
)
857
934
858
935
# If the latest_transcription we end up with is a rollback transcription,
@@ -867,6 +944,7 @@ def can_rollforward(self):
867
944
"Can not rollforward transcription on an asset if the latest "
868
945
"non-rollforward transcription is not a rollback transcription"
869
946
),
947
+ None ,
870
948
)
871
949
872
950
# If that replaced transcription doesn't exist, we can't do anything
@@ -879,11 +957,33 @@ def can_rollforward(self):
879
957
"Can not rollforward transcription on an asset if the latest "
880
958
"rollback transcription did not supersede a previous transcription"
881
959
),
960
+ None ,
882
961
)
883
962
884
963
return True , transcription_to_rollforward , original_latest_transcription
885
964
886
- def rollforward_transcription (self , user ):
965
+ def rollforward_transcription (self , user : User ) -> Transcription :
966
+ """
967
+ Perform a rollforward of the most recent rollback transcription.
968
+
969
+ This creates a new transcription that restores the text from the
970
+ rollback's superseded transcription and marks it as a rollforward.
971
+
972
+ Args:
973
+ user (User): The user initiating the rollforward.
974
+
975
+ Returns:
976
+ Transcription: The newly created rollforward transcription.
977
+
978
+ Raises:
979
+ ValueError: If rollforward is not possible (e.g. no valid rollback
980
+ history or malformed transcription chain).
981
+
982
+ The new transcription will:
983
+ - Have `rolled_forward=True`.
984
+ - Set its `source` to the transcription being rolled forward to.
985
+ - Set `supersedes` to the current latest transcription.
986
+ """
887
987
results = self .can_rollforward ()
888
988
if results [0 ] is not True :
889
989
raise ValueError (results [1 ])
0 commit comments