Refactor dewar and sample handling; improve grid data binding

Updated Dewar and Sample schemas, added nested relationships, and adjusted API responses for better data handling. Simplified puck normalization, enhanced data grid logic in the frontend, and implemented a PUT endpoint for updating samples. Incremented backend version to 0.1.0a15 and added new HTTP request example.
This commit is contained in:
GotthardG 2025-01-09 13:01:52 +01:00
parent ae20d6112a
commit 9bfcc30981
7 changed files with 188 additions and 124 deletions

View File

@ -17,7 +17,12 @@ from app.schemas import (
DewarTypeCreate,
DewarSerialNumber as DewarSerialNumberSchema,
DewarSerialNumberCreate,
Shipment as ShipmentSchema, # Clearer name for schema
Shipment as ShipmentSchema,
Dewar,
SampleUpdate,
Sample,
Puck,
SampleEventResponse, # Clearer name for schema
)
from app.models import (
Dewar as DewarModel,
@ -308,41 +313,86 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
)
@router.get("/dewars/{dewar_id}/samples", response_model=dict)
@router.get("/dewars/{dewar_id}/samples", response_model=Dewar)
async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
# Fetch Dewar, associated Pucks, and Samples
# Fetch the Dewar with nested relationships
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
pucks = db.query(PuckModel).filter(PuckModel.dewar_id == dewar.id).all()
data = {"dewar": {"id": dewar.id, "dewar_name": dewar.dewar_name}, "pucks": []}
for puck in pucks:
samples = db.query(SampleModel).filter(SampleModel.puck_id == puck.id).all()
data["pucks"].append(
{
"id": puck.id,
"name": puck.puck_name,
"type": puck.puck_type,
"samples": [
{
"id": sample.id,
"position": sample.position,
"dewar_name": dewar.dewar_name,
"sample_name": sample.sample_name,
"priority": sample.priority,
"comments": sample.comments,
"proteinname": sample.proteinname,
**(sample.data_collection_parameters or {}),
}
for sample in samples
# Explicitly map nested relationships
dewar_data = {
"id": dewar.id,
"dewar_name": dewar.dewar_name,
"tracking_number": dewar.tracking_number,
"status": dewar.status,
"number_of_pucks": len(dewar.pucks), # Calculate number of pucks
"number_of_samples": sum(
len(puck.samples) for puck in dewar.pucks
), # Calculate total samples
"ready_date": dewar.ready_date,
"shipping_date": dewar.shipping_date,
"arrival_date": dewar.arrival_date,
"returning_date": dewar.returning_date,
"contact_person_id": dewar.contact_person.id if dewar.contact_person else None,
"return_address_id": dewar.return_address.id if dewar.return_address else None,
"shipment_id": dewar.shipment_id,
"contact_person": dewar.contact_person,
"return_address": dewar.return_address,
"pucks": [
Puck(
id=puck.id,
puck_name=puck.puck_name,
puck_type=puck.puck_type,
puck_location_in_dewar=puck.puck_location_in_dewar,
dewar_id=dewar.id,
samples=[
Sample(
id=sample.id,
sample_name=sample.sample_name,
position=sample.position,
puck_id=sample.puck_id,
crystalname=getattr(sample, "crystalname", None),
proteinname=getattr(sample, "proteinname", None),
positioninpuck=getattr(sample, "positioninpuck", None),
priority=sample.priority,
comments=sample.comments,
data_collection_parameters=sample.data_collection_parameters,
events=[
SampleEventResponse.from_orm(event)
for event in sample.events
],
)
for sample in puck.samples
],
}
)
)
for puck in dewar.pucks
],
}
return data
# Return the Pydantic Dewar model
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])

View File

@ -34,8 +34,7 @@ def normalize_puck_name(name: str) -> str:
"""
Normalize a puck_name to remove special characters and ensure consistent formatting.
"""
name = str(name).strip().replace(" ", "_").upper()
name = re.sub(r"[^A-Z0-9]", "", name) # Remove special characters
name = re.sub(r"[^A-Z0-9]", "", name.upper()) # Remove special characters
return name
@ -50,22 +49,11 @@ async def set_tell_positions(
):
results = []
# Helper function to normalize puck names for database querying
def normalize_puck_name(name: str) -> str:
return str(name).strip().replace(" ", "_").upper()
if not pucks:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Payload cannot be empty. Provide at least one puck.",
)
# Retrieve all pucks in the database with their most recent
# `tell_position_set` event
all_pucks_with_last_event = (
db.query(PuckModel, PuckEventModel)
.outerjoin(PuckEventModel, PuckEventModel.puck_id == PuckModel.id)
.filter(PuckEventModel.event_type == "tell_position_set")
.order_by(PuckEventModel.puck_id, PuckEventModel.timestamp.desc())
.all()
)

View File

