Make shipment fields optional and refactor test scripts.

Updated the `number_of_pucks` and `number_of_samples` fields in the `schemas.py` to be optional for greater flexibility. Simplified the test Jupyter Notebook by restructuring imports and consolidating function calls for better readability and maintainability.
This commit is contained in:
GotthardG
2025-01-17 09:36:16 +01:00
parent 481068603b
commit 9739b8cfe9
6 changed files with 485 additions and 309 deletions

View File

@ -126,7 +126,7 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
throw new Error('Missing required fields');
}
const createdDewar = await DewarsService.createDewarDewarsPost(newDewarToPost);
const createdDewar = await DewarsService.createOrUpdateDewarDewarsPost(selectedShipment.id, newDewarToPost);
if (createdDewar && selectedShipment) {
const updatedShipment = await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(selectedShipment.id, createdDewar.id);

View File

@ -63,7 +63,6 @@ const SpreadsheetTable = ({
shipping_date: null,
arrival_date: null,
returning_date: null,
qrcode: 'N/A',
contact_person_id: selectedShipment?.contact_person?.id,
return_address_id: selectedShipment?.return_address?.id,
dewar_name: '',
@ -236,14 +235,23 @@ const SpreadsheetTable = ({
'chiphiangles': 31
};
const checkIfDewarExists = async (dewarName) => {
const checkIfDewarExists = async (dewarName: string) => {
if (!selectedShipment) return null;
try {
// Fetch dewars related to the current shipment via API
const shipDewars = await ShipmentsService.getDewarsByShipmentIdShipmentsShipmentIdDewarsGet(selectedShipment.id);
return shipDewars.find((d) => d.dewar_name === dewarName);
// Search for dewar by name within the shipment
const existingDewar = shipDewars.find((d) => d.dewar_name === dewarName);
if (existingDewar) {
console.log(`Dewar "${dewarName}" exists with ID: ${existingDewar.id}`);
return existingDewar;
}
return null;
} catch (error) {
console.error('Failed to fetch existing dewars:', error);
console.error("Failed to fetch existing dewars:", error);
return null;
}
};
@ -258,75 +266,34 @@ const SpreadsheetTable = ({
const puckPositionMap = new Map();
const dewarsToReplace = [];
const dewarNameIdx = fieldToCol['dewarname'];
const puckNameIdx = fieldToCol['puckname'];
const puckTypeIdx = fieldToCol['pucktype'];
const sampleNameIdx = fieldToCol['crystalname'];
const proteinNameIdx = fieldToCol['proteinname'];
const samplePositionIdx = fieldToCol['positioninpuck'];
const priorityIdx = fieldToCol['priority'];
const commentsIdx = fieldToCol['comments'];
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
const row = data[rowIndex];
if (!row.data) {
console.error('Row data is missing');
console.error(`Row ${rowIndex}: Missing or invalid data`);
continue;
}
// Extract values from the appropriate columns
const dewarName = typeof row.data[dewarNameIdx] === 'string' ? row.data[dewarNameIdx].trim() : null;
const puckName = row.data[puckNameIdx] !== undefined && row.data[puckNameIdx] !== null ? String(row.data[puckNameIdx]).trim() : null;
const puckType = typeof row.data[puckTypeIdx] === 'string' ? row.data[puckTypeIdx] : 'Unipuck';
const sampleName = typeof row.data[sampleNameIdx] === 'string' ? row.data[sampleNameIdx].trim() : null;
const proteinName = typeof row.data[proteinNameIdx] === 'string' ? row.data[proteinNameIdx].trim() : null;
const samplePosition = row.data[samplePositionIdx] !== undefined && row.data[samplePositionIdx] !== null ? Number(row.data[samplePositionIdx]) : null;
const priority = row?.data?.[priorityIdx] ? Number(row.data[priorityIdx]) : null;
const comments = typeof row.data[commentsIdx] === 'string' ? row.data[commentsIdx].trim() : null;
// Create data_collection_parameters object
const dataCollectionParameters = {
directory: row.data[fieldToCol['directory']],
oscillation: row.data[fieldToCol['oscillation']] ? parseFloat(row.data[fieldToCol['oscillation']]) : undefined,
aperture: row.data[fieldToCol['aperture']] ? row.data[fieldToCol['aperture']].trim() : undefined,
exposure: row.data[fieldToCol['exposure']] ? parseFloat(row.data[fieldToCol['exposure']]) : undefined,
totalrange: row.data[fieldToCol['totalrange']] ? parseInt(row.data[fieldToCol['totalrange']], 10) : undefined,
transmission: row.data[fieldToCol['transmission']] ? parseInt(row.data[fieldToCol['transmission']], 10) : undefined,
dose: row.data[fieldToCol['dose']] ? parseFloat(row.data[fieldToCol['dose']]) : undefined,
targetresolution: row.data[fieldToCol['targetresolution']] ? parseFloat(row.data[fieldToCol['targetresolution']]) : undefined,
datacollectiontype: row.data[fieldToCol['datacollectiontype']],
processingpipeline: row.data[fieldToCol['processingpipeline']],
spacegroupnumber: row.data[fieldToCol['spacegroupnumber']] ? parseInt(row.data[fieldToCol['spacegroupnumber']], 10) : undefined,
cellparameters: row.data[fieldToCol['cellparameters']],
rescutkey: row.data[fieldToCol['rescutkey']],
rescutvalue: row.data[fieldToCol['rescutvalue']] ? parseFloat(row.data[fieldToCol['rescutvalue']]) : undefined,
userresolution: row.data[fieldToCol['userresolution']] ? parseFloat(row.data[fieldToCol['userresolution']]) : undefined,
pdbid: row.data[fieldToCol['pdbid']],
autoprocfull: row.data[fieldToCol['autoprocfull']] === true,
procfull: row.data[fieldToCol['procfull']] === true,
adpenabled: row.data[fieldToCol['adpenabled']] === true,
noano: row.data[fieldToCol['noano']] === true,
ffcscampaign: row.data[fieldToCol['ffcscampaign']] === true,
trustedhigh: row.data[fieldToCol['trustedhigh']] ? parseFloat(row.data[fieldToCol['trustedhigh']]) : undefined,
autoprocextraparams: row.data[fieldToCol['autoprocextraparams']],
chiphiangles: row.data[fieldToCol['chiphiangles']] ? parseFloat(row.data[fieldToCol['chiphiangles']]) : undefined,
};
const dewarName = sanitizeAndValidateColumn(row, fieldToCol['dewarname'], 'string', 'Dewar Name');
const puckName = sanitizeAndValidateColumn(row, fieldToCol['puckname'], 'string', 'Puck Name');
const puckType = sanitizeAndValidateColumn(row, fieldToCol['pucktype'], 'string', 'Unipuck', true); // Default: `Unipuck`
const sampleName = sanitizeAndValidateColumn(row, fieldToCol['crystalname'], 'string', 'Sample Name');
if (dewarName && puckName) {
let dewar;
if (!dewars.has(dewarName)) {
// Initialize new dewar object
dewar = {
...initialNewDewarState,
dewar_name: dewarName,
contact_person_id: contactPerson.id,
return_address_id: returnAddress.id,
pucks: []
pucks: [],
};
dewars.set(dewarName, dewar);
puckPositionMap.set(dewarName, new Map());
// Check if the dewar exists in the shipment
// Check if dewar exists using backend
const existingDewar = await checkIfDewarExists(dewarName);
if (existingDewar) {
dewarsToReplace.push(existingDewar);
@ -335,78 +302,115 @@ const SpreadsheetTable = ({
dewar = dewars.get(dewarName);
}
// Handle puck positions
let puckPositions = puckPositionMap.get(dewarName);
if (!puckPositions.has(puckName)) {
puckPositions.set(puckName, puckPositions.size + 1);
}
const puckPosition = puckPositions.get(puckName);
let puck = dewar.pucks.find(p => p.puck_name === puckName);
// Create puck and attach it to the dewar
let puck = dewar.pucks.find((p) => p.puck_name === puckName);
if (!puck) {
puck = {
puck_name: puckName,
puck_type: puckType,
puck_location_in_dewar: puckPosition,
samples: []
};
puck = { puck_name: puckName, puck_type: puckType, puck_location_in_dewar: puckPosition, samples: [] };
dewar.pucks.push(puck);
}
// Add sample to puck
const sample = {
sample_name: sampleName,
proteinname: proteinName,
position: samplePosition,
priority: priority,
comments: comments,
data_collection_parameters: dataCollectionParameters, // Attach the parameters
results: null // Placeholder for results field
position: sanitizeIntColumn(row, fieldToCol['positioninpuck'], 'Sample Position'),
proteinname: sanitizeAndValidateColumn(row, fieldToCol['proteinname'], 'string', 'Protein Name'),
priority: sanitizeIntColumn(row, fieldToCol['priority'], 'Priority'),
comments: sanitizeAndValidateColumn(row, fieldToCol['comments'], 'string', 'Comments', true),
data_collection_parameters: collectDataParameters(row), // Consolidate data parameters
};
if (isNaN(sample.position)) {
console.error(`Invalid sample position for sample ${sample.sample_name} in puck ${puckName}`);
} else {
puck.samples.push(sample);
}
puck.samples.push(sample);
} else {
if (!dewarName) {
console.error(`Dewar name is missing in row ${rowIndex}`);
}
if (!puckName) {
console.error(`Puck name is missing in row ${rowIndex}`);
}
console.error(`Row ${rowIndex} is missing required fields for dewar/puck creation.`);
}
}
const dewarsArray = Array.from(dewars.values());
// Save dewars array for later use in handleConfirmUpdate
// Save for update dialog control
setDewarsToCreate(dewars);
if (dewarsArray.length > 0 && dewarsToReplace.length > 0) {
setDewarsToReplace(dewarsToReplace);
setShowUpdateDialog(true);
} else {
} else if (dewarsArray.length > 0) {
await handleDewarCreation(dewarsArray);
}
};
const sanitizeAndValidateColumn = (row, colIndex, type, columnName, isOptional = false) => {
const value = row?.data?.[colIndex];
const sanitizedValue = type === 'string' ? value?.trim() : value;
if (!sanitizedValue && !isOptional) {
console.error(`${columnName} is missing or invalid.`);
return null;
}
if (type === 'number' && isNaN(sanitizedValue)) {
console.error(`${columnName} is not a valid number.`);
}
return sanitizedValue;
};
// Utility to sanitize integer columns
const sanitizeIntColumn = (row, colIndex, columnName) => {
const rawValue = row?.data?.[colIndex];
const intValue = parseInt(rawValue, 10);
if (isNaN(intValue)) {
console.error(`${columnName} is not a valid integer.`);
}
return intValue;
};
// Consolidate data collection parameters
const collectDataParameters = (row) => ({
directory: sanitizeAndValidateColumn(row, fieldToCol['directory'], 'string', 'Directory', true),
oscillation: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['oscillation'], 'number', 'Oscillation', true)),
aperture: sanitizeAndValidateColumn(row, fieldToCol['aperture'], 'string', 'Aperture', true),
exposure: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['exposure'], 'number', 'Exposure', true)),
totalrange: sanitizeIntColumn(row, fieldToCol['totalrange'], 'Total Range'),
transmission: sanitizeIntColumn(row, fieldToCol['transmission'], 'Transmission'),
dose: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['dose'], 'number', 'Dose', true)),
targetresolution: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['targetresolution'], 'number', 'Target Resolution', true)),
datacollectiontype: sanitizeAndValidateColumn(row, fieldToCol['datacollectiontype'], 'string', 'Data Collection Type', true),
processingpipeline: sanitizeAndValidateColumn(row, fieldToCol['processingpipeline'], 'string', 'Processing Pipeline', true),
spacegroupnumber: sanitizeIntColumn(row, fieldToCol['spacegroupnumber'], 'Space Group Number'),
cellparameters: sanitizeAndValidateColumn(row, fieldToCol['cellparameters'], 'string', 'Cell Parameters', true),
rescutkey: sanitizeAndValidateColumn(row, fieldToCol['rescutkey'], 'string', 'Resolution Cut Key', true),
rescutvalue: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['rescutvalue'], 'number', 'Resolution Cut Value', true)),
userresolution: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['userresolution'], 'number', 'User Resolution', true)),
pdbid: sanitizeAndValidateColumn(row, fieldToCol['pdbid'], 'string', 'PDB ID', true),
autoprocfull: row.data[fieldToCol['autoprocfull']] === true,
procfull: row.data[fieldToCol['procfull']] === true,
adpenabled: row.data[fieldToCol['adpenabled']] === true,
noano: row.data[fieldToCol['noano']] === true,
ffcscampaign: row.data[fieldToCol['ffcscampaign']] === true,
trustedhigh: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['trustedhigh'], 'number', 'Trusted High', true)),
autoprocextraparams: sanitizeAndValidateColumn(row, fieldToCol['autoprocextraparams'], 'string', 'Autoproc Extra Params', true),
chiphiangles: parseFloat(sanitizeAndValidateColumn(row, fieldToCol['chiphiangles'], 'number', 'Chi/Phi Angles', true)),
});
const handleConfirmUpdate = async () => {
if (dewarsToReplace.length === 0) return;
try {
for (const dewar of dewarsToReplace) {
await DewarsService.deleteDewarDewarsDewarIdDelete(dewar.id);
}
const dewarsArray = Array.from(dewarsToCreate.values());
await handleDewarCreation(dewarsArray);
await handleDewarCreation(dewarsArray); // Updated logic handles replacement
console.log('Dewars replaced successfully');
} catch (error) {
console.error('Error replacing dewar', error);
if (error instanceof ApiError && error.body) {
console.error('Validation errors:', error.body.detail);
} else {
console.error('Unexpected error:', error);
}
console.error('Error replacing dewars:', error);
}
setShowUpdateDialog(false);
setDewarsToReplace([]);
@ -419,30 +423,18 @@ const SpreadsheetTable = ({
setDewarsToCreate(new Map());
};
const handleDewarCreation = async (dewarsArray) => {
const handleDewarCreation = async (dewarsArray: any[]) => {
for (const dewar of dewarsArray) {
try {
if (!dewar.pucks || dewar.pucks.length === 0) {
console.error(`Dewar ${dewar.dewar_name} does not have any pucks.`);
continue;
}
// Prepare payload and exclude number_of_pucks before sending
const { number_of_pucks, number_of_samples, ...payload } = dewar;
const createdDewar = await DewarsService.createDewarDewarsPost(dewar);
// Call the backend to create or update dewars
await DewarsService.createOrUpdateDewarDewarsPost(selectedShipment.id, payload);
if (createdDewar && selectedShipment) {
await ShipmentsService.addDewarToShipmentShipmentsShipmentIdAddDewarPost(
selectedShipment.id,
createdDewar.id
);
console.log(`Dewar ${createdDewar.dewar_name} with ID ${createdDewar.id} created and added to the shipment.`);
}
console.log(`Dewar "${dewar.dewar_name}" processed successfully.`);
} catch (error) {
console.error('Error adding dewar', error);
if (error instanceof ApiError && error.body) {
console.error('Validation errors:', error.body.detail);
} else {
console.error('Unexpected error:', error);
}
console.error("Error creating/updating dewar:", error);
}
}
};