Skip to content

Commit 403c054

Browse files
Added docstrings and type hints for the rollforward/rollback system parts (#2834)
1 parent 9a5c4ff commit 403c054

File tree

2 files changed

+215
-8
lines changed

2 files changed

+215
-8
lines changed

concordia/models.py

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from __future__ import annotations # Necessary until Python 3.12
2+
13
import datetime
24
import os.path
35
import time
46
import uuid
57
from itertools import chain
68
from logging import getLogger
9+
from typing import Optional, Tuple, Union
710

811
import pytesseract
912
from django.conf import settings
@@ -726,7 +729,31 @@ def get_contributor_count(self):
726729
def turn_off_ocr(self):
727730
return self.disable_ocr or self.item.turn_off_ocr()
728731

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+
"""
730757
# original_latest_transcription holds the actual latest transcription
731758
# latest_transcription starts by holding the actual latest transcription,
732759
# but if it's a rolled forward or backward transcription, we use it to
@@ -741,6 +768,7 @@ def can_rollback(self):
741768
"Can not rollback transcription on an asset "
742769
"with no transcriptions"
743770
),
771+
None,
744772
)
745773
# If the latest transcription has a source (i.e., is a rollback
746774
# or rollforward transcription), we want the original transcription
@@ -769,11 +797,34 @@ def can_rollback(self):
769797
"Can not rollback transcription on an asset "
770798
"with no non-rollforward older transcriptions"
771799
),
800+
None,
772801
)
773802

774803
return True, transcription_to_rollback_to, original_latest_transcription
775804

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+
"""
777828
results = self.can_rollback()
778829
if results[0] is not True:
779830
raise ValueError(results[1])
@@ -793,12 +844,35 @@ def rollback_transcription(self, user):
793844
new_transcription.save()
794845
return new_transcription
795846

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+
"""
797872
# original_latest_transcription holds the actual latest transcription
798873
# latest_transcription starts by holding the actual latest transcription,
799874
# but if it's a rolled forward transcription, we use it to find the most
800875
# recent non-rolled-forward transcription and store that in latest_transcription
801-
802876
original_latest_transcription = latest_transcription = (
803877
self.latest_transcription()
804878
)
@@ -810,6 +884,7 @@ def can_rollforward(self):
810884
"Can not rollforward transcription on an asset "
811885
"with no transcriptions"
812886
),
887+
None,
813888
)
814889

815890
if latest_transcription.rolled_forward:
@@ -826,6 +901,7 @@ def can_rollforward(self):
826901
"Can not rollforward transcription on an asset with no "
827902
"non-rollforward transcriptions"
828903
),
904+
None,
829905
)
830906
# latest_transcription is now the most recent non-rolled-forward
831907
# transcription, but we need to go back fruther based on the number
@@ -853,6 +929,7 @@ def can_rollforward(self):
853929
"transcriptions, which shouldn't be possible. Possibly "
854930
"incorrectly modified transcriptions for this asset."
855931
),
932+
None,
856933
)
857934

858935
# If the latest_transcription we end up with is a rollback transcription,
@@ -867,6 +944,7 @@ def can_rollforward(self):
867944
"Can not rollforward transcription on an asset if the latest "
868945
"non-rollforward transcription is not a rollback transcription"
869946
),
947+
None,
870948
)
871949

872950
# If that replaced transcription doesn't exist, we can't do anything
@@ -879,11 +957,33 @@ def can_rollforward(self):
879957
"Can not rollforward transcription on an asset if the latest "
880958
"rollback transcription did not supersede a previous transcription"
881959
),
960+
None,
882961
)
883962

884963
return True, transcription_to_rollforward, original_latest_transcription
885964

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+
"""
887987
results = self.can_rollforward()
888988
if results[0] is not True:
889989
raise ValueError(results[1])

concordia/views/ajax.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import logging
22
import re
33
from time import time
4+
from typing import Union
45

56
from django.conf import settings
67
from django.contrib.auth.decorators import login_required
78
from django.contrib.messages import get_messages
89
from django.core.exceptions import ValidationError
910
from django.db import connection
1011
from django.db.transaction import atomic
11-
from django.http import HttpResponse, JsonResponse
12+
from django.http import HttpRequest, HttpResponse, JsonResponse
1213
from django.shortcuts import get_object_or_404
1314
from django.urls import reverse
1415
from django.utils.timezone import now
@@ -186,7 +187,60 @@ def generate_ocr_transcription(request, *, asset_pk):
186187
@validate_anonymous_user
187188
@atomic
188189
@ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK)
189-
def rollback_transcription(request, *, asset_pk):
190+
def rollback_transcription(
191+
request: HttpRequest, *, asset_pk: Union[int, str]
192+
) -> JsonResponse:
193+
"""
194+
Perform a rollback on the latest transcription for the given asset.
195+
196+
Requires the asset to have at least one earlier transcription that is not
197+
a rollforward or the source of a rollforward. If rollback is not possible,
198+
returns a 400 response.
199+
200+
Anonymous users are supported and handled via get_anonymous_user(). They must
201+
be validated through validate_anonymous_user (this happens seamlessly if
202+
Turnstile validation is included.)
203+
204+
Args:
205+
request (HttpRequest): The incoming HTTP request.
206+
asset_pk (int or str): Primary key of the asset to roll back.
207+
208+
Returns:
209+
JsonResponse: A JSON object with the following fields:
210+
211+
- **id** (int): ID of the created transcription. Example: `123`
212+
- **sent** (float): Timestamp (UNIX epoch) when the response was sent.
213+
Example: `1715091212.123456`
214+
- **submissionUrl** (str): URL where the transcription can be edited.
215+
Example: `"/transcriptions/submit/123/"`
216+
- **text** (str): Text of the transcription.
217+
- **asset** (dict):
218+
- **id** (int): ID of the asset. Example: `456`
219+
- **status** (str): Current transcription status. Example: `"completed"`
220+
- **contributors** (int): Number of distinct contributors. Example: `3`
221+
- **message** (str): Status message.
222+
Example: `"Successfully rolled back transcription to previous version"`
223+
- **undo_available** (bool): Whether a rollback is currently possible.
224+
- **redo_available** (bool): Whether a rollforward is currently possible.
225+
226+
Example:
227+
```json
228+
{
229+
"id": 123,
230+
"sent": 1715091212.123456,
231+
"submissionUrl": "/transcriptions/submit/123/",
232+
"text": "Transcribed text...",
233+
"asset": {
234+
"id": 456,
235+
"status": "completed",
236+
"contributors": 3
237+
},
238+
"message": "Successfully rolled back transcription to previous version",
239+
"undo_available": true,
240+
"redo_available": false
241+
}
242+
```
243+
"""
190244
asset = get_object_or_404(Asset, pk=asset_pk)
191245