@ -423,7 +423,10 @@ class Sample(BaseModel):
priority: Optional[int] = None
comments: Optional[str] = None
data_collection_parameters: Optional[DataCollectionParameters]
events: List[SampleEventCreate] = []
events: List[SampleEventResponse] = []
class Config:
from_attributes = True
class SampleCreate(BaseModel):
@ -501,6 +504,9 @@ class DewarBase(BaseModel):
return_address_id: Optional[int]
pucks: List[PuckCreate] = []
class Config:
from_attributes = True
class DewarCreate(DewarBase):
pass
@ -612,6 +618,18 @@ class SlotSchema(BaseModel):
from_attributes = True
class SampleUpdate(BaseModel):
sample_name: Optional[str]
proteinname: Optional[str]
priority: Optional[int]
position: Optional[int]
comments: Optional[str]
data_collection_parameters: Optional[DataCollectionParameters]
class Config:
from_attributes = True
class SetTellPosition(BaseModel):
puck_name: str
segment: Optional[str] = Field(

View File

@ -139,8 +139,8 @@ def on_startup():
load_slots_data(db)
else: # dev or test environments
print(f"{environment.capitalize()} environment: Regenerating database.")
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
# Base.metadata.drop_all(bind=engine)
# Base.metadata.create_all(bind=engine)
if environment == "dev":
from app.database import load_sample_data

View File

@ -18,83 +18,79 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
}
const fetchSamples = async () => {
try {
const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId);
const dewarData = response; // Assume response is already in correct structure
try {
const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId);
console.log("Response from backend:", response);
const dewarData = response; // Assume response is already in correct structure
// Transform pucks and samples into rows for the grid
const allRows = [];
dewarData.pucks.forEach((puck: any) => {
puck.samples.forEach((sample: any) => {
allRows.push({
id: sample.id,
puckId: puck.id,
puckName: puck.name,
puckType: puck.type,
dewarName: dewarData.dewar.dewar_name,
position: sample.position,
crystalName: sample.sample_name,
proteinName: sample.proteinname,
priority: sample.priority,
comments: sample.comments,
directory: sample.directory,
oscillation: sample.oscillation,
aperture: sample.aperture,
exposure: sample.exposure,
totalRange: sample.totalrange,
transmission: sample.transmission,
dose: sample.dose,
targetResolution: sample.targetresolution,
datacollectiontype: sample.datacollectiontype,
processingpipeline: sample.processingpipeline,
spacegroupnumber: sample.spacegroupnumber,
cellparameters: sample.cellparameters,
rescutkey: sample.rescutkey,
//rescutvalues: sample.rescutvalues,
pdbid: sample.pdbid,
autoprocfull: sample.autoprocfull,
procfull: sample.procfull,
adpenabled: sample.adpenabled,
noano: sample.noano,
ffcscampaign: sample.ffcscampaign,
// Transform pucks and samples into rows for the grid
const allRows: any[] = [];
dewarData.pucks.forEach((puck: any) => {
puck.samples.forEach((sample: any) => {
const {
directory,
oscillation,
exposure,
totalrange,
transmission,
dose,
} = sample.data_collection_parameters || {};
allRows.push({
id: sample.id,
puckId: puck.id,
puckName: puck.puck_name,
puckType: puck.puck_type,
dewarName: dewarData.dewar_name,
position: sample.position,
crystalName: sample.sample_name,
proteinName: sample.proteinname,
priority: sample.priority,
comments: sample.comments,
directory: directory || null,
oscillation: oscillation || null,
exposure: exposure || null,
totalRange: totalrange || null,
transmission: transmission || null,
dose: dose || null,
dataCollectionParameters: sample.data_collection_parameters || {}, // Ensure this is never undefined
});
});
});
});
});
setRows(allRows);
console.log("All rows for DataGrid:", allRows);
setRows(allRows);
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: "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: "oscillation", headerName: "Oscillation", width: 150, editable: true, type: "number" },
{ field: "aperture", headerName: "Aperture", 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: "targetResolution", headerName: "Target Resolution", width: 200, editable: true, type: "number" },
{ field: "datacollectiontype", headerName: "Data Collection Type", width: 200 },
{ field: "processingpipeline", headerName: "Processing Pipeline", width: 200 },
{ field: "spacegroupnumber", headerName: "Space Group Number", width: 150, type: "number" },
{ field: "cellparameters", headerName: "Cell Parameters", width: 200 },
{ field: "rescutkey", headerName: "Rescut Key", width: 150 },
{ field: "pdbid", headerName: "PDB ID", width: 150 },
{ field: "autoprocfull", headerName: "Auto Proc Full", width: 150 },
{ field: "procfull", headerName: "Proc Full", width: 150 },
{ field: "adpenabled", headerName: "ADP Enabled", width: 150, editable: true, type: "boolean" },
{ field: "noano", headerName: "No Ano", width: 150, editable: true, type: "boolean" },
{ field: "ffcscampaign", headerName: "FFCS Campaign", width: 200 },
]);
} catch (error) {
console.error("Error fetching dewar samples:", error);
}
};
// 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: "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: "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,
},
]);
} catch (error) {
console.error("Error fetching dewar samples:", error);
}
};
fetchSamples();
}, [dewarId]);
@ -102,12 +98,19 @@ 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;
if (!id || !field || value === undefined) {
console.error("Invalid edit inputs");
return;
}
try {
// Example: Replace with a proper OpenAPI call if available
await DewarsService.updateSampleData(id as number, { [field]: value }); // Assuming this exists
console.log("Updated successfully");
// Call the update_sample API endpoint
await DewarsService.updateSampleSampleIdPut(id as number, {
[field]: value,
});
console.log("Sample updated successfully");
} catch (error) {
console.error("Error saving data:", error);
console.error(`Error updating sample (id: ${id}):`, error);
}
};

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aareDB"
version = "0.1.0a14"
version = "0.1.0a15"
description = "Backend for next gen sample management system"
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
license = {text = "MIT"}

5
req.http Normal file
View File

@ -0,0 +1,5 @@
### GET request to example server
PUT https://127.0.0.1:8000/pucks/set-tell-positions
###