Refactor beamtime relationships in models and related APIs

Updated relationships for beamtime in models to support many-to-many associations with pucks, samples, and dewars. Refactored API endpoints to accommodate these changes, ensuring accurate assignment and retrieval of data. Improved sample data generation logic and incremented the application version for the new updates.
This commit is contained in:
GotthardG 2025-05-08 16:04:05 +02:00
parent 0fa038be94
commit 6a0953c913
12 changed files with 404 additions and 210 deletions

View File

@ -407,17 +407,43 @@ beamtimes = [
),
Beamtime(
id=2,
pgroups="p20003",
pgroups="p20001",
shift="afternoon",
beamtime_name="p20003-test",
beamtime_name="p20001-test",
beamline="X06DA",
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
start_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
end_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
status="confirmed",
comments="this is a test beamtime",
proposal_id=2,
local_contact_id=2,
),
Beamtime(
id=3,
pgroups="p20003",
shift="morning",
beamtime_name="p20003-test",
beamline="X06SA",
start_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
end_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
status="confirmed",
comments="this is a test beamtime",
proposal_id=1,
local_contact_id=1,
),
Beamtime(
id=4,
pgroups="p20002",
shift="night",
beamtime_name="p20002-test",
beamline="X06DA",
start_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
status="confirmed",
comments="this is a test beamtime",
proposal_id=3,
local_contact_id=2,
),
]
# Define shipments
@ -679,7 +705,8 @@ samples = []
sample_id_counter = 1
# Assign a beamtime to each dewar
dewar_to_beamtime = {
dewar.id: random.choice([1, 2]) for dewar in dewars # Or use actual beamtime ids
dewar.id: random.choice([1, 2, 3, 4])
for dewar in dewars # Or use actual beamtime ids
}
# Update dewars and their pucks with consistent beamtime
@ -688,10 +715,9 @@ for dewar in dewars:
for puck in pucks:
dewar_id = puck.dewar_id # Assuming puck has dewar_id
assigned_beamtime = dewar_to_beamtime[dewar_id]
puck.beamtime_id = (
assigned_beamtime # Associate puck to the same beamtime as its dewar
)
assigned_beamtime = dewar_to_beamtime[dewar_id] # this is the id (int)
# Fix here: use assigned_beamtime (which is the id)
assigned_beamtime_obj = next(b for b in beamtimes if b.id == assigned_beamtime)
positions_with_samples = random.randint(1, 16)
occupied_positions = random.sample(range(1, 17), positions_with_samples)
@ -703,11 +729,14 @@ for puck in pucks:
sample_name=f"Sample{sample_id_counter:03}",
position=pos,
puck_id=puck.id,
beamtime_id=assigned_beamtime,
)
sample.beamtimes.append(
assigned_beamtime_obj
) # assigned_beamtime_obj is a Beamtime instance
samples.append(sample)
sample_id_counter += 1
# Define possible event types for samples
event_types = ["Mounting", "Failed", "Unmounting", "Lost"]

View File

@ -9,6 +9,7 @@ from sqlalchemy import (
Boolean,
func,
Enum,
Table,
)
from sqlalchemy.orm import relationship
from .database import Base
@ -16,6 +17,26 @@ from datetime import datetime
import enum
dewar_beamtime_association = Table(
"dewar_beamtime_association",
Base.metadata,
Column("dewar_id", Integer, ForeignKey("dewars.id")),
Column("beamtime_id", Integer, ForeignKey("beamtimes.id")),
)
puck_beamtime_association = Table(
"puck_beamtime_association",
Base.metadata,
Column("puck_id", Integer, ForeignKey("pucks.id")),
Column("beamtime_id", Integer, ForeignKey("beamtimes.id")),
)
sample_beamtime_association = Table(
"sample_beamtime_association",
Base.metadata,
Column("sample_id", Integer, ForeignKey("samples.id")),
Column("beamtime_id", Integer, ForeignKey("beamtimes.id")),
)
class Shipment(Base):
__tablename__ = "shipments"
@ -120,8 +141,9 @@ class Dewar(Base):
beamline_location = None
local_contact_id = Column(Integer, ForeignKey("local_contacts.id"), nullable=True)
local_contact = relationship("LocalContact")
beamtime = relationship("Beamtime", back_populates="dewars")
beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True)
beamtimes = relationship(
"Beamtime", secondary=dewar_beamtime_association, back_populates="dewars"
)
@property
def number_of_pucks(self) -> int:
@ -155,9 +177,8 @@ class Puck(Base):
dewar = relationship("Dewar", back_populates="pucks")
samples = relationship("Sample", back_populates="puck")
events = relationship("PuckEvent", back_populates="puck")
beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True)
beamtime = relationship(
"Beamtime", back_populates="pucks", foreign_keys=[beamtime_id]
beamtimes = relationship(
"Beamtime", secondary=puck_beamtime_association, back_populates="pucks"
)
@ -178,8 +199,9 @@ class Sample(Base):
puck = relationship("Puck", back_populates="samples")
events = relationship("SampleEvent", back_populates="sample", lazy="joined")
images = relationship("Image", back_populates="sample", lazy="joined")
beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True)
beamtime = relationship("Beamtime", back_populates="samples")
beamtimes = relationship(
"Beamtime", secondary=sample_beamtime_association, back_populates="samples"
)
@property
def mount_count(self) -> int:
@ -262,9 +284,15 @@ class Beamtime(Base):
local_contact_id = Column(Integer, ForeignKey("local_contacts.id"), nullable=False)
local_contact = relationship("LocalContact")
dewars = relationship("Dewar", back_populates="beamtime")
pucks = relationship("Puck", back_populates="beamtime")
samples = relationship("Sample", back_populates="beamtime")
dewars = relationship(
"Dewar", secondary=dewar_beamtime_association, back_populates="beamtimes"
)
pucks = relationship(
"Puck", secondary=puck_beamtime_association, back_populates="beamtimes"
)
samples = relationship(
"Sample", secondary=sample_beamtime_association, back_populates="beamtimes"
)
class Image(Base):