192246
if request.user.is_anonymous:
@@ -225,7 +279,60 @@ def rollback_transcription(request, *, asset_pk):
225279
@validate_anonymous_user
226280
@atomic
227281
@ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK)
228-
def rollforward_transcription(request, *, asset_pk):
282+
def rollforward_transcription(
283+
request: HttpRequest, *, asset_pk: Union[int, str]
284+
) -> JsonResponse:
285+
"""
286+
Perform a rollforward to the transcription previously replaced by a rollback.
287+
288+
Requires the most recent transcription to be a rollback, and for the
289+
transcription it superseded to exist and be valid. If rollforward is not
290+
possible, returns a 400 response.
291+
292+
Anonymous users are supported and handled via get_anonymous_user(). They must
293+
be validated through validate_anonymous_user (this happens seamlessly if
294+
Turnstile validation is included.)
295+
296+
Args:
297+
request (HttpRequest): The incoming HTTP request.
298+
asset_pk (int or str): Primary key of the asset to roll forward.
299+
300+
Returns:
301+
JsonResponse: A JSON object with the following fields:
302+
303+
- **id** (int): ID of the created transcription. Example: `123`
304+
- **sent** (float): Timestamp (UNIX epoch) when the response was sent.
305+
Example: `1715091212.123456`
306+
- **submissionUrl** (str): URL where the transcription can be edited.
307+
Example: `"/transcriptions/submit/123/"`
308+
- **text** (str): Text of the transcription.
309+
- **asset** (dict):
310+
- **id** (int): ID of the asset. Example: `456`
311+
- **status** (str): Current transcription status. Example: `"completed"`
312+
- **contributors** (int): Number of distinct contributors. Example: `3`
313+
- **message** (str): Status message.
314+
Example: `"Successfully restored transcription to next version"`
315+
- **undo_available** (bool): Whether a rollback is currently possible.
316+
- **redo_available** (bool): Whether a rollforward is currently possible.
317+
318+
Example:
319+
```json
320+
{
321+
"id": 123,
322+
"sent": 1715091212.123456,
323+
"submissionUrl": "/transcriptions/submit/123/",
324+
"text": "Transcribed text...",
325+
"asset": {
326+
"id": 456,
327+
"status": "completed",
328+
"contributors": 3
329+
},
330+
"message": "Successfully restored transcription to next version",
331+
"undo_available": true,
332+
"redo_available": false
333+
}
334+
```
335+
"""
229336
asset = get_object_or_404(Asset, pk=asset_pk)
230337

231338
if request.user.is_anonymous:

0 commit comments

Comments
 (0)