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:
parent
ae20d6112a
commit
9bfcc30981
@ -17,7 +17,12 @@ from app.schemas import (
|
|||||||
DewarTypeCreate,
|
DewarTypeCreate,
|
||||||
DewarSerialNumber as DewarSerialNumberSchema,
|
DewarSerialNumber as DewarSerialNumberSchema,
|
||||||
DewarSerialNumberCreate,
|
DewarSerialNumberCreate,
|
||||||
Shipment as ShipmentSchema, # Clearer name for schema
|
Shipment as ShipmentSchema,
|
||||||
|
Dewar,
|
||||||
|
SampleUpdate,
|
||||||
|
Sample,
|
||||||
|
Puck,
|
||||||
|
SampleEventResponse, # Clearer name for schema
|
||||||
)
|
)
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Dewar as DewarModel,
|
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)):
|
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()
|
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
|
||||||
if not dewar:
|
if not dewar:
|
||||||
raise HTTPException(status_code=404, detail="Dewar not found")
|
raise HTTPException(status_code=404, detail="Dewar not found")
|
||||||
|
|
||||||
pucks = db.query(PuckModel).filter(PuckModel.dewar_id == dewar.id).all()
|
# Explicitly map nested relationships
|
||||||
|
dewar_data = {
|
||||||
data = {"dewar": {"id": dewar.id, "dewar_name": dewar.dewar_name}, "pucks": []}
|
"id": dewar.id,
|
||||||
|
|
||||||
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,
|
"dewar_name": dewar.dewar_name,
|
||||||
"sample_name": sample.sample_name,
|
"tracking_number": dewar.tracking_number,
|
||||||
"priority": sample.priority,
|
"status": dewar.status,
|
||||||
"comments": sample.comments,
|
"number_of_pucks": len(dewar.pucks), # Calculate number of pucks
|
||||||
"proteinname": sample.proteinname,
|
"number_of_samples": sum(
|
||||||
**(sample.data_collection_parameters or {}),
|
len(puck.samples) for puck in dewar.pucks
|
||||||
}
|
), # Calculate total samples
|
||||||
for sample in 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])
|
@router.get("/", response_model=List[DewarSchema])
|
||||||
|
@ -34,8 +34,7 @@ def normalize_puck_name(name: str) -> str:
|
|||||||
"""
|
"""
|
||||||
Normalize a puck_name to remove special characters and ensure consistent formatting.
|
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.upper()) # Remove special characters
|
||||||
name = re.sub(r"[^A-Z0-9]", "", name) # Remove special characters
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
@ -50,22 +49,11 @@ async def set_tell_positions(
|
|||||||
):
|
):
|
||||||
results = []
|
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
|
# Retrieve all pucks in the database with their most recent
|
||||||
# `tell_position_set` event
|
# `tell_position_set` event
|
||||||
all_pucks_with_last_event = (
|
all_pucks_with_last_event = (
|
||||||
db.query(PuckModel, PuckEventModel)
|
db.query(PuckModel, PuckEventModel)
|
||||||
.outerjoin(PuckEventModel, PuckEventModel.puck_id == PuckModel.id)
|
.outerjoin(PuckEventModel, PuckEventModel.puck_id == PuckModel.id)
|
||||||
.filter(PuckEventModel.event_type == "tell_position_set")
|
|
||||||
.order_by(PuckEventModel.puck_id, PuckEventModel.timestamp.desc())
|
.order_by(PuckEventModel.puck_id, PuckEventModel.timestamp.desc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
@ -423,7 +423,10 @@ class Sample(BaseModel):
|
|||||||
priority: Optional[int] = None
|
priority: Optional[int] = None
|
||||||
comments: Optional[str] = None
|
comments: Optional[str] = None
|
||||||
data_collection_parameters: Optional[DataCollectionParameters]
|
data_collection_parameters: Optional[DataCollectionParameters]
|
||||||
events: List[SampleEventCreate] = []
|
events: List[SampleEventResponse] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class SampleCreate(BaseModel):
|
class SampleCreate(BaseModel):
|
||||||
@ -501,6 +504,9 @@ class DewarBase(BaseModel):
|
|||||||
return_address_id: Optional[int]
|
return_address_id: Optional[int]
|
||||||
pucks: List[PuckCreate] = []
|
pucks: List[PuckCreate] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class DewarCreate(DewarBase):
|
class DewarCreate(DewarBase):
|
||||||
pass
|
pass
|
||||||
@ -612,6 +618,18 @@ class SlotSchema(BaseModel):
|
|||||||
from_attributes = True
|
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):
|
class SetTellPosition(BaseModel):
|
||||||
puck_name: str
|
puck_name: str
|
||||||
segment: Optional[str] = Field(
|
segment: Optional[str] = Field(
|
||||||
|
@ -139,8 +139,8 @@ def on_startup():
|
|||||||
load_slots_data(db)
|
load_slots_data(db)
|
||||||
else: # dev or test environments
|
else: # dev or test environments
|
||||||
print(f"{environment.capitalize()} environment: Regenerating database.")
|
print(f"{environment.capitalize()} environment: Regenerating database.")
|
||||||
Base.metadata.drop_all(bind=engine)
|
# Base.metadata.drop_all(bind=engine)
|
||||||
Base.metadata.create_all(bind=engine)
|
# Base.metadata.create_all(bind=engine)
|
||||||
if environment == "dev":
|
if environment == "dev":
|
||||||
from app.database import load_sample_data
|
from app.database import load_sample_data
|
||||||
|
|
||||||
|
@ -20,48 +20,47 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
|||||||
const fetchSamples = async () => {
|
const fetchSamples = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId);
|
const response = await DewarsService.getDewarSamplesDewarsDewarsDewarIdSamplesGet(dewarId);
|
||||||
|
console.log("Response from backend:", response);
|
||||||
const dewarData = response; // Assume response is already in correct structure
|
const dewarData = response; // Assume response is already in correct structure
|
||||||
|
|
||||||
// Transform pucks and samples into rows for the grid
|
// Transform pucks and samples into rows for the grid
|
||||||
const allRows = [];
|
const allRows: any[] = [];
|
||||||
dewarData.pucks.forEach((puck: any) => {
|
dewarData.pucks.forEach((puck: any) => {
|
||||||
puck.samples.forEach((sample: any) => {
|
puck.samples.forEach((sample: any) => {
|
||||||
|
const {
|
||||||
|
directory,
|
||||||
|
oscillation,
|
||||||
|
exposure,
|
||||||
|
totalrange,
|
||||||
|
transmission,
|
||||||
|
dose,
|
||||||
|
} = sample.data_collection_parameters || {};
|
||||||
|
|
||||||
allRows.push({
|
allRows.push({
|
||||||
id: sample.id,
|
id: sample.id,
|
||||||
puckId: puck.id,
|
puckId: puck.id,
|
||||||
puckName: puck.name,
|
puckName: puck.puck_name,
|
||||||
puckType: puck.type,
|
puckType: puck.puck_type,
|
||||||
dewarName: dewarData.dewar.dewar_name,
|
dewarName: dewarData.dewar_name,
|
||||||
position: sample.position,
|
position: sample.position,
|
||||||
crystalName: sample.sample_name,
|
crystalName: sample.sample_name,
|
||||||
proteinName: sample.proteinname,
|
proteinName: sample.proteinname,
|
||||||
priority: sample.priority,
|
priority: sample.priority,
|
||||||
comments: sample.comments,
|
comments: sample.comments,
|
||||||
directory: sample.directory,
|
directory: directory || null,
|
||||||
oscillation: sample.oscillation,
|
oscillation: oscillation || null,
|
||||||
aperture: sample.aperture,
|
exposure: exposure || null,
|
||||||
exposure: sample.exposure,
|
totalRange: totalrange || null,
|
||||||
totalRange: sample.totalrange,
|
transmission: transmission || null,
|
||||||
transmission: sample.transmission,
|
dose: dose || null,
|
||||||
dose: sample.dose,
|
dataCollectionParameters: sample.data_collection_parameters || {}, // Ensure this is never undefined
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
console.log("All rows for DataGrid:", allRows);
|
||||||
setRows(allRows);
|
setRows(allRows);
|
||||||
|
|
||||||
|
// Define columns for the grid
|
||||||
setColumns([
|
setColumns([
|
||||||
{ field: "dewarName", headerName: "Dewar Name", width: 150, editable: false },
|
{ field: "dewarName", headerName: "Dewar Name", width: 150, editable: false },
|
||||||
{ field: "puckName", headerName: "Puck Name", width: 150 },
|
{ field: "puckName", headerName: "Puck Name", width: 150 },
|
||||||
@ -73,28 +72,25 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
|||||||
{ field: "comments", headerName: "Comments", width: 300, editable: true },
|
{ field: "comments", headerName: "Comments", width: 300, editable: true },
|
||||||
{ field: "directory", headerName: "Directory", width: 200 },
|
{ field: "directory", headerName: "Directory", width: 200 },
|
||||||
{ field: "oscillation", headerName: "Oscillation", width: 150, editable: true, type: "number" },
|
{ 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: "exposure", headerName: "Exposure", width: 150, editable: true, type: "number" },
|
||||||
{ field: "totalRange", headerName: "Total Range", 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: "transmission", headerName: "Transmission", width: 150, editable: true, type: "number" },
|
||||||
{ field: "dose", headerName: "Dose", 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: "dataCollectionParameters",
|
||||||
{ field: "processingpipeline", headerName: "Processing Pipeline", width: 200 },
|
headerName: "Data Collection Parameters (Raw)",
|
||||||
{ field: "spacegroupnumber", headerName: "Space Group Number", width: 150, type: "number" },
|
width: 300,
|
||||||
{ field: "cellparameters", headerName: "Cell Parameters", width: 200 },
|
valueGetter: (params) =>
|
||||||
{ field: "rescutkey", headerName: "Rescut Key", width: 150 },
|
params.row?.dataCollectionParameters
|
||||||
{ field: "pdbid", headerName: "PDB ID", width: 150 },
|
? JSON.stringify(params.row.dataCollectionParameters)
|
||||||
{ field: "autoprocfull", headerName: "Auto Proc Full", width: 150 },
|
: "N/A", // Fallback if undefined
|
||||||
{ field: "procfull", headerName: "Proc Full", width: 150 },
|
editable: false,
|
||||||
{ 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) {
|
} catch (error) {
|
||||||
console.error("Error fetching dewar samples:", error);
|
console.error("Error fetching dewar samples:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchSamples();
|
fetchSamples();
|
||||||
}, [dewarId]);
|
}, [dewarId]);
|
||||||
@ -102,12 +98,19 @@ const SampleSpreadsheet: React.FC<SampleSpreadsheetProps> = ({ dewarId }) => {
|
|||||||
// Handle cell editing to persist changes to the backend
|
// Handle cell editing to persist changes to the backend
|
||||||
const handleCellEditStop = async (params: GridCellEditStopParams) => {
|
const handleCellEditStop = async (params: GridCellEditStopParams) => {
|
||||||
const { id, field, value } = params;
|
const { id, field, value } = params;
|
||||||
|
if (!id || !field || value === undefined) {
|
||||||
|
console.error("Invalid edit inputs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Example: Replace with a proper OpenAPI call if available
|
// Call the update_sample API endpoint
|
||||||
await DewarsService.updateSampleData(id as number, { [field]: value }); // Assuming this exists
|
await DewarsService.updateSampleSampleIdPut(id as number, {
|
||||||
console.log("Updated successfully");
|
[field]: value,
|
||||||
|
});
|
||||||
|
console.log("Sample updated successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving data:", error);
|
console.error(`Error updating sample (id: ${id}):`, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "aareDB"
|
name = "aareDB"
|
||||||
version = "0.1.0a14"
|
version = "0.1.0a15"
|
||||||
description = "Backend for next gen sample management system"
|
description = "Backend for next gen sample management system"
|
||||||
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
|
authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}]
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user