Add default values to optional fields in SampleUpdate
This commit is contained in:
parent
ac38bc3bb6
commit
0f6759e417
@ -313,6 +313,40 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
|
||||
@router.put("/samples/{sample_id}", response_model=Sample)
|
||||
async def update_sample(
|
||||
sample_id: int,
|
||||
sample_update: SampleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# Log the payload received from the frontend
|
||||
logging.info(
|
||||
f"Payload received for sample ID {sample_id}: "
|
||||
f"{sample_update.dict(exclude_unset=True)}"
|
||||
)
|
||||
|
||||
# Query the sample from the database
|
||||
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
|
||||
|
||||
if not sample:
|
||||
logging.error(f"Sample with ID {sample_id} not found")
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
|
||||
# Apply updates
|
||||
for key, value in sample_update.dict(exclude_unset=True).items():
|
||||
if hasattr(sample, key):
|
||||
setattr(sample, key, value)
|
||||
|
||||
# Commit changes to the database
|
||||
db.commit()
|
||||
db.refresh(sample) # Reload the updated sample object
|
||||
|
||||
# Log the updated sample before returning it
|
||||
logging.info(f"Updated sample with ID {sample_id}: {Sample.from_orm(sample)}")
|
||||
|
||||
return Sample.from_orm(sample)
|
||||
|
||||
|
||||
@router.get("/dewars/{dewar_id}/samples", response_model=Dewar)
|
||||
async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
|
||||
# Fetch the Dewar with nested relationships
|
||||
@ -374,27 +408,6 @@ async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
|
||||
return Dewar(**dewar_data)
|
||||
|
||||
|
||||
@router.put("/samples/{sample_id}", response_model=Sample)
|
||||
async def update_sample(
|
||||
sample_id: int,
|
||||
sample_update: SampleUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
|
||||
if not sample:
|
||||
raise HTTPException(status_code=404, detail="Sample not found")
|
||||
|
||||
# Apply updates
|
||||
for key, value in sample_update.dict(exclude_unset=True).items():
|
||||
if hasattr(sample, key):
|
||||
setattr(sample, key, value)
|
||||
|
||||
# Save changes
|
||||
db.commit()
|
||||
db.refresh(sample)
|
||||
return Sample.from_orm(sample)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DewarSchema])
|
||||
async def get_dewars(db: Session = Depends(get_db)):
|
||||
try:
|
||||
|
@ -619,12 +619,12 @@ class SlotSchema(BaseModel):
|
||||
|
||||
|
||||
class SampleUpdate(BaseModel):
|
||||
sample_name: Optional[str]
|
||||
proteinname: Optional[str]
|
||||
priority: Optional[int]
|
||||
position: Optional[int]
|
||||
comments: Optional[str]
|
||||
data_collection_parameters: Optional[DataCollectionParameters]
|
||||
sample_name: Optional[str] = None
|
||||
proteinname: Optional[str] = None
|
||||
priority: Optional[int] = None
|
||||
position: Optional[int] = None
|
||||
comments: Optional[str] = None
|
||||
data_collection_parameters: Optional[DataCollectionParameters] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DataGrid, GridColDef, GridCellEditStopParams } from "@mui/x-data-grid";
|
||||
import { DataGrid, GridColDef, GridCellEditStopParams, GridRowModel } from "@mui/x-data-grid";
|
||||
import { DewarsService } from "../../openapi";
|
||||
|
||||
interface SampleSpreadsheetProps {
|
||||
@ -34,6 +34,24 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
totalrange,
|
||||
transmission,
|
||||
dose,
|
||||
targetresolution,
|
||||
aperture,
|
||||
datacollectiontype,
|
||||
processingpipeline,
|
||||
spacegroupnumber,
|
||||
cellparameters,
|
||||
rescutkey,
|
||||
rescutvalue,
|
||||
userresolution,
|
||||
pdbid,
|
||||
autoprocfull,
|
||||
procfull,
|
||||
adpenabled,
|
||||
noano,
|
||||
ffcscampaig,
|
||||
trustedhigh,
|
||||
autoprocextraparams,
|
||||
chiphiangles
|
||||
} = sample.data_collection_parameters || {};
|
||||
|
||||
allRows.push({
|
||||
@ -53,7 +71,24 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
totalRange: totalrange || null,
|
||||
transmission: transmission || null,
|
||||
dose: dose || null,
|
||||
dataCollectionParameters: sample.data_collection_parameters || {}, // Ensure this is never undefined
|
||||
targetresolution: targetresolution || null,
|
||||
aperture: aperture || null,
|
||||
datacollectiontype: datacollectiontype || null,
|
||||
processingpipeline: processingpipeline || null,
|
||||
spacegroupnumber: spacegroupnumber || null,
|
||||
cellparameters: cellparameters || {},
|
||||
rescutkey: rescutkey || null,
|
||||
rescutvalue: rescutvalue || null,
|
||||
userresolution: userresolution || null,
|
||||
pdbid: pdbid || null,
|
||||
autoprocfull: autoprocfull || null,
|
||||
procfull: procfull || null,
|
||||
adpenabled: adpenabled || null,
|
||||
noano: noano || null,
|
||||
ffcscampaig: ffcscampaig || null,
|
||||
trustedhigh: trustedhigh || null,
|
||||
autoprocextraparams: autoprocextraparams || {},
|
||||
chiphiangles: chiphiangles || null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -62,30 +97,38 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
|
||||
// Define columns for the grid
|
||||
setColumns([
|
||||
{ field: "dewarName", headerName: "Dewar Name", width: 150, editable: false },
|
||||
{ field: "puckName", headerName: "Puck Name", width: 150 },
|
||||
{ field: "puckType", headerName: "Puck Type", width: 150 },
|
||||
{ field: "crystalName", headerName: "Crystal Name", width: 200, editable: true },
|
||||
{ field: "proteinName", headerName: "Protein Name", width: 200, editable: true },
|
||||
{ field: "dewarName", headerName: "Dewar Name", width: 150, editable: false }, // not editable for now
|
||||
{ field: "puckName", headerName: "Puck Name", width: 150, editable: false }, // not editable for now
|
||||
{ field: "puckType", headerName: "Puck Type", width: 150, editable: false }, // not editable for now
|
||||
{ field: "crystalName", headerName: "Crystal Name", width: 200, editable: false }, // not editable for now
|
||||
{ field: "proteinName", headerName: "Protein Name", width: 200, editable: false }, // not editable for now
|
||||
{ field: "position", headerName: "Position", width: 100, editable: true, type: "number" },
|
||||
{ field: "priority", headerName: "Priority", width: 100, editable: true, type: "number" },
|
||||
{ field: "comments", headerName: "Comments", width: 300, editable: true },
|
||||
{ field: "directory", headerName: "Directory", width: 200 },
|
||||
{ field: "directory", headerName: "Directory", width: 200, editable: true },
|
||||
{ field: "oscillation", headerName: "Oscillation", width: 150, editable: true, type: "number" },
|
||||
{ field: "exposure", headerName: "Exposure", width: 150, editable: true, type: "number" },
|
||||
{ field: "totalRange", headerName: "Total Range", width: 150, editable: true, type: "number" },
|
||||
{ field: "transmission", headerName: "Transmission", width: 150, editable: true, type: "number" },
|
||||
{ field: "dose", headerName: "Dose", width: 150, editable: true, type: "number" },
|
||||
{
|
||||
field: "dataCollectionParameters",
|
||||
headerName: "Data Collection Parameters (Raw)",
|
||||
width: 300,
|
||||
valueGetter: (params) =>
|
||||
params.row?.dataCollectionParameters
|
||||
? JSON.stringify(params.row.dataCollectionParameters)
|
||||
: "N/A", // Fallback if undefined
|
||||
editable: false,
|
||||
},
|
||||
{ field: "targetresolution", headerName: "Target Resolution", width: 150, editable: true, type: "number" },
|
||||
{ field: "aperture", headerName: "Aperture", width: 150, editable: true },
|
||||
{ field: "datacollectiontype", headerName: "Data Collection Type", width: 200, editable: true },
|
||||
{ field: "processingpipeline", headerName: "Processing Pipeline", width: 200, editable: true },
|
||||
{ field: "spacegroupnumber", headerName: "Space Group Number", width: 200, editable: true },
|
||||
{ field: "cellparameters", headerName: "Cell Parameters", width: 300, editable: true },
|
||||
{ field: "rescutkey", headerName: "ResCut Key", width: 150, editable: true },
|
||||
{ field: "rescutvalue", headerName: "ResCut Value", width: 150, editable: true, editable: true },
|
||||
{ field: "userresolution", headerName: "User Resolution", width: 150, editable: true, editable: true },
|
||||
{ field: "pdbid", headerName: "PDB ID", width: 150, editable: true },
|
||||
{ field: "autoprocfull", headerName: "AutoProc Full", width: 200, editable: true },
|
||||
{ field: "procfull", headerName: "Proc Full", width: 200, editable: true },
|
||||
{ field: "adpenabled", headerName: "ADP Enabled", width: 150, editable: true },
|
||||
{ field: "noano", headerName: "No Anomalous", width: 150, editable: true },
|
||||
{ field: "ffcscampaig", headerName: "FFCS Campaign", width: 150, editable: true },
|
||||
{ field: "trustedhigh", headerName: "Trusted High", width: 150, editable: true },
|
||||
{ field: "autoprocextraparams", headerName: "AutoProc Extra Params", width: 300, editable: true },
|
||||
{ field: "chiphiangles", headerName: "Chi Phi Angles", width: 150, editable: true },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dewar samples:", error);
|
||||
@ -98,19 +141,112 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
// Handle cell editing to persist changes to the backend
|
||||
const handleCellEditStop = async (params: GridCellEditStopParams) => {
|
||||
const { id, field, value } = params;
|
||||
|
||||
// Validation to ensure we have valid data from the cell edit
|
||||
if (!id || !field || value === undefined) {
|
||||
console.error("Invalid edit inputs");
|
||||
console.error("Invalid edit inputs:", { id, field, value });
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the current row data (old state) based on its ID
|
||||
const updatedRow = rows.find((row) => row.id === id);
|
||||
|
||||
if (!updatedRow) {
|
||||
console.error("Row not found for ID:", id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the updated sample, force-overwriting the changed cell value
|
||||
const updatedSample = {
|
||||
...updatedRow, // Include other fields from the existing row
|
||||
[field]: value, // Explicitly overwrite the updated field with new value
|
||||
};
|
||||
|
||||
console.log("Payload sent to the backend:", updatedSample); // Log fixed payload
|
||||
|
||||
try {
|
||||
// Call the update_sample API endpoint
|
||||
await DewarsService.updateSampleSampleIdPut(id as number, {
|
||||
[field]: value,
|
||||
});
|
||||
console.log("Sample updated successfully");
|
||||
// Optimistically update UI for better experience
|
||||
setRows((prevRows) =>
|
||||
prevRows.map((row) =>
|
||||
row.id === id ? { ...row, [field]: value } : row
|
||||
)
|
||||
);
|
||||
|
||||
// API call to persist changes
|
||||
await DewarsService.updateSampleDewarsSamplesSampleIdPut(Number(id), updatedSample);
|
||||
|
||||
console.log(`Sample with ID ${id} successfully updated.`);
|
||||
} catch (error) {
|
||||
console.error(`Error updating sample (id: ${id}):`, error);
|
||||
console.error(`Failed to update sample with ID ${id}:`, error);
|
||||
|
||||
// Revert optimistic update on error
|
||||
setRows((prevRows) =>
|
||||
prevRows.map((row) => (row.id === id ? updatedRow : row))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const processRowUpdate = async (newRow: GridRowModel, oldRow: GridRowModel) => {
|
||||
try {
|
||||
console.log("Old row:", oldRow); // Log the original row
|
||||
console.log("Updated row:", newRow); // Log the updated data from the grid
|
||||
|
||||
// Reconstruct 'data_collection_parameters' from the flat table structure
|
||||
const updatedDataCollectionParameters = {
|
||||
...(oldRow.data_collection_parameters || {}), // Preserve old values
|
||||
directory: newRow.directory ?? oldRow.data_collection_parameters?.directory,
|
||||
oscillation: newRow.oscillation ?? oldRow.data_collection_parameters?.oscillation,
|
||||
exposure: newRow.exposure ?? oldRow.data_collection_parameters?.exposure,
|
||||
totalrange: newRow.totalRange ?? oldRow.data_collection_parameters?.totalrange,
|
||||
transmission: newRow.transmission ?? oldRow.data_collection_parameters?.transmission,
|
||||
dose: newRow.dose ?? oldRow.data_collection_parameters?.dose,
|
||||
targetresolution: newRow.targetresolution ?? oldRow.data_collection_parameters?.targetresolution,
|
||||
aperture: newRow.aperture ?? oldRow.data_collection_parameters?.aperture,
|
||||
datacollectiontype: newRow.datacollectiontype ?? oldRow.data_collection_parameters?.datacollectiontype,
|
||||
processingpipeline: newRow.processingpipeline ?? oldRow.data_collection_parameters?.processingpipeline,
|
||||
spacegroupnumber: newRow.spacegroupnumber ?? oldRow.data_collection_parameters?.spacegroupnumber,
|
||||
//cellparameters: newRow.cellparameters ?? oldRow.data_collection_parameters?.cellparameters,
|
||||
rescutkey: newRow.rescutkey ?? oldRow.data_collection_parameters?.rescutkey,
|
||||
rescutvalue: newRow.rescutvalue ?? oldRow.data_collection_parameters?.rescutvalue,
|
||||
userresolution: newRow.userresolution ?? oldRow.data_collection_parameters?.userresolution,
|
||||
pdbid: newRow.pdbid ?? oldRow.data_collection_parameters?.pdbid,
|
||||
autoprocfull: newRow.autoprocfull ?? oldRow.data_collection_parameters?.autoprocfull,
|
||||
procfull: newRow.procfull ?? oldRow.data_collection_parameters?.procfull,
|
||||
adpenabled: newRow.adpenabled ?? oldRow.data_collection_parameters?.adpenabled,
|
||||
noano: newRow.noano ?? oldRow.data_collection_parameters?.noano,
|
||||
ffcscampaig: newRow.ffcscampaig ?? oldRow.data_collection_parameters?.ffcscampaig,
|
||||
trustedhigh: newRow.trustedhigh ?? oldRow.data_collection_parameters?.trustedhigh,
|
||||
//autoprocextraparams: newRow.autoprocextraparams ?? oldRow.data_collection_parameters?.autoprocextraparams,
|
||||
chiphiangles: newRow.chiphiangles ?? oldRow.data_collection_parameters?.chiphiangles,
|
||||
};
|
||||
|
||||
// Assemble the final payload
|
||||
const payload = {
|
||||
...oldRow, // Include all original fields
|
||||
...newRow, // Overwrite or add updated fields
|
||||
data_collection_parameters: updatedDataCollectionParameters, // Include the merged/validated structure
|
||||
};
|
||||
|
||||
console.log("Final payload sent to backend:", payload);
|
||||
|
||||
// Optimistically update the UI
|
||||
setRows((prevRows) =>
|
||||
prevRows.map((row) =>
|
||||
row.id === newRow.id ? { ...row, ...payload } : row
|
||||
)
|
||||
);
|
||||
|
||||
// Send the payload to the backend
|
||||
await DewarsService.updateSampleDewarsSamplesSampleIdPut(Number(newRow.id), payload);
|
||||
|
||||
console.log(`Successfully updated sample with ID ${newRow.id}.`);
|
||||
return payload; // Return the updated row
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to update sample with ID ${newRow.id}:`, error);
|
||||
|
||||
// On failure, revert to the old row
|
||||
return oldRow;
|
||||
}
|
||||
};
|
||||
|
||||
@ -120,7 +256,7 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
pageSize={10}
|
||||
onCellEditStop={handleCellEditStop}
|
||||
processRowUpdate={processRowUpdate} // Exclusively handle updates
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -3506,8 +3506,8 @@
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-01-09T19:33:01.143326Z",
|
||||
"start_time": "2025-01-09T19:33:01.128023Z"
|
||||
"end_time": "2025-01-09T19:41:14.264895Z",
|
||||
"start_time": "2025-01-09T19:41:14.245643Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@ -3518,9 +3518,9 @@
|
||||
" api_instance = aareDBclient.SamplesApi(api_client)\n",
|
||||
"\n",
|
||||
" # Define the sample ID and event payload using the expected model\n",
|
||||
" sample_id = 260\n",
|
||||
" sample_id = 261\n",
|
||||
" event_payload = SampleEventCreate(\n",
|
||||
" event_type=\"Unmounted\" # Replace with actual event type if different\n",
|
||||
" event_type=\"Failed\" # Replace with actual event type if different\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
@ -3543,7 +3543,7 @@
|
||||
"text": [
|
||||
"The response of post_sample_event:\n",
|
||||
"\n",
|
||||
"SampleEventResponse(id=417, sample_id=260, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 9, 20, 33, 1))\n"
|
||||
"SampleEventResponse(id=418, sample_id=261, event_type='Failed', timestamp=datetime.datetime(2025, 1, 9, 20, 41, 14))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -3555,13 +3555,13 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 67
|
||||
"execution_count": 70
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-01-09T19:33:04.470152Z",
|
||||
"start_time": "2025-01-09T19:33:04.454152Z"
|
||||
"end_time": "2025-01-09T19:41:23.052434Z",
|
||||
"start_time": "2025-01-09T19:41:23.036108Z"
|
||||
}
|
||||
},
|
||||
"cell_type": "code",
|
||||
@ -3573,7 +3573,7 @@
|
||||
"\n",
|
||||
" try:\n",
|
||||
" # Get the last sample event\n",
|
||||
" last_event_response = api_instance.get_last_sample_event_samples_samples_sample_id_events_last_get(260)\n",
|
||||
" last_event_response = api_instance.get_last_sample_event_samples_samples_sample_id_events_last_get(261)\n",
|
||||
" print(\"The response of get_last_sample_event:\\n\")\n",
|
||||
" pprint(last_event_response)\n",
|
||||
"\n",
|
||||
@ -3588,7 +3588,7 @@
|
||||
"text": [
|
||||
"The response of get_last_sample_event:\n",
|
||||
"\n",
|
||||
"SampleEventResponse(id=417, sample_id=260, event_type='Unmounted', timestamp=datetime.datetime(2025, 1, 9, 20, 33, 1))\n"
|
||||
"SampleEventResponse(id=418, sample_id=261, event_type='Failed', timestamp=datetime.datetime(2025, 1, 9, 20, 41, 14))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -3600,7 +3600,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"execution_count": 68
|
||||
"execution_count": 71
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user