Skip to content
Merged
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
5 changes: 5 additions & 0 deletions specifyweb/frontend/js_src/lib/components/WbActions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function WbActions({
useBooleanState();
const [operationCompleted, openOperationCompleted, closeOperationCompleted] =
useBooleanState();

const { mode, refreshInitiatorAborted, startUpload, triggerStatusComponent } =
useWbActions({
datasetId: dataset.id,
Expand Down Expand Up @@ -262,6 +263,10 @@ function useWbActions({
const refreshInitiatorAborted = React.useRef<boolean>(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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export type Dataset = DatasetBase &
readonly uploadplan: UploadPlan | null;
readonly visualorder: RA<number> | null;
readonly isupdate: boolean;
readonly rolledback: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -376,6 +377,11 @@ export function DataSetName({
{dataset.uploadresult?.success === true && (
<span className="text-red-600">{wbText.dataSetUploadedLabel()}</span>
)}
{dataset.isupdate && dataset.rolledback && (
<span className="text-red-600">
{batchEditText.cannotEditAfterRollback()}
</span>
)}
</h2>
<Button.Small onClick={handleOpen}>
{getField(tables.WorkbenchTemplateMappingItem, 'metaData').label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function WbSpreadsheetComponent({
workbench,
mappings,
isResultsOpen,
hasBatchEditRolledBack,
checkDeletedFail,
spreadsheetChanged,
onClickDisambiguate: handleClickDisambiguate,
Expand All @@ -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;
Expand Down Expand Up @@ -173,7 +175,7 @@ function WbSpreadsheetComponent({
};

React.useEffect(() => {
if (hot === undefined) return;
if (hot === undefined || hasBatchEditRolledBack) return;
hot.batch(() => {
(mappings === undefined
? Promise.resolve({})
Expand Down
11 changes: 10 additions & 1 deletion specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,17 @@ export function WbView({

const searchRef = React.useRef<HTMLInputElement | null>(null);

const hasBatchEditRolledBack = dataset.rolledback && dataset.isupdate;

return (
<ReadOnlyContext.Provider
value={isAlreadyReadOnly || isUploaded || showResults || !canUpdate}
value={
isAlreadyReadOnly ||
isUploaded ||
showResults ||
!canUpdate ||
hasBatchEditRolledBack
}
>
<section
className={`wbs-form ${className.containerFull}`}
Expand Down Expand Up @@ -228,6 +236,7 @@ export function WbView({
checkDeletedFail={checkDeletedFail}
data={data}
dataset={dataset}
hasBatchEditRolledBack={hasBatchEditRolledBack}
hot={hot}
isResultsOpen={showResults}
isUploaded={isUploaded}
Expand Down
10 changes: 6 additions & 4 deletions specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOMServer from 'react-dom/server';

import { wbPlanText } from '../../localization/wbPlan';
import { icons } from '../Atoms/Icons';
import { ReadOnlyContext } from '../Core/Contexts';
import { TableIcon } from '../Molecules/TableIcon';
import { userPreferences } from '../Preferences/userPreferences';
import type { Dataset } from '../WbPlanView/Wrapped';
Expand All @@ -26,6 +27,7 @@ export function useHotProps({
readonly mappings: WbMapping | undefined;
readonly physicalColToMappingCol: (physicalCol: number) => number | undefined;
}) {
const isReadOnly = React.useContext(ReadOnlyContext);
const [autoWrapCol] = userPreferences.use(
'workBench',
'editor',
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,21 @@ type PropagatedFailure = State<'PropagatedFailure'>;
type MatchedAndChanged = State<'MatchedAndChanged', Omit<Matched, 'type'>>;

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 = {
Expand Down
4 changes: 4 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/batchEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
18 changes: 18 additions & 0 deletions specifyweb/workbench/migrations/0008_spdataset_rolledback.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 2 additions & 0 deletions specifyweb/workbench/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion specifyweb/workbench/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
ds.rolledback = True
ds.save(update_fields=['uploaderstatus', 'rolledback'])