View File

@ -36,6 +36,7 @@ from app.models import (
LogisticsEvent,
PuckEvent,
SampleEvent,
Beamtime as BeamtimeModel,
)
from app.dependencies import get_db
import qrcode
@ -598,18 +599,37 @@ async def assign_beamtime_to_dewar(
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
dewar.beamtime_id = None if beamtime_id == 0 else beamtime_id
# Find the Beamtime instance, if not unassigning
beamtime = (
db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first()
if beamtime_id
else None
)
if beamtime_id == 0:
dewar.beamtimes = []
else:
dewar.beamtimes = [
beamtime
] # assign one; append if you want to support multiple
db.commit()
db.refresh(dewar)
for puck in dewar.pucks:
puck.beamtime_id = None if beamtime_id == 0 else beamtime_id
if beamtime_id == 0:
puck.beamtimes = []
else:
puck.beamtimes = [beamtime]
for sample in puck.samples:
has_sample_event = (
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count()
> 0
)
if not has_sample_event:
sample.beamtime_id = None if beamtime_id == 0 else beamtime_id
if beamtime_id == 0:
sample.beamtimes = []
else:
sample.beamtimes = [beamtime]
db.commit()
return {"status": "success", "dewar_id": dewar.id, "beamtime_id": beamtime_id}
@ -726,5 +746,12 @@ async def get_single_shipment(id: int, db: Session = Depends(get_db)):
operation_id="get_dewars_by_beamtime",
)
async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)):
"""List all dewars assigned to a given beamtime."""
return db.query(DewarModel).filter(DewarModel.beamtime_id == beamtime_id).all()
beamtime = (
db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.dewars))
.filter(BeamtimeModel.id == beamtime_id)
.first()
)
if not beamtime:
raise HTTPException(status_code=404, detail="Beamtime not found")
return beamtime.dewars

View File

@ -1,6 +1,6 @@
from datetime import datetime
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.sql import func
from typing import List
import uuid
@ -21,6 +21,7 @@ from app.models import (
LogisticsEvent as LogisticsEventModel,
Dewar as DewarModel,
SampleEvent,
Beamtime as BeamtimeModel,
)
from app.dependencies import get_db
import logging
@ -664,23 +665,38 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
@router.patch("/puck/{puck_id}/assign-beamtime", operation_id="assignPuckToBeamtime")
async def assign_beamtime_to_puck(
puck_id: int,
beamtime_id: int, # If you want ?beamtime_id=123 in the query
beamtime_id: int, # expects ?beamtime_id=123 in the query
db: Session = Depends(get_db),
):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
puck.beamtime_id = None if beamtime_id == 0 else beamtime_id
beamtime = (
db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first()
if beamtime_id
else None
)
if beamtime_id == 0:
puck.beamtimes = []
else:
puck.beamtimes = [
beamtime
] # or use .append(beamtime) if you want to support multiple
db.commit()
db.refresh(puck)
# Update samples
# Update samples as well
for sample in puck.samples:
has_sample_event = (
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count() > 0
)
if not has_sample_event:
sample.beamtime_id = None if beamtime_id == 0 else beamtime_id
if beamtime_id == 0:
sample.beamtimes = []
else:
sample.beamtimes = [beamtime]
db.commit()
return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id}
@ -691,5 +707,12 @@ async def assign_beamtime_to_puck(
operation_id="get_pucks_by_beamtime",
)
async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)):
"""List all pucks assigned to a given beamtime."""
return db.query(PuckModel).filter(PuckModel.beamtime_id == beamtime_id).all()
beamtime = (
db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.pucks)) # eager load pucks
.filter(BeamtimeModel.id == beamtime_id)
.first()
)
if not beamtime:
raise HTTPException(status_code=404, detail="Beamtime not found")
return beamtime.pucks

