From 6a0953c913c20af85c1623baa20d79c0472f1e8e Mon Sep 17 00:00:00 2001 From: GotthardG <51994228+GotthardG@users.noreply.github.com> Date: Thu, 8 May 2025 16:04:05 +0200 Subject: [PATCH] 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. --- backend/app/data/data.py | 49 +++- backend/app/models.py | 48 +++- backend/app/routers/dewar.py | 37 ++- backend/app/routers/puck.py | 37 ++- backend/app/routers/sample.py | 13 +- backend/app/schemas.py | 118 +++++----- backend/main.py | 4 +- backend/pyproject.toml | 2 +- config_dev.json | 2 +- frontend/src/components/BeamtimeOverview.tsx | 4 +- frontend/src/components/Calendar.tsx | 236 ++++++++++++------- testfunctions.ipynb | 64 ++--- 12 files changed, 404 insertions(+), 210 deletions(-) diff --git a/backend/app/data/data.py b/backend/app/data/data.py index c58154f..70b771f 100644 --- a/backend/app/data/data.py +++ b/backend/app/data/data.py @@ -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"] diff --git a/backend/app/models.py b/backend/app/models.py index 4f95bda..ece34d2 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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): diff --git a/backend/app/routers/dewar.py b/backend/app/routers/dewar.py index 4fc2824..d1dfc98 100644 --- a/backend/app/routers/dewar.py +++ b/backend/app/routers/dewar.py @@ -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 diff --git a/backend/app/routers/puck.py b/backend/app/routers/puck.py index cc85d05..b53ae98 100644 --- a/backend/app/routers/puck.py +++ b/backend/app/routers/puck.py @@ -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 diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 5927bd0..7289e4d 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -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 diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 85cca2e..9c8ad72 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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 diff --git a/backend/main.py b/backend/main.py index caf1b7d..328c368 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index f093fa4..b872c77 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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"} diff --git a/config_dev.json b/config_dev.json index 670d0bf..3e0cd61 100644 --- a/config_dev.json +++ b/config_dev.json @@ -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, diff --git a/frontend/src/components/BeamtimeOverview.tsx b/frontend/src/components/BeamtimeOverview.tsx index 964e020..858b7cd 100644 --- a/frontend/src/components/BeamtimeOverview.tsx +++ b/frontend/src/components/BeamtimeOverview.tsx @@ -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 { diff --git a/frontend/src/components/Calendar.tsx b/frontend/src/components/Calendar.tsx index d7e6391..f692724 100644 --- a/frontend/src/components/Calendar.tsx +++ b/frontend/src/components/Calendar.tsx @@ -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(); + let pucksSet = new Set(); + 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(); + let pucksSet = new Set(); + 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 ( -
+ return ( +
+ {eventInfo.event.title} - {hasAssociations && 🧊} -
- ); - }; + + + + 🧊 + {assoc.dewars.length} + + + ⚪ + {assoc.pucks.length} + + +
+ ); +}; + // Used in Dewar/Puck assign status reporting function getAssignedEventForDewar(dewarId: string) { diff --git a/testfunctions.ipynb b/testfunctions.ipynb index cbd0651..73fe46f 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -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": {