diff --git a/requirements.txt b/requirements.txt index 85dcd6373a8..922ec732a82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ setuptools>=50.0.0 tzdata wheel backports.zoneinfo==0.2.1 -kombu==5.2.4 -celery[redis]==5.2.7 +kombu==5.5.2 +celery[redis]==5.5.1 Django==4.2.18 mysqlclient==2.1.1 SQLAlchemy==1.2.11 @@ -12,4 +12,4 @@ pycryptodome==3.21.0 PyJWT==2.3.0 django-auth-ldap==1.2.17 jsonschema==3.2.0 -typing-extensions==4.3.0 +typing-extensions==4.12.2 diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index ac6afb9ca41..70e40984783 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -58,6 +58,7 @@ export function WbActions({ useBooleanState(); const [operationCompleted, openOperationCompleted, closeOperationCompleted] = useBooleanState(); + const { mode, refreshInitiatorAborted, startUpload, triggerStatusComponent } = useWbActions({ datasetId: dataset.id, @@ -262,6 +263,10 @@ function useWbActions({ const refreshInitiatorAborted = React.useRef(false); const loading = React.useContext(LoadingContext); + /** + * NOTE: Only validate and upload use startUpload + * For rollback, we directly call the API inside the RollbackConfirmation component + */ const startUpload = (newMode: WbStatus): void => { workbench.validation.stopLiveValidation(); loading( diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx index 12da809ef87..505ec512d1d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/Wrapped.tsx @@ -78,6 +78,7 @@ export type Dataset = DatasetBase & readonly uploadplan: UploadPlan | null; readonly visualorder: RA | null; readonly isupdate: boolean; + readonly rolledback: boolean; }; /** diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts index e726fbe1015..960b9a54fa3 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/uploadPlanBuilder.ts @@ -7,7 +7,7 @@ import type { Tables } from '../DataModel/types'; import { getTreeDefinitions, isTreeTable } from '../InitialContext/treeRanks'; import { defaultColumnOptions } from './linesGetter'; import type { BatchEditPrefs } from './Mapper'; -import type { SplitMappingPath} from './mappingHelpers'; +import type { SplitMappingPath } from './mappingHelpers'; import { valueIsTreeMeta } from './mappingHelpers'; import { getNameFromTreeDefinitionName, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index f2097e632ed..ea9c9ca54b5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -4,6 +4,7 @@ import type { LocalizedString } from 'typesafe-i18n'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; +import { batchEditText } from '../../localization/batchEdit'; import { commonText } from '../../localization/common'; import { StringToJsx } from '../../localization/utils'; import { wbText } from '../../localization/workbench'; @@ -376,6 +377,11 @@ export function DataSetName({ {dataset.uploadresult?.success === true && ( {wbText.dataSetUploadedLabel()} )} + {dataset.isupdate && dataset.rolledback && ( + + {batchEditText.cannotEditAfterRollback()} + + )} {getField(tables.WorkbenchTemplateMappingItem, 'metaData').label} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index d9d57c0dabf..404bbddf63c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -41,6 +41,7 @@ function WbSpreadsheetComponent({ workbench, mappings, isResultsOpen, + hasBatchEditRolledBack, checkDeletedFail, spreadsheetChanged, onClickDisambiguate: handleClickDisambiguate, @@ -53,6 +54,7 @@ function WbSpreadsheetComponent({ readonly workbench: Workbench; readonly mappings: WbMapping | undefined; readonly isResultsOpen: boolean; + readonly hasBatchEditRolledBack: boolean; readonly checkDeletedFail: (statusCode: number) => boolean; readonly spreadsheetChanged: () => void; readonly onClickDisambiguate: () => void; @@ -173,7 +175,7 @@ function WbSpreadsheetComponent({ }; React.useEffect(() => { - if (hot === undefined) return; + if (hot === undefined || hasBatchEditRolledBack) return; hot.batch(() => { (mappings === undefined ? Promise.resolve({}) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index c502c2d6abf..c03ba4b5d2a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -169,9 +169,17 @@ export function WbView({ const searchRef = React.useRef(null); + const hasBatchEditRolledBack = dataset.rolledback && dataset.isupdate; + return (
number | undefined; }) { + const isReadOnly = React.useContext(ReadOnlyContext); const [autoWrapCol] = userPreferences.use( 'workBench', 'editor', @@ -46,12 +48,12 @@ export function useHotProps({ (_, physicalCol) => ({ // Get data from nth column for nth column data: physicalCol, - readOnly: [-1, undefined].includes( - physicalColToMappingCol(physicalCol) - ), + readOnly: + isReadOnly || + [-1, undefined].includes(physicalColToMappingCol(physicalCol)), }) ), - [dataset.columns.length] + [dataset.columns.length, isReadOnly] ); const [enterMovesPref] = userPreferences.use( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/resultsParser.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/resultsParser.ts index 0df58ddfca6..8c1dc83d106 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/resultsParser.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/resultsParser.ts @@ -154,7 +154,21 @@ type PropagatedFailure = State<'PropagatedFailure'>; type MatchedAndChanged = State<'MatchedAndChanged', Omit>; type RecordResultTypes = - Deleted | Deleted | FailedBusinessRule | Matched | MatchedAndChanged | MatchedAndChanged | MatchedMultiple | NoChange | NoChange | NoMatch | NullRecord | ParseFailures | PropagatedFailure | Updated | Uploaded; + | Deleted + | Deleted + | FailedBusinessRule + | Matched + | MatchedAndChanged + | MatchedAndChanged + | MatchedMultiple + | NoChange + | NoChange + | NoMatch + | NullRecord + | ParseFailures + | PropagatedFailure + | Updated + | Uploaded; // Records the specific result of attempting to upload a particular record type WbRecordResult = { diff --git a/specifyweb/frontend/js_src/lib/localization/batchEdit.ts b/specifyweb/frontend/js_src/lib/localization/batchEdit.ts index 0a4f6018582..8e699df85f4 100644 --- a/specifyweb/frontend/js_src/lib/localization/batchEdit.ts +++ b/specifyweb/frontend/js_src/lib/localization/batchEdit.ts @@ -109,4 +109,8 @@ export const batchEditText = createDictionary({ 'en-us': 'Batch Edit is disabled for system tables and scoping hierarchy tables', }, + cannotEditAfterRollback: { + 'en-us': + '(Batch Edit datasets cannot be edited after rollback - Read Only)', + }, } as const); diff --git a/specifyweb/workbench/migrations/0008_spdataset_rolledback.py b/specifyweb/workbench/migrations/0008_spdataset_rolledback.py new file mode 100644 index 00000000000..431fd957b5e --- /dev/null +++ b/specifyweb/workbench/migrations/0008_spdataset_rolledback.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2025-04-17 15:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workbench', '0007_spdatasetattachment'), + ] + + operations = [ + migrations.AddField( + model_name='spdataset', + name='rolledback', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/specifyweb/workbench/models.py b/specifyweb/workbench/models.py index 378fa5b7e8a..9fa5caf09ce 100644 --- a/specifyweb/workbench/models.py +++ b/specifyweb/workbench/models.py @@ -145,6 +145,7 @@ class Spdataset(Dataset): rowresults = models.TextField(null=True) isupdate = models.BooleanField(default=False, null=True) + rolledback = models.BooleanField(default=False, null=True) # very complicated. Essentially, each batch-edit dataset gets backed by another dataset (for rollbacks). # This should be a one-to-one field, imagine the mess otherwise. @@ -163,6 +164,7 @@ def get_dataset_as_dict(self): "visualorder": self.visualorder, "rowresults": self.rowresults and json.loads(self.rowresults), "isupdate": self.isupdate == True, + "rolledback": self.rolledback == True, } ) return ds_dict diff --git a/specifyweb/workbench/tasks.py b/specifyweb/workbench/tasks.py index e4b0ee5eacb..61f9e34fe94 100644 --- a/specifyweb/workbench/tasks.py +++ b/specifyweb/workbench/tasks.py @@ -92,4 +92,5 @@ def progress(current: int, total: Optional[int]) -> None: unupload_dataset(ds, agent, progress) ds.uploaderstatus = None - ds.save(update_fields=['uploaderstatus']) \ No newline at end of file + ds.rolledback = True + ds.save(update_fields=['uploaderstatus', 'rolledback']) \ No newline at end of file