View File

@ -32,6 +32,7 @@ from app.models import (
Results as ResultsModel,
Jobs as JobModel,
JobStatus,
Beamtime as BeamtimeModel,
)
from app.dependencies import get_db
import logging
@ -463,6 +464,7 @@ async def get_results_for_run_and_sample(
formatted_results = [
ResultResponse(
id=result.id,
status=result.status,
sample_id=result.sample_id,
run_id=result.run_id,
result=ProcessingResults(**result.result),
@ -479,5 +481,12 @@ async def get_results_for_run_and_sample(
operation_id="get_samples_by_beamtime",
)
async def get_samples_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)):
"""List all samples assigned to a given beamtime."""
return db.query(SampleModel).filter(SampleModel.beamtime_id == beamtime_id).all()
beamtime = (
db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.samples))
.filter(BeamtimeModel.id == beamtime_id)
.first()
)
if not beamtime:
raise HTTPException(status_code=404, detail="Beamtime not found")
return beamtime.samples

View File

@ -373,13 +373,13 @@ class Results(BaseModel):
resolution: float
unit_cell: str
spacegroup: str
rmerge: float
rmeas: float
isig: float
rmerge: List[CurvePoint]
rmeas: List[CurvePoint]
isig: List[CurvePoint]
cc: List[CurvePoint]
cchalf: List[CurvePoint]
completeness: float
multiplicity: float
completeness: List[CurvePoint]
multiplicity: List[CurvePoint]
nobs: int
total_refl: int
unique_refl: int
@ -478,6 +478,55 @@ class AddressMinimal(BaseModel):
id: int
class Beamtime(BaseModel):
id: int
pgroups: str
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[constr(max_length=200)] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class BeamtimeCreate(BaseModel):
pgroups: str # this should be changed to pgroup
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[constr(max_length=200)] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
class BeamtimeResponse(BaseModel):
id: int
pgroups: str
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[str] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class Sample(BaseModel):
id: int
sample_name: str
@ -493,6 +542,7 @@ class Sample(BaseModel):
mount_count: Optional[int] = None
unmount_count: Optional[int] = None
# results: Optional[Results] = None
beamtimes: List[Beamtime] = []
class Config:
from_attributes = True
@ -507,6 +557,7 @@ class SampleCreate(BaseModel):
comments: Optional[str] = None
results: Optional[Results] = None
events: Optional[List[str]] = None
beamtime_ids: List[int] = []
class Config:
populate_by_name = True
@ -534,7 +585,7 @@ class PuckCreate(BaseModel):
puck_type: str
puck_location_in_dewar: int
samples: List[SampleCreate] = []
beamtime_id: Optional[int] = None
beamtime_ids: List[int] = []
class PuckUpdate(BaseModel):
@ -542,7 +593,7 @@ class PuckUpdate(BaseModel):
puck_type: Optional[str] = None
puck_location_in_dewar: Optional[int] = None
dewar_id: Optional[int] = None
beamtime_id: Optional[int] = None
beamtime_ids: List[int] = []
class Puck(BaseModel):
@ -551,9 +602,9 @@ class Puck(BaseModel):
puck_type: str
puck_location_in_dewar: int
dewar_id: int
beamtime_id: Optional[int] = None
events: List[PuckEvent] = []
samples: List[Sample] = []
beamtimes: List[Beamtime] = []
class Config:
from_attributes = True
@ -573,6 +624,7 @@ class DewarBase(BaseModel):
contact_id: Optional[int]
return_address_id: Optional[int]
pucks: List[PuckCreate] = []
beamtimes: List[Beamtime] = []
class Config:
from_attributes = True
@ -605,6 +657,7 @@ class DewarUpdate(BaseModel):
status: Optional[str] = None
contact_id: Optional[int] = None
address_id: Optional[int] = None
beamtime_ids: List[int] = []
class DewarSchema(BaseModel):
@ -786,55 +839,6 @@ class DewarWithPucksResponse(BaseModel):
pucks: List[PuckResponse]
class Beamtime(BaseModel):
id: int
pgroups: str
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[constr(max_length=200)] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class BeamtimeCreate(BaseModel):
pgroups: str # this should be changed to pgroup
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[constr(max_length=200)] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
class BeamtimeResponse(BaseModel):
id: int
pgroups: str
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[str] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class ImageCreate(BaseModel):
pgroup: str
sample_id: int

