Add beamtime relationships and enhance sample handling
This commit adds relationships to link Pucks and Samples to Beamtime in the models, enabling better data association. Includes changes to assign beamtime IDs during data generation and updates in API response models for improved data loading. Removed redundant code in testfunctions.ipynb to clean up the notebook.
This commit is contained in:
parent
102a11eed7
commit
4328b84795
@ -407,9 +407,9 @@ beamtimes = [
|
|||||||
),
|
),
|
||||||
Beamtime(
|
Beamtime(
|
||||||
id=2,
|
id=2,
|
||||||
pgroups="p20002",
|
pgroups="p20003",
|
||||||
shift="afternoon",
|
shift="afternoon",
|
||||||
beamtime_name="p20001-test",
|
beamtime_name="p20003-test",
|
||||||
beamline="X06DA",
|
beamline="X06DA",
|
||||||
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
|
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
|
||||||
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
|
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
|
||||||
@ -677,8 +677,15 @@ pucks = [
|
|||||||
# Define samples
|
# Define samples
|
||||||
samples = []
|
samples = []
|
||||||
sample_id_counter = 1
|
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
|
||||||
|
}
|
||||||
|
|
||||||
for puck in pucks:
|
for puck in pucks:
|
||||||
|
dewar_id = puck.dewar_id # Assuming puck has dewar_id
|
||||||
|
assigned_beamtime = dewar_to_beamtime[dewar_id]
|
||||||
|
|
||||||
positions_with_samples = random.randint(1, 16)
|
positions_with_samples = random.randint(1, 16)
|
||||||
occupied_positions = random.sample(range(1, 17), positions_with_samples)
|
occupied_positions = random.sample(range(1, 17), positions_with_samples)
|
||||||
|
|
||||||
@ -689,6 +696,7 @@ for puck in pucks:
|
|||||||
sample_name=f"Sample{sample_id_counter:03}",
|
sample_name=f"Sample{sample_id_counter:03}",
|
||||||
position=pos,
|
position=pos,
|
||||||
puck_id=puck.id,
|
puck_id=puck.id,
|
||||||
|
beamtime_id=assigned_beamtime, # IMPORTANT: Use the dewar's beamtime
|
||||||
)
|
)
|
||||||
samples.append(sample)
|
samples.append(sample)
|
||||||
sample_id_counter += 1
|
sample_id_counter += 1
|
||||||
|
@ -154,6 +154,10 @@ class Puck(Base):
|
|||||||
dewar = relationship("Dewar", back_populates="pucks")
|
dewar = relationship("Dewar", back_populates="pucks")
|
||||||
samples = relationship("Sample", back_populates="puck")
|
samples = relationship("Sample", back_populates="puck")
|
||||||
events = relationship("PuckEvent", 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]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Sample(Base):
|
class Sample(Base):
|
||||||
@ -173,6 +177,8 @@ class Sample(Base):
|
|||||||
puck = relationship("Puck", back_populates="samples")
|
puck = relationship("Puck", back_populates="samples")
|
||||||
events = relationship("SampleEvent", back_populates="sample", lazy="joined")
|
events = relationship("SampleEvent", back_populates="sample", lazy="joined")
|
||||||
images = relationship("Image", 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")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mount_count(self) -> int:
|
def mount_count(self) -> int:
|
||||||
@ -256,6 +262,8 @@ class Beamtime(Base):
|
|||||||
|
|
||||||
local_contact = relationship("LocalContact")
|
local_contact = relationship("LocalContact")
|
||||||
dewars = relationship("Dewar", back_populates="beamtime")
|
dewars = relationship("Dewar", back_populates="beamtime")
|
||||||
|
pucks = relationship("Puck", back_populates="beamtime")
|
||||||
|
samples = relationship("Sample", back_populates="beamtime")
|
||||||
|
|
||||||
|
|
||||||
class Image(Base):
|
class Image(Base):
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
from fastapi import APIRouter, HTTPException, status, Depends
|
from fastapi import APIRouter, HTTPException, status, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
|
|
||||||
from app.models import Beamtime as BeamtimeModel
|
from app.models import Beamtime as BeamtimeModel
|
||||||
from app.schemas import Beamtime as BeamtimeSchema, BeamtimeCreate, loginData
|
from app.schemas import (
|
||||||
|
Beamtime as BeamtimeSchema,
|
||||||
|
BeamtimeCreate,
|
||||||
|
loginData,
|
||||||
|
BeamtimeResponse,
|
||||||
|
)
|
||||||
from app.dependencies import get_db
|
from app.dependencies import get_db
|
||||||
from app.routers.auth import get_current_user
|
from app.routers.auth import get_current_user
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ async def create_beamtime(
|
|||||||
|
|
||||||
@beamtime_router.get(
|
@beamtime_router.get(
|
||||||
"/my-beamtimes",
|
"/my-beamtimes",
|
||||||
response_model=list[BeamtimeSchema],
|
response_model=list[BeamtimeResponse],
|
||||||
)
|
)
|
||||||
async def get_my_beamtimes(
|
async def get_my_beamtimes(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@ -68,5 +73,10 @@ async def get_my_beamtimes(
|
|||||||
):
|
):
|
||||||
user_pgroups = current_user.pgroups
|
user_pgroups = current_user.pgroups
|
||||||
filters = [BeamtimeModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
|
filters = [BeamtimeModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
|
||||||
beamtimes = db.query(BeamtimeModel).filter(or_(*filters)).all()
|
beamtimes = (
|
||||||
|
db.query(BeamtimeModel)
|
||||||
|
.options(joinedload(BeamtimeModel.local_contact))
|
||||||
|
.filter(or_(*filters))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
return beamtimes
|
return beamtimes
|
||||||
|
@ -425,6 +425,7 @@ def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
result_entry = ResultsModel(
|
result_entry = ResultsModel(
|
||||||
sample_id=payload.sample_id,
|
sample_id=payload.sample_id,
|
||||||
|
status=payload.status,
|
||||||
run_id=payload.run_id,
|
run_id=payload.run_id,
|
||||||
result=payload.result.model_dump(), # Serialize entire result to JSON
|
result=payload.result.model_dump(), # Serialize entire result to JSON
|
||||||
)
|
)
|
||||||
@ -435,6 +436,7 @@ def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
return ResultResponse(
|
return ResultResponse(
|
||||||
id=result_entry.id,
|
id=result_entry.id,
|
||||||
|
status=result_entry.status,
|
||||||
sample_id=result_entry.sample_id,
|
sample_id=result_entry.sample_id,
|
||||||
run_id=result_entry.run_id,
|
run_id=result_entry.run_id,
|
||||||
result=payload.result, # return original payload directly
|
result=payload.result, # return original payload directly
|
||||||
|
@ -534,6 +534,7 @@ class PuckCreate(BaseModel):
|
|||||||
puck_type: str
|
puck_type: str
|
||||||
puck_location_in_dewar: int
|
puck_location_in_dewar: int
|
||||||
samples: List[SampleCreate] = []
|
samples: List[SampleCreate] = []
|
||||||
|
beamtime_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class PuckUpdate(BaseModel):
|
class PuckUpdate(BaseModel):
|
||||||
@ -541,6 +542,7 @@ class PuckUpdate(BaseModel):
|
|||||||
puck_type: Optional[str] = None
|
puck_type: Optional[str] = None
|
||||||
puck_location_in_dewar: Optional[int] = None
|
puck_location_in_dewar: Optional[int] = None
|
||||||
dewar_id: Optional[int] = None
|
dewar_id: Optional[int] = None
|
||||||
|
beamtime_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class Puck(BaseModel):
|
class Puck(BaseModel):
|
||||||
@ -549,6 +551,7 @@ class Puck(BaseModel):
|
|||||||
puck_type: str
|
puck_type: str
|
||||||
puck_location_in_dewar: int
|
puck_location_in_dewar: int
|
||||||
dewar_id: int
|
dewar_id: int
|
||||||
|
beamtime_id: Optional[int] = None
|
||||||
events: List[PuckEvent] = []
|
events: List[PuckEvent] = []
|
||||||
samples: List[Sample] = []
|
samples: List[Sample] = []
|
||||||
|
|
||||||
@ -800,6 +803,24 @@ class BeamtimeCreate(BaseModel):
|
|||||||
local_contact_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):
|
class ImageCreate(BaseModel):
|
||||||
pgroup: str
|
pgroup: str
|
||||||
sample_id: int
|
sample_id: int
|
||||||
|
@ -168,8 +168,8 @@ async def lifespan(app: FastAPI):
|
|||||||
load_slots_data(db)
|
load_slots_data(db)
|
||||||
else: # dev or test environments
|
else: # dev or test environments
|
||||||
print(f"{environment.capitalize()} environment: Regenerating database.")
|
print(f"{environment.capitalize()} environment: Regenerating database.")
|
||||||
# Base.metadata.drop_all(bind=engine)
|
Base.metadata.drop_all(bind=engine)
|
||||||
# Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
# from sqlalchemy.engine import reflection
|
# from sqlalchemy.engine import reflection
|
||||||
# from app.models import ExperimentParameters # adjust the import as needed
|
# from app.models import ExperimentParameters # adjust the import as needed
|
||||||
# inspector = reflection.Inspector.from_engine(engine)
|
# inspector = reflection.Inspector.from_engine(engine)
|
||||||
|
@ -12,6 +12,7 @@ import AddressManager from './pages/AddressManagerView';
|
|||||||
import ContactsManager from './pages/ContactsManagerView';
|
import ContactsManager from './pages/ContactsManagerView';
|
||||||
import LoginView from './pages/LoginView';
|
import LoginView from './pages/LoginView';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
import BeamtimeOverview from './components/BeamtimeOverview';
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [openAddressManager, setOpenAddressManager] = useState(false);
|
const [openAddressManager, setOpenAddressManager] = useState(false);
|
||||||
@ -84,7 +85,12 @@ const App: React.FC = () => {
|
|||||||
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
|
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
|
||||||
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView pgroups={pgroups} activePgroup={activePgroup} />} />} />
|
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView pgroups={pgroups} activePgroup={activePgroup} />} />} />
|
||||||
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
|
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
|
||||||
<Route path="/results" element={<ProtectedRoute element={<ResultsView pgroups={pgroups} activePgroup={activePgroup} />} />} />
|
<Route path="/results/:beamtimeId" element={<ProtectedRoute element={<ResultsView pgroups={pgroups} activePgroup={activePgroup} />} />} />
|
||||||
|
<Route path="/beamtime-overview" element={<ProtectedRoute element={<BeamtimeOverview activePgroup={activePgroup} />} />} />
|
||||||
|
<Route path="/results" element={<ProtectedRoute element={<BeamtimeOverview activePgroup={activePgroup} />} />}/>
|
||||||
|
{/* Optionally, add a 404 fallback route */}
|
||||||
|
<Route path="*" element={<div>Page not found</div>} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
|
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
|
||||||
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />
|
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />
|
||||||
|
138
frontend/src/components/BeamtimeOverview.tsx
Normal file
138
frontend/src/components/BeamtimeOverview.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
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 { Chip, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface BeamtimeRecord {
|
||||||
|
id: number;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
shift: string;
|
||||||
|
beamline: string;
|
||||||
|
local_contact: string;
|
||||||
|
pgroups: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeamtimeOverviewProps {
|
||||||
|
activePgroup: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BeamtimeOverview: React.FC<BeamtimeOverviewProps> = ({ activePgroup }) => {
|
||||||
|
const [rows, setRows] = useState<BeamtimeRecord[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// For navigation
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const renderPgroupChips = (pgroups: string, activePgroup: string) => {
|
||||||
|
// Safely handle pgroups as an array
|
||||||
|
const pgroupsArray = pgroups.split(",").map((pgroup: string) => pgroup.trim());
|
||||||
|
|
||||||
|
if (!pgroupsArray.length) {
|
||||||
|
return <Typography variant="body2">No associated pgroups</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pgroupsArray.map((pgroup: string) => (
|
||||||
|
<Chip
|
||||||
|
key={pgroup}
|
||||||
|
label={pgroup}
|
||||||
|
color={pgroup === activePgroup ? "primary" : "default"} // Highlight active pgroups
|
||||||
|
sx={{
|
||||||
|
margin: 0.5,
|
||||||
|
backgroundColor: pgroup === activePgroup ? '#19d238' : '#b0b0b0',
|
||||||
|
color: pgroup === activePgroup ? 'white' : 'black',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderRadius: '8px',
|
||||||
|
height: '20px',
|
||||||
|
fontSize: '12px',
|
||||||
|
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
|
||||||
|
mr: 1,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch beamtime records from the backend
|
||||||
|
const fetchBeamtimeRecords = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const records = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(activePgroup);
|
||||||
|
|
||||||
|
const mappedRecords: BeamtimeRecord[] = records.map((record: any) => ({
|
||||||
|
id: record.id,
|
||||||
|
start_date: record.start_date || 'N/A',
|
||||||
|
end_date: record.end_date || 'N/A',
|
||||||
|
shift: record.shift || 'N/A',
|
||||||
|
beamline: record.beamline || 'N/A',
|
||||||
|
local_contact: `${record.local_contact.firstname || "N/A"} ${record.local_contact.lastname || "N/A"}`,
|
||||||
|
pgroups: record.pgroups || '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
setRows(mappedRecords);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch beamtime records:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBeamtimeRecords();
|
||||||
|
}, [activePgroup]);
|
||||||
|
|
||||||
|
// Define table columns, including the "View Results" button
|
||||||
|
const columns: GridColDef<BeamtimeRecord>[] = [
|
||||||
|
{ field: 'start_date', headerName: 'Start Date', flex: 1 },
|
||||||
|
{ field: 'end_date', headerName: 'End Date', flex: 1 },
|
||||||
|
{ field: 'shift', headerName: "Shift", flex: 1 },
|
||||||
|
{ field: 'beamline', headerName: 'Beamline', flex: 1 },
|
||||||
|
{ field: 'local_contact', headerName: 'Local Contact', flex: 1 },
|
||||||
|
{
|
||||||
|
field: 'pgroups',
|
||||||
|
headerName: 'Pgroups',
|
||||||
|
flex: 2, // Slightly wider column for chips
|
||||||
|
renderCell: (params) => renderPgroupChips(params.row.pgroups, activePgroup),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'viewResults',
|
||||||
|
headerName: 'Actions',
|
||||||
|
flex: 1,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<button
|
||||||
|
onClick={() => handleViewResults(params.row.id)}
|
||||||
|
style={{
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#1976d2',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Results
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Navigate to the ResultsView page for the selected beamtime
|
||||||
|
const handleViewResults = (beamtimeId: number) => {
|
||||||
|
navigate(`/results/${beamtimeId}`); // Pass the beamtimeId in the URL
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: 400, width: '100%' }}>
|
||||||
|
<h2>Beamtime Overview</h2>
|
||||||
|
<DataGridPremium
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
loading={isLoading}
|
||||||
|
disableRowSelectionOnClick
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeamtimeOverview;
|
@ -1,24 +1,24 @@
|
|||||||
// components/ResultView.tsx
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import SampleTracker from '../components/SampleTracker';
|
import SampleTracker from '../components/SampleTracker';
|
||||||
import ResultGrid from '../components/ResultGrid';
|
import ResultGrid from '../components/ResultGrid';
|
||||||
|
|
||||||
interface ResultsViewProps {
|
interface ResultsViewProps {}
|
||||||
activePgroup: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResultsView: React.FC<ResultsViewProps> = ({activePgroup
|
const ResultsView: React.FC<ResultsViewProps> = () => {
|
||||||
}) => {
|
// Get the selected beamtime ID from the URL
|
||||||
|
const { beamtimeId } = useParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Results Page</h1>
|
<h1>Results Page</h1>
|
||||||
<SampleTracker activePgroup={activePgroup}/>
|
<h2>Results for Beamtime ID: {beamtimeId}</h2>
|
||||||
<ResultGrid activePgroup={activePgroup} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Use the beamtimeId to filter or query specific results */}
|
||||||
|
<SampleTracker activePgroup={`beamtime_${beamtimeId}`} />
|
||||||
|
<ResultGrid activePgroup={`beamtime_${beamtimeId}`} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ResultsView;
|
export default ResultsView;
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user