Add Image models and clean up test code structure

Introduced `ImageCreate` and `Image` models to handle image-related data in the backend. Improved the organization and readability of the testing notebook by consolidating and formatting code into distinct sections with markdown cells.
This commit is contained in:
GotthardG 2025-02-26 15:11:20 +01:00
parent 1606e80f81
commit b04c7b8c95
8 changed files with 191 additions and 11 deletions

View File

@ -11,12 +11,14 @@ from app.schemas import (
Sample, Sample,
Image, Image,
ImageCreate, ImageCreate,
SampleResult,
) )
from app.models import ( from app.models import (
Puck as PuckModel, Puck as PuckModel,
Sample as SampleModel, Sample as SampleModel,
SampleEvent as SampleEventModel, SampleEvent as SampleEventModel,
Image as ImageModel, Image as ImageModel,
Dewar as DewarModel,
) )
from app.dependencies import get_db from app.dependencies import get_db
import logging import logging
@ -165,3 +167,40 @@ async def upload_sample_image(
# Returning the mapped SQLAlchemy object, which will be converted to the # Returning the mapped SQLAlchemy object, which will be converted to the
# Pydantic response model. # Pydantic response model.
return new_image return new_image
@router.get("/results", response_model=List[SampleResult])
async def get_sample_results(active_pgroup: str, db: Session = Depends(get_db)):
# Query samples for the active pgroup using joins
samples = (
db.query(SampleModel)
.join(SampleModel.puck)
.join(PuckModel.dewar)
.filter(DewarModel.pgroups == active_pgroup)
.all()
)
if not samples:
raise HTTPException(
status_code=404, detail="No samples found for the active pgroup"
)
results = []
for sample in samples:
# Query images associated with each sample
images = db.query(ImageModel).filter(ImageModel.sample_id == sample.id).all()
results.append(
{
"sample_id": sample.id,
"sample_name": sample.sample_name,
"puck_name": sample.puck.puck_name if sample.puck else None,
"dewar_name": sample.puck.dewar.dewar_name
if (sample.puck and sample.puck.dewar)
else None,
"images": [
{"id": img.id, "filepath": img.filepath, "comment": img.comment}
for img in images
],
}
)
return results

View File

@ -804,3 +804,17 @@ class Image(ImageCreate):
class Config: class Config:
from_attributes = True from_attributes = True
class ImageInfo(BaseModel):
id: int
filepath: str
comment: Optional[str] = None
class SampleResult(BaseModel):
sample_id: int
sample_name: str
puck_name: Optional[str]
dewar_name: Optional[str]
images: List[ImageInfo]

View File

@ -4,6 +4,7 @@ import tomllib
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app import ssl_heidi from app import ssl_heidi
from app.routers import ( from app.routers import (
proposal, proposal,
@ -159,6 +160,9 @@ app.include_router(spreadsheet.router, tags=["spreadsheet"])
app.include_router(logistics.router, prefix="/logistics", tags=["logistics"]) app.include_router(logistics.router, prefix="/logistics", tags=["logistics"])
app.include_router(sample.router, prefix="/samples", tags=["samples"]) app.include_router(sample.router, prefix="/samples", tags=["samples"])
app.mount("/images", StaticFiles(directory="images"), name="images")
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from dotenv import load_dotenv from dotenv import load_dotenv

View File

@ -84,7 +84,7 @@ 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 />} />} /> <Route path="/results" element={<ProtectedRoute element={<ResultsView pgroups={pgroups} activePgroup={activePgroup} />} />} />
</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} />

View File