View File

@ -168,8 +168,8 @@ async def lifespan(app: FastAPI):
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)
# from sqlalchemy.engine import reflection
# from app.models import ExperimentParameters # adjust the import as needed
# inspector = reflection.Inspector.from_engine(engine)

View File

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

View File

@ -1,7 +1,7 @@
{
"ssl_cert_path": "ssl/cert.pem",
"ssl_key_path": "ssl/key.pem",
"OPENAPI_URL": "https://127.0.0.1:8000/openapi.json",
"OPENAPI_URL": "https://0.0.0.0:8000/openapi.json",
"SCHEMA_PATH": "./src/openapi.json",
"OUTPUT_DIRECTORY": "./openapi",
"PORT": 8000,

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { DataGridPremium, GridColDef } from '@mui/x-data-grid-premium';
import { useNavigate } from 'react-router-dom'; // For navigation
import { BeamtimesService } from '../../openapi';
import { useNavigate } from 'react-router-dom';
import {Beamtime, BeamtimesService} from '../../openapi';
import { Chip, Typography } from '@mui/material';
interface BeamtimeRecord {

View File

@ -59,32 +59,59 @@ const Calendar: React.FC = () => {
setFetchError(null);
try {
const beamtimes = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet();
const formattedEvents: CustomEvent[] = beamtimes.map((beamtime: any) => ({
id: `${beamtime.beamline}-${beamtime.start_date}`,
title: `${beamtime.beamline}: ${beamtime.shift}`,
start: beamtime.start_date,
end: beamtime.end_date,
beamtime_id: beamtime.id,
beamline: beamtime.beamline || 'Unknown',
beamtime_shift: beamtime.shift || 'Unknown',
backgroundColor: beamlineColors[beamtime.beamline] || beamlineColors.Unknown,
borderColor: '#000',
textColor: '#fff',
}));
const grouped: { [key: string]: any[] } = {};
beamtimes.forEach((beamtime: any) => {
const key = `${beamtime.start_date}|${beamtime.beamline}|${beamtime.pgroups}`;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(beamtime);
});
const formattedEvents: CustomEvent[] = Object.values(grouped).map((group) => {
const shifts = group.map((bt: any) => bt.shift).join(" + ");
const ids = group.map((bt: any) => bt.id);
const first = group[0];
return {
id: `${first.beamline}-${first.start_date}-${first.pgroups}`, // ensure uniqueness
title: `${first.beamline}: ${shifts}`,
start: first.start_date,
end: first.end_date,
beamtime_ids: ids,
beamline: first.beamline || 'Unknown',
beamtime_shift: shifts,
backgroundColor: beamlineColors[first.beamline] || beamlineColors.Unknown,
borderColor: '#000',
textColor: '#fff',
beamtimes: group,
};
});
setEvents(formattedEvents);
// Fetch associations for all
const assoc: { [id: string]: { dewars: string[]; pucks: string[] } } = {};
await Promise.all(
beamtimes.map(async (bm: any) => {
const [dewars, pucks] = await Promise.all([
DewarsService.getDewarsByBeamtime(bm.id),
PucksService.getPucksByBeamtime(bm.id),
]);
const eventId = `${bm.beamline}-${bm.start_date}`;
Object.values(grouped).map(async (group) => {
// multiple (or single) beamtimes per group
const ids = group.map((bt: any) => bt.id);
// fetch and merge for all ids in this group:
let dewarsSet = new Set<string>();
let pucksSet = new Set<string>();
await Promise.all(
ids.map(async (beamtimeId: number) => {
const [dewars, pucks] = await Promise.all([
DewarsService.getDewarsByBeamtime(beamtimeId),
PucksService.getPucksByBeamtime(beamtimeId),
]);
dewars.forEach((d: any) => dewarsSet.add(d.id));
pucks.forEach((p: any) => pucksSet.add(p.id));
})
);
// key must match event id
const eventId = `${group[0].beamline}-${group[0].start_date}-${group[0].pgroups}`;
assoc[eventId] = {
dewars: dewars.map((d: any) => d.id),
pucks: pucks.map((p: any) => p.id),
dewars: Array.from(dewarsSet),
pucks: Array.from(pucksSet),
};
})
);
@ -119,20 +146,29 @@ const Calendar: React.FC = () => {
}, [eventDetails]);
// Refresh associations after (un)assign action
const refetchEventAssociations = async (beamtimeId: number, eventId: string) => {
const [dewars, pucks] = await Promise.all([
DewarsService.getDewarsByBeamtime(beamtimeId),
PucksService.getPucksByBeamtime(beamtimeId),
]);
const refetchEventAssociations = async (beamtimeIds: number[], eventId: string) => {
let dewarsSet = new Set<string>();
let pucksSet = new Set<string>();
await Promise.all(
beamtimeIds.map(async (beamtimeId: number) => {
const [dewars, pucks] = await Promise.all([
DewarsService.getDewarsByBeamtime(beamtimeId),
PucksService.getPucksByBeamtime(beamtimeId),
]);
dewars.forEach((d: any) => dewarsSet.add(d.id));
pucks.forEach((p: any) => pucksSet.add(p.id));
})
);
setEventAssociations(prev => ({
...prev,
[eventId]: {
dewars: dewars.map((d: any) => d.id),
pucks: pucks.map((p: any) => p.id),
dewars: Array.from(dewarsSet),
pucks: Array.from(pucksSet),
}
}));
};
const handleEventClick = (eventInfo: any) => {
const clickedEventId = eventInfo.event.id;
setSelectedEventId(clickedEventId);
@ -171,77 +207,115 @@ const Calendar: React.FC = () => {
const handleDewarAssignment = async (dewarId: string) => {
if (!selectedEventId) return;
const event = events.find(e => e.id === selectedEventId)!;
const beamtimeId = event.beamtime_id;
if (!beamtimeId) return;
// Is this dewar already assigned here?
const beamtimeIds: number[] = event.beamtime_ids || [];
if (!beamtimeIds.length) return;
const assigned = eventAssociations[selectedEventId]?.dewars.includes(dewarId);
try {
if (assigned) {
await DewarsService.assignDewarToBeamtime(Number(dewarId), 0);
} else {
await DewarsService.assignDewarToBeamtime(Number(dewarId), Number(beamtimeId));
}
await refetchEventAssociations(Number(beamtimeId), selectedEventId);
await Promise.all(
beamtimeIds.map(btId =>
assigned
? DewarsService.assignDewarToBeamtime(Number(dewarId), 0)
: DewarsService.assignDewarToBeamtime(Number(dewarId), Number(btId))
)
);
await refetchEventAssociations(beamtimeIds, selectedEventId);
} catch (e) {
// Optionally report error
}
/* error handling */}
};
// Unified assign/unassign for Pucks
const handlePuckAssignment = async (puckId: string) => {
if (!selectedEventId) return;
const event = events.find(e => e.id === selectedEventId)!;
const beamtimeId = event.beamtime_id;
if (!beamtimeId) return;
const beamtimeIds: number[] = event.beamtime_ids || [];
if (!beamtimeIds.length) return;
const assigned = eventAssociations[selectedEventId]?.pucks.includes(puckId);
try {
if (assigned) {
await PucksService.assignPuckToBeamtime(Number(puckId), 0);
} else {
await PucksService.assignPuckToBeamtime(Number(puckId), Number(beamtimeId));
}
await refetchEventAssociations(Number(beamtimeId), selectedEventId);
} catch (e) {
// Optionally report error
}
await Promise.all(
beamtimeIds.map(async btId =>
assigned
? PucksService.assignPuckToBeamtime(Number(puckId), 0)
: PucksService.assignPuckToBeamtime(Number(puckId), Number(btId))
)
);
await refetchEventAssociations(beamtimeIds, selectedEventId);
} catch (e) {/* error handling */}
};
// For displaying badge in calendar and UI
const eventContent = (eventInfo: any) => {
const beamline = eventInfo.event.extendedProps.beamline || 'Unknown';
const isSelected = selectedEventId === eventInfo.event.id;
const isSubmitted = eventInfo.event.extendedProps.isSubmitted;
const hasAssociations =
eventAssociations[eventInfo.event.id]?.dewars.length > 0 ||
eventAssociations[eventInfo.event.id]?.pucks.length > 0;
const backgroundColor = isSubmitted
? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20)
: isSelected
? '#FFD700'
: (beamlineColors[beamline] || beamlineColors.Unknown);
const beamtimesInGroup = eventInfo.event.extendedProps.beamtimes
? eventInfo.event.extendedProps.beamtimes.length
: 1;
const minHeight = beamtimesInGroup * 26;
const beamline = eventInfo.event.extendedProps.beamline || 'Unknown';
const isSelected = selectedEventId === eventInfo.event.id;
const isSubmitted = eventInfo.event.extendedProps.isSubmitted;
const assoc = eventAssociations[eventInfo.event.id] || { dewars: [], pucks: [] };
const backgroundColor = isSubmitted
? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20)
: isSelected
? '#FFD700'
: (beamlineColors[beamline] || beamlineColors.Unknown);
return (
<div
style={{
backgroundColor: backgroundColor,
color: 'white',
border: isSelected ? '2px solid black' : 'none',
borderRadius: '3px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
cursor: 'pointer',
overflow: 'hidden',
boxSizing: 'border-box',
}}
>
return (
<div
style={{
backgroundColor: backgroundColor,
color: 'white',
border: isSelected ? '2px solid black' : 'none',
borderRadius: '3px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%',
width: '100%',
cursor: 'pointer',
overflow: 'hidden',
boxSizing: 'border-box',
padding: '0 6px',
minHeight: `${minHeight}px`,
}}
>
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{eventInfo.event.title}
{hasAssociations && <span style={{marginLeft: 8}}>🧊</span>}
</div>
);
};
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: 8 }}>
<span title="Dewars" style={{ display: 'flex', alignItems: 'center', fontSize: 13 }}>
🧊
<span style={{
background: 'rgba(0,0,0,0.45)',
borderRadius: '8px',
marginLeft: 2,
minWidth: 14,
color: '#fff',
fontSize: 12,
padding: '0 4px',
fontWeight: 600,
textAlign: 'center'
}}>{assoc.dewars.length}</span>
</span>
<span title="Pucks" style={{ display: 'flex', alignItems: 'center', fontSize: 13 }}>
<span style={{
background: 'rgba(0,0,0,0.45)',
borderRadius: '8px',
marginLeft: 2,
minWidth: 14,
color: '#fff',
fontSize: 12,
padding: '0 4px',
fontWeight: 600,
textAlign: 'center'
}}>{assoc.pucks.length}</span>
</span>
</span>
</div>
);
};
// Used in Dewar/Puck assign status reporting
function getAssignedEventForDewar(dewarId: string) {

View File

@ -446,21 +446,21 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-05T13:31:12.902282Z",
"start_time": "2025-05-05T13:31:12.900432Z"
"end_time": "2025-05-08T13:31:36.929465Z",
"start_time": "2025-05-08T13:31:36.925054Z"
}
},
"cell_type": "code",
"source": "sample_id = 277",
"source": "sample_id = 44",
"id": "54d4d46ca558e7b9",
"outputs": [],
"execution_count": 7
"execution_count": 28
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-05T13:31:16.752616Z",
"start_time": "2025-05-05T13:31:16.720296Z"
"end_time": "2025-05-08T13:31:40.023546Z",
"start_time": "2025-05-08T13:31:39.978510Z"
}
},
"cell_type": "code",
@ -512,7 +512,7 @@
"DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n",
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n",
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/277/events HTTP/1.1\" 200 413\n"
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/44/events HTTP/1.1\" 200 884\n"
]
},
{
@ -522,29 +522,29 @@
"Payload being sent to API:\n",
"{\"event_type\":\"Collecting\"}\n",
"API response:\n",
"('id', 277)\n",
"('sample_name', 'Sample277')\n",
"('position', 15)\n",
"('puck_id', 30)\n",
"('id', 44)\n",
"('sample_name', 'Sample044')\n",
"('position', 7)\n",
"('puck_id', 7)\n",
"('crystalname', None)\n",
"('proteinname', None)\n",
"('positioninpuck', None)\n",
"('priority', None)\n",
"('comments', None)\n",
"('data_collection_parameters', None)\n",
"('events', [SampleEventResponse(event_type='Mounting', id=533, sample_id=277, timestamp=datetime.datetime(2025, 5, 4, 14, 9)), SampleEventResponse(event_type='Collecting', id=534, sample_id=277, timestamp=datetime.datetime(2025, 5, 5, 13, 31, 16, 741949))])\n",
"('events', [SampleEventResponse(event_type='Mounting', id=87, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16)), SampleEventResponse(event_type='Unmounting', id=88, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16, 50)), SampleEventResponse(event_type='Collecting', id=507, sample_id=44, timestamp=datetime.datetime(2025, 5, 8, 13, 31, 40, 6059))])\n",
"('mount_count', 0)\n",
"('unmount_count', 0)\n"
]
}
],
"execution_count": 8
"execution_count": 29
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-04-29T14:27:46.730515Z",
"start_time": "2025-04-29T14:27:46.622922Z"
"end_time": "2025-05-08T13:31:43.663278Z",
"start_time": "2025-05-08T13:31:43.651369Z"
}
},
"cell_type": "code",
@ -572,18 +572,18 @@
"traceback": [
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
"\u001B[0;31mAttributeError\u001B[0m Traceback (most recent call last)",
"Cell \u001B[0;32mIn[48], line 8\u001B[0m\n\u001B[1;32m 4\u001B[0m api_instance \u001B[38;5;241m=\u001B[39m aareDBclient\u001B[38;5;241m.\u001B[39mSamplesApi(api_client)\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 7\u001B[0m \u001B[38;5;66;03m# Get the last sample event\u001B[39;00m\n\u001B[0;32m----> 8\u001B[0m last_event_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget_last_sample_event_samples_samples_sample_id_events_last_get\u001B[49m(\u001B[38;5;241m27\u001B[39m)\n\u001B[1;32m 9\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mThe response of get_last_sample_event:\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 10\u001B[0m pprint(last_event_response)\n",
"Cell \u001B[0;32mIn[30], line 8\u001B[0m\n\u001B[1;32m 4\u001B[0m api_instance \u001B[38;5;241m=\u001B[39m aareDBclient\u001B[38;5;241m.\u001B[39mSamplesApi(api_client)\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[1;32m 7\u001B[0m \u001B[38;5;66;03m# Get the last sample event\u001B[39;00m\n\u001B[0;32m----> 8\u001B[0m last_event_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget_last_sample_event_samples_samples_sample_id_events_last_get\u001B[49m(\u001B[38;5;241m27\u001B[39m)\n\u001B[1;32m 9\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mThe response of get_last_sample_event:\u001B[39m\u001B[38;5;130;01m\\n\u001B[39;00m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 10\u001B[0m pprint(last_event_response)\n",
"\u001B[0;31mAttributeError\u001B[0m: 'SamplesApi' object has no attribute 'get_last_sample_event_samples_samples_sample_id_events_last_get'"
]
}
],
"execution_count": 48
"execution_count": 30
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-05T13:31:21.833174Z",
"start_time": "2025-05-05T13:31:21.791711Z"
"end_time": "2025-05-08T13:31:46.103881Z",
"start_time": "2025-05-08T13:31:46.061151Z"
}
},
"cell_type": "code",
@ -654,7 +654,7 @@
"DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 127.0.0.1:8000\n",
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n",
"DEBUG:urllib3.connectionpool:https://127.0.0.1:8000 \"POST /samples/277/upload-images HTTP/1.1\" 200 205\n"
"DEBUG:urllib3.connectionpool:https://127.0.0.1:8000 \"POST /samples/44/upload-images HTTP/1.1\" 200 203\n"
]
},
{
@ -664,11 +664,11 @@
"Uploading after_dc.jpeg.jpg...\n",
"API Response for after_dc.jpeg.jpg:\n",
"200\n",
"{'pgroup': 'p20003', 'sample_id': 277, 'sample_event_id': 534, 'filepath': 'images/p20003/2025-05-05/Dewar Five/PKK007/15/Collecting_2025-05-05_13-31-16/after_dc.jpeg.jpg', 'status': 'active', 'comment': None, 'id': 1}\n"
"{'pgroup': 'p20001', 'sample_id': 44, 'sample_event_id': 507, 'filepath': 'images/p20001/2025-05-08/Dewar One/PUCK007/7/Collecting_2025-05-08_13-31-40/after_dc.jpeg.jpg', 'status': 'active', 'comment': None, 'id': 1}\n"
]
}
],
"execution_count": 9
"execution_count": 31
},
{
"metadata": {},
@ -681,8 +681,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-05T13:34:32.164108Z",
"start_time": "2025-05-05T13:34:32.130230Z"
"end_time": "2025-05-08T13:53:49.337315Z",
"start_time": "2025-05-08T13:53:49.288039Z"
}
},
"cell_type": "code",
@ -799,7 +799,7 @@
"DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n",
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n",
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/277/experiment_parameters HTTP/1.1\" 200 904\n"
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/44/experiment_parameters HTTP/1.1\" 200 903\n"
]
},
{
@ -807,17 +807,17 @@
"output_type": "stream",
"text": [
"API Response:\n",
"run_number=2 type='standard' beamline_parameters=BeamlineParameters(synchrotron='Swiss Light Source', beamline='PXIII', detector=Detector(manufacturer='DECTRIS', model='PILATUS4 2M', type='photon-counting', serial_number='16684dscsd668468', detector_distance_mm=232.0, beam_center_x_px=768.0, beam_center_y_px=857.0, pixel_size_x_um=150.0, pixel_size_y_um=150.0), wavelength=1.033, ring_current_a=0.4, ring_mode='Machine Development', undulator=None, undulatorgap_mm=None, monochromator='Si111', transmission=10.0, focusing_optic='Kirkpatrick-Baez', beamline_flux_at_sample_ph_s=0.0, beam_size_width=30.0, beam_size_height=30.0, characterization=None, rotation=RotationParameters(omega_start_deg=0.0, omega_step=0.2, chi=0.0, phi=10.0, number_of_images=1800, exposure_time_s=0.01), grid_scan=None, jet=None, cryojet_temperature_k=None, humidifier_temperature_k=None, humidifier_humidity=None) dataset=None sample_id=277 id=2\n"
"run_number=2 type='standard' beamline_parameters=BeamlineParameters(synchrotron='Swiss Light Source', beamline='PXIII', detector=Detector(manufacturer='DECTRIS', model='PILATUS4 2M', type='photon-counting', serial_number='16684dscsd668468', detector_distance_mm=232.0, beam_center_x_px=768.0, beam_center_y_px=857.0, pixel_size_x_um=150.0, pixel_size_y_um=150.0), wavelength=1.033, ring_current_a=0.4, ring_mode='Machine Development', undulator=None, undulatorgap_mm=None, monochromator='Si111', transmission=10.0, focusing_optic='Kirkpatrick-Baez', beamline_flux_at_sample_ph_s=0.0, beam_size_width=30.0, beam_size_height=30.0, characterization=None, rotation=RotationParameters(omega_start_deg=0.0, omega_step=0.2, chi=0.0, phi=10.0, number_of_images=1800, exposure_time_s=0.01), grid_scan=None, jet=None, cryojet_temperature_k=None, humidifier_temperature_k=None, humidifier_humidity=None) dataset=None sample_id=44 id=2\n"
]
}
],
"execution_count": 13
"execution_count": 34
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-05-05T13:34:49.891432Z",
"start_time": "2025-05-05T13:34:49.872697Z"
"end_time": "2025-05-08T13:53:58.864551Z",
"start_time": "2025-05-08T13:53:58.837176Z"
}
},
"cell_type": "code",
@ -859,7 +859,7 @@
"DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n",
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n",
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"PATCH /samples/update-dataset/277/2 HTTP/1.1\" 200 1085\n"
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"PATCH /samples/update-dataset/44/2 HTTP/1.1\" 200 1084\n"
]
},
{
@ -867,11 +867,11 @@
"output_type": "stream",
"text": [
"Dataset updated successfully:\n",
"run_number=2 type='standard' beamline_parameters=BeamlineParameters(synchrotron='Swiss Light Source', beamline='PXIII', detector=Detector(manufacturer='DECTRIS', model='PILATUS4 2M', type='photon-counting', serial_number='16684dscsd668468', detector_distance_mm=232.0, beam_center_x_px=768.0, beam_center_y_px=857.0, pixel_size_x_um=150.0, pixel_size_y_um=150.0), wavelength=1.033, ring_current_a=0.4, ring_mode='Machine Development', undulator=None, undulatorgap_mm=None, monochromator='Si111', transmission=10.0, focusing_optic='Kirkpatrick-Baez', beamline_flux_at_sample_ph_s=0.0, beam_size_width=30.0, beam_size_height=30.0, characterization=None, rotation=RotationParameters(omega_start_deg=0.0, omega_step=0.2, chi=0.0, phi=10.0, number_of_images=1800, exposure_time_s=0.01), grid_scan=None, jet=None, cryojet_temperature_k=None, humidifier_temperature_k=None, humidifier_humidity=None) dataset=Datasets(filepath='/das/work/p11/p11206/raw_data/vincent/20250415_6D_SLS2_1st_data/20250415_fullbeam_dtz220_Lyso102_again_360deg', status='written', written_at=datetime.datetime(2025, 5, 5, 15, 34, 49, 874526)) sample_id=277 id=2\n"
"run_number=2 type='standard' beamline_parameters=BeamlineParameters(synchrotron='Swiss Light Source', beamline='PXIII', detector=Detector(manufacturer='DECTRIS', model='PILATUS4 2M', type='photon-counting', serial_number='16684dscsd668468', detector_distance_mm=232.0, beam_center_x_px=768.0, beam_center_y_px=857.0, pixel_size_x_um=150.0, pixel_size_y_um=150.0), wavelength=1.033, ring_current_a=0.4, ring_mode='Machine Development', undulator=None, undulatorgap_mm=None, monochromator='Si111', transmission=10.0, focusing_optic='Kirkpatrick-Baez', beamline_flux_at_sample_ph_s=0.0, beam_size_width=30.0, beam_size_height=30.0, characterization=None, rotation=RotationParameters(omega_start_deg=0.0, omega_step=0.2, chi=0.0, phi=10.0, number_of_images=1800, exposure_time_s=0.01), grid_scan=None, jet=None, cryojet_temperature_k=None, humidifier_temperature_k=None, humidifier_humidity=None) dataset=Datasets(filepath='/das/work/p11/p11206/raw_data/vincent/20250415_6D_SLS2_1st_data/20250415_fullbeam_dtz220_Lyso102_again_360deg', status='written', written_at=datetime.datetime(2025, 5, 8, 15, 53, 58, 838599)) sample_id=44 id=2\n"
]
}
],
"execution_count": 15
"execution_count": 35
},
{
"metadata": {