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": {