Add mount_count and unmount_count tracking for samples

Introduced `mount_count` and `unmount_count` fields to track mounting events for samples. Updated models, schemas, and front-end components to support dynamic calculation and display of these counts. Enhanced backend queries and API responses to include the new data.
This commit is contained in:
GotthardG 2025-01-20 11:02:56 +01:00
parent 3d804c1635
commit 3b315f2997
4 changed files with 56 additions and 13 deletions

View File

@ -151,6 +151,16 @@ class Sample(Base):
puck = relationship("Puck", back_populates="samples") puck = relationship("Puck", back_populates="samples")
events = relationship("SampleEvent", back_populates="sample") events = relationship("SampleEvent", back_populates="sample")
@property
def mount_count(self) -> int:
# Dynamically calculate mount_count
return len([event for event in self.events if event.event_type == "Mounted"])
@property
def unmount_count(self) -> int:
# Dynamically calculate unmount_count
return len([event for event in self.events if event.event_type == "Unmounted"])
class Slot(Base): class Slot(Base):
__tablename__ = "slots" __tablename__ = "slots"

View File

@ -9,6 +9,7 @@ from app.schemas import (
Sample as SampleSchema, Sample as SampleSchema,
SampleEventResponse, SampleEventResponse,
SampleEventCreate, SampleEventCreate,
Sample,
) )
from app.models import ( from app.models import (
Puck as PuckModel, Puck as PuckModel,
@ -17,6 +18,8 @@ from app.models import (
) )
from app.dependencies import get_db from app.dependencies import get_db
import logging import logging
from sqlalchemy.orm import joinedload
router = APIRouter() router = APIRouter()
@ -43,22 +46,25 @@ async def get_samples_with_events(puck_id: int, db: Session = Depends(get_db)):
async def get_all_pucks_with_samples_and_events(db: Session = Depends(get_db)): async def get_all_pucks_with_samples_and_events(db: Session = Depends(get_db)):
logging.info("Fetching all pucks with samples and events") logging.info("Fetching all pucks with samples and events")
pucks = db.query(PuckModel).all() pucks = (
logging.info(f"Found {len(pucks)} pucks in the database") db.query(PuckModel)
for puck in pucks: .options(
if puck.dewar_id is None: joinedload(PuckModel.samples).joinedload(
puck.dewar_id = -1 SampleModel.events
logging.info(f"Puck ID: {puck.id}, Name: {puck.puck_name}") ), # Correct nested relationship
joinedload(PuckModel.events), # If Puck has its own events relationship
)
.all()
)
if not pucks: if not pucks:
raise HTTPException( raise HTTPException(status_code=404, detail="No pucks found in the database")
status_code=404, detail="No pucks found in the database"
) # More descriptive
return pucks return pucks
# Route to post a new sample event # Route to post a new sample event
@router.post("/samples/{sample_id}/events", response_model=SampleEventResponse) @router.post("/samples/{sample_id}/events", response_model=Sample)
async def create_sample_event( async def create_sample_event(
sample_id: int, event: SampleEventCreate, db: Session = Depends(get_db) sample_id: int, event: SampleEventCreate, db: Session = Depends(get_db)
): ):
@ -77,10 +83,13 @@ async def create_sample_event(
db.commit() db.commit()
db.refresh(sample_event) db.refresh(sample_event)
return ( # Load events for the sample to be serialized in the response
sample_event # Response will automatically use the SampleEventResponse schema sample.events = (
db.query(SampleEventModel).filter(SampleEventModel.sample_id == sample_id).all()
) )
return sample # Return the sample, now including `mount_count`
# Route to fetch the last (most recent) sample event # Route to fetch the last (most recent) sample event
@router.get("/samples/{sample_id}/events/last", response_model=SampleEventResponse) @router.get("/samples/{sample_id}/events/last", response_model=SampleEventResponse)

View File

@ -424,6 +424,9 @@ class Sample(BaseModel):
comments: Optional[str] = None comments: Optional[str] = None
data_collection_parameters: Optional[DataCollectionParameters] data_collection_parameters: Optional[DataCollectionParameters]
events: List[SampleEventResponse] = [] events: List[SampleEventResponse] = []
mount_count: Optional[int] = None
unmount_count: Optional[int] = None
# results: Optional[Results] = None
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -13,6 +13,8 @@ interface Sample {
crystalname?: string; crystalname?: string;
positioninpuck?: number; positioninpuck?: number;
events: Event[]; events: Event[];
mount_count: number; // Add this
unmount_count: number; // Add this
} }
interface Puck { interface Puck {
@ -32,6 +34,8 @@ const SampleTracker: React.FC = () => {
const fetchPucks = async () => { const fetchPucks = async () => {
try { try {
const data: Puck[] = await SamplesService.getAllPucksWithSamplesAndEventsSamplesPucksSamplesGet(); const data: Puck[] = await SamplesService.getAllPucksWithSamplesAndEventsSamplesPucksSamplesGet();
console.log('Fetched Pucks:', data); // Check for dynamic mount_count and unmount_count
setPucks(data); setPucks(data);
} catch (error) { } catch (error) {
console.error('Error fetching pucks', error); console.error('Error fetching pucks', error);
@ -110,12 +114,29 @@ const SampleTracker: React.FC = () => {
border: sample && sample.events.some((e) => e.event_type === 'Lost') border: sample && sample.events.some((e) => e.event_type === 'Lost')
? '1px solid red' ? '1px solid red'
: '1px solid lightgray', : '1px solid lightgray',
position: 'relative', // Add for overlay positioning
}} }}
onMouseEnter={() => onMouseEnter={() =>
sample && setHoveredSample({ name: sample.sample_name, status }) sample && setHoveredSample({ name: sample.sample_name, status })
} }
onMouseLeave={() => setHoveredSample(null)} onMouseLeave={() => setHoveredSample(null)}
></div> >
{sample && sample.mount_count > 0 && ( // Render only if mount_count > 0
<span
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
color: 'white',
fontSize: '8px',
fontWeight: 'bold',
}}
>
{sample.mount_count}
</span>
)}
</div>
); );
})} })}
</div> </div>