@ -0,0 +1,114 @@
import React, { useEffect, useState } from 'react';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { OpenAPI, SamplesService } from '../../openapi';
interface ImageInfo {
id: number;
filepath: string;
comment?: string;
}
interface SampleResult {
sample_id: number;
sample_name: string;
puck_name?: string;
dewar_name?: string;
images: ImageInfo[];
}
interface ResultGridProps {
activePgroup: string;
}
const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
const [rows, setRows] = useState<SampleResult[]>([]);
useEffect(() => {
console.log("Fetching sample results for active_pgroup:", activePgroup);
SamplesService.getSampleResultsSamplesResultsGet(activePgroup)
.then((response: SampleResult[]) => {
console.log("Response received:", response);
setRows(response);
})
.catch((err: Error) => {
console.error('Error fetching sample results:', err);
});
}, [activePgroup]);
const columns: GridColDef[] = [
{ field: 'sample_id', headerName: 'ID', width: 70 },
{ field: 'sample_name', headerName: 'Sample Name', width: 150 },
{ field: 'puck_name', headerName: 'Puck Name', width: 150 },
{ field: 'dewar_name', headerName: 'Dewar Name', width: 150 },
{
field: 'images',
headerName: 'Images',
width: 300,
renderCell: (params) => {
const imageList: ImageInfo[] = params.value;
if (imageList && imageList.length) {
const primaryImage = imageList[0];
// Define the base path to your backend images directory
const basePath = "https://localhost:8000/";
const imageUrl = basePath + primaryImage.filepath;
console.log("Local relative path:", imageUrl);
console.log("Updated image URL:", imageUrl);
return (
<div style={{display: 'flex', alignItems: 'center', position: 'relative'}}>
<img
src={imageUrl}
alt="sample"
style={{width: 50, height: 50, marginRight: 5, borderRadius: 4}}
/>
{imageList.length > 1 && (
<div className="tooltip" style={{position: 'relative', cursor: 'pointer'}}>
<span>+{imageList.length - 1}</span>
<div
className="tooltip-content"
style={{
display: 'none',
position: 'absolute',
top: '60px',
left: 0,
background: '#fff',
padding: '5px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
zIndex: 100,
}}
>
{imageList.slice(1).map((img) => {
const tooltipImageUrl = basePath + img.filepath;
return (
<img
key={img.id}
src={tooltipImageUrl}
alt="sample"
style={{width: 50, height: 50, margin: 2, borderRadius: 4}}
/>
);
})}
</div>
</div>
)}
</div>
);
}
return null;
},
},
];
// Map each row so that DataGrid can use a unique "id" prop; here we use sample_id.
const gridRows = rows.map((row) => ({ ...row, id: row.sample_id }));
return (
<div style={{ height: 500, width: '100%' }}>
<DataGrid rows={gridRows} columns={columns} pageSize={5} />
</div>
);
};
export default ResultGrid;

View File

@ -50,7 +50,7 @@ const SampleTracker: React.FC = () => {
// Set up polling every 1 second // Set up polling every 1 second
const interval = setInterval(() => { const interval = setInterval(() => {
fetchPucks(); fetchPucks();
}, 1000); }, 100000);
// Clear interval on component unmount // Clear interval on component unmount
return () => clearInterval(interval); return () => clearInterval(interval);

View File

@ -2,13 +2,22 @@
import React from 'react'; import React from 'react';
import SampleTracker from '../components/SampleTracker'; import SampleTracker from '../components/SampleTracker';
import ResultGrid from '../components/ResultGrid';
interface ResultsViewProps {
activePgroup: string;
}
const ResultsView: React.FC<ResultsViewProps> = ({activePgroup
}) => {
const ResultsView: React.FC = () => {
return ( return (
<div> <div>
<h1>Results Page</h1> <h1>Results Page</h1>
<SampleTracker /> <SampleTracker />
<ResultGrid activePgroup={activePgroup} />
</div> </div>
); );
}; };

View File

@ -3,8 +3,8 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-02-26T12:02:38.993926Z", "end_time": "2025-02-26T13:09:58.719218Z",
"start_time": "2025-02-26T12:02:38.991283Z" "start_time": "2025-02-26T13:09:58.716771Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -46,7 +46,7 @@
] ]
} }
], ],
"execution_count": 74 "execution_count": 86
}, },
{ {
"metadata": {}, "metadata": {},
@ -528,8 +528,8 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-02-26T12:29:51.615501Z", "end_time": "2025-02-26T13:17:13.591355Z",
"start_time": "2025-02-26T12:29:51.592886Z" "start_time": "2025-02-26T13:17:13.561947Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -546,7 +546,7 @@
" mime_type = \"application/octet-stream\"\n", " mime_type = \"application/octet-stream\"\n",
"\n", "\n",
"# Sample ID (ensure this exists on your backend)\n", "# Sample ID (ensure this exists on your backend)\n",
"sample_id = 58\n", "sample_id = 16\n",
"\n", "\n",
"# Build the URL for the upload endpoint.\n", "# Build the URL for the upload endpoint.\n",
"url = f\"https://127.0.0.1:8000/samples/{sample_id}/upload-images\"\n", "url = f\"https://127.0.0.1:8000/samples/{sample_id}/upload-images\"\n",
@ -580,7 +580,7 @@
"text": [ "text": [
"API Response:\n", "API Response:\n",
"200\n", "200\n",
"{'pgroup': 'p20001, p20002', 'sample_id': 58, 'filepath': 'images/p20001, p20002/2025-02-26/Dewar One/PUCK007/12/IMG_1942.jpg', 'status': 'active', 'comment': None, 'id': 1}\n" "{'pgroup': 'p20001', 'sample_id': 16, 'filepath': 'images/p20001/2025-02-26/Dewar One/PUCK-001/16/IMG_1942.jpg', 'status': 'active', 'comment': None, 'id': 3}\n"
] ]
}, },
{ {
@ -592,7 +592,7 @@
] ]
} }
], ],
"execution_count": 85 "execution_count": 88
}, },
{ {
"metadata": {}, "metadata": {},