Refactor Dewar service methods and improve field handling

Updated Dewar API methods to use protected endpoints for enhanced security and consistency. Added `pgroups` handling in various frontend components and modified the LogisticsView contact field for clarity. Simplified backend router imports for better readability.
This commit is contained in:
GotthardG
2025-01-30 13:39:49 +01:00
parent 44582cf38e
commit c2215860bf
20 changed files with 304 additions and 262 deletions

View File

@ -9,6 +9,7 @@ from app.models import (
DewarType,
DewarSerialNumber,
SampleEvent,
LogisticsEvent,
)
from datetime import datetime, timedelta
import random
@ -196,11 +197,11 @@ dewars = [
tracking_number="TRACK123",
return_address_id=1,
contact_id=1,
status="Ready for Shipping",
ready_date=datetime.strptime("2023-09-30", "%Y-%m-%d"),
shipping_date=None,
arrival_date=None,
returning_date=None,
status="active",
# ready_date=datetime.strptime("2023-09-30", "%Y-%m-%d"),
# shipping_date=None,
# arrival_date=None,
# returning_date=None,
unique_id=generate_unique_id(),
),
Dewar(
@ -212,11 +213,11 @@ dewars = [
tracking_number="TRACK124",
return_address_id=2,
contact_id=2,
status="In Preparation",
ready_date=None,
shipping_date=None,
arrival_date=None,
returning_date=None,
status="active",
# ready_date=None,
# shipping_date=None,
# arrival_date=None,
# returning_date=None,
unique_id=generate_unique_id(),
),
Dewar(
@ -228,11 +229,11 @@ dewars = [
tracking_number="TRACK125",
return_address_id=1,
contact_id=3,
status="Not Shipped",
ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"),
shipping_date=None,
arrival_date=None,
returning_date=None,
status="active",
# ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"),
# shipping_date=None,
# arrival_date=None,
# returning_date=None,
unique_id=None,
),
Dewar(
@ -244,11 +245,11 @@ dewars = [
tracking_number="",
return_address_id=1,
contact_id=3,
status="Delayed",
ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"),
shipping_date=datetime.strptime("2024-01-02", "%Y-%m-%d"),
arrival_date=None,
returning_date=None,
status="active",
# ready_date=datetime.strptime("2024-01-01", "%Y-%m-%d"),
# shipping_date=datetime.strptime("2024-01-02", "%Y-%m-%d"),
# arrival_date=None,
# returning_date=None,
unique_id=None,
),
Dewar(
@ -257,16 +258,103 @@ dewars = [
dewar_name="Dewar Five",
dewar_type_id=1,
dewar_serial_number_id=1,
tracking_number="",
tracking_number="TRACK126",
return_address_id=1,
contact_id=3,
status="Returned",
arrival_date=datetime.strptime("2024-01-03", "%Y-%m-%d"),
returning_date=datetime.strptime("2024-01-07", "%Y-%m-%d"),
status="active",
# arrival_date=datetime.strptime("2024-01-03", "%Y-%m-%d"),
# returning_date=datetime.strptime("2024-01-07", "%Y-%m-%d"),
unique_id=None,
),
]
logistics_events = [
LogisticsEvent(
id=1,
event_type="in preparation",
dewar_id=1,
timestamp=datetime.strptime("2024-12-03", "%Y-%m-%d"),
),
LogisticsEvent(
id=2,
event_type="ready for shipping",
dewar_id=1,
timestamp=datetime.strptime("2024-12-03", "%Y-%m-%d"),
),
LogisticsEvent(
id=3,
event_type="shipped",
dewar_id=1,
timestamp=datetime.strptime("2024-12-04", "%Y-%m-%d"),
),
LogisticsEvent(
id=4,
event_type="arrived",
dewar_id=1,
timestamp=datetime.strptime("2024-12-06", "%Y-%m-%d"),
),
LogisticsEvent(
id=5,
event_type="returned",
dewar_id=1,
timestamp=datetime.strptime("2024-12-12", "%Y-%m-%d"),
),
LogisticsEvent(
id=6,
event_type="in preparation",
dewar_id=2,
timestamp=datetime.strptime("2025-01-06", "%Y-%m-%d"),
),
LogisticsEvent(
id=7,
event_type="ready",
dewar_id=2,
timestamp=datetime.strptime("2025-01-06", "%Y-%m-%d"),
),
LogisticsEvent(
id=8,
event_type="shipped",
dewar_id=2,
timestamp=datetime.strptime("2025-01-06", "%Y-%m-%d"),
),
LogisticsEvent(
id=9,
event_type="in preparation",
dewar_id=3,
timestamp=datetime.strptime("2025-01-06", "%Y-%m-%d"),
),
LogisticsEvent(
id=10,
event_type="ready",
dewar_id=3,
timestamp=datetime.strptime("2025-01-07", "%Y-%m-%d"),
),
LogisticsEvent(
id=11,
event_type="in preparation",
dewar_id=4,
timestamp=datetime.strptime("2025-01-12", "%Y-%m-%d"),
),
LogisticsEvent(
id=12,
event_type="ready",
dewar_id=5,
timestamp=datetime.strptime("2025-01-12", "%Y-%m-%d"),
),
LogisticsEvent(
id=12,
event_type="shipped",
dewar_id=5,
timestamp=datetime.strptime("2025-01-13", "%Y-%m-%d"),
),
LogisticsEvent(
id=12,
event_type="delayed",
dewar_id=5,
timestamp=datetime.strptime("2025-01-15", "%Y-%m-%d"),
),
]
# Define proposals
proposals = [
Proposal(id=1, number="202400125"),

View File

@ -86,13 +86,9 @@ class Dewar(Base):
dewar_serial_number_id = Column(
Integer, ForeignKey("dewar_serial_numbers.id"), nullable=True
)
tracking_number = Column(String(255), nullable=True)
status = Column(String(255), nullable=True)
ready_date = Column(Date, nullable=True)
shipping_date = Column(Date, nullable=True)
arrival_date = Column(Date, nullable=True)
returning_date = Column(Date, nullable=True)
unique_id = Column(String(255), unique=True, index=True, nullable=True)
tracking_number = Column(String(255), nullable=True)
shipment_id = Column(Integer, ForeignKey("shipments.id"))
return_address_id = Column(Integer, ForeignKey("addresses.id"))
contact_id = Column(Integer, ForeignKey("contacts.id"))

View File

@ -1,7 +1,7 @@
from .address import address_router
from .contact import contact_router
from .proposal import router as proposal_router
from .dewar import router as dewar_router
from .dewar import dewar_router
from .shipment import shipment_router
from .auth import router as auth_router
from .protected_router import protected_router as protected_router

View File

@ -9,7 +9,6 @@ from typing import List
import logging
from sqlalchemy.exc import SQLAlchemyError
from app.schemas import (
Dewar as DewarSchema,
DewarCreate,
DewarUpdate,
DewarType as DewarTypeSchema,
@ -21,7 +20,8 @@ from app.schemas import (
SampleUpdate,
Sample,
Puck,
SampleEventResponse, # Clearer name for schema
SampleEventResponse,
DewarSchema, # Clearer name for schema
)
from app.models import (
Dewar as DewarModel,
@ -42,11 +42,11 @@ from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib.units import cm
from reportlab.pdfgen import canvas
from app.crud import (
get_shipments,
get_shipment_by_id,
) # Import CRUD functions for shipment
)
router = APIRouter()
dewar_router = APIRouter()
def generate_unique_id(db: Session, length: int = 16) -> str:
@ -63,12 +63,12 @@ def generate_unique_id(db: Session, length: int = 16) -> str:
return unique_id
@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED)
@dewar_router.post("/", response_model=Dewar, status_code=status.HTTP_201_CREATED)
async def create_or_update_dewar(
shipment_id: int,
dewar: DewarCreate,
db: Session = Depends(get_db),
) -> DewarSchema:
):
try:
# Query existing dewar by name within the shipment
existing_dewar = (
@ -170,14 +170,11 @@ async def create_or_update_dewar(
# Create a completely new dewar if none exists
dewar_obj = DewarModel(
pgroups=dewar.pgroups,
dewar_name=dewar.dewar_name,
tracking_number=dewar.tracking_number,
status=dewar.status,
ready_date=dewar.ready_date,
shipping_date=dewar.shipping_date,
arrival_date=dewar.arrival_date,
returning_date=dewar.returning_date,
contact_person_id=dewar.contact_person_id,
contact_id=dewar.contact_id,
return_address_id=dewar.return_address_id,
shipment_id=shipment_id, # Associate with the shipment
)
@ -220,7 +217,7 @@ async def create_or_update_dewar(
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{dewar_id}/generate-qrcode")
@dewar_router.post("/{dewar_id}/generate-qrcode")
async def generate_dewar_qrcode(dewar_id: int, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
@ -365,7 +362,7 @@ def generate_label(dewar):
return buffer
@router.get("/{dewar_id}/download-label", response_class=Response)
@dewar_router.get("/{dewar_id}/download-label", response_class=Response)
async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
dewar = (
db.query(DewarModel)
@ -397,7 +394,7 @@ async def download_dewar_label(dewar_id: int, db: Session = Depends(get_db)):
)
@router.put("/samples/{sample_id}", response_model=Sample)
@dewar_router.put("/samples/{sample_id}", response_model=Sample)
async def update_sample(
sample_id: int,
sample_update: SampleUpdate,
@ -431,7 +428,7 @@ async def update_sample(
return Sample.from_orm(sample)
@router.get("/dewars/{dewar_id}/samples", response_model=Dewar)
@dewar_router.get("/dewars/{dewar_id}/samples", response_model=Dewar)
async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
# Fetch the Dewar with nested relationships
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
@ -441,6 +438,7 @@ async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
# Explicitly map nested relationships
dewar_data = {
"id": dewar.id,
"pgroups": dewar.pgroups,
"dewar_name": dewar.dewar_name,
"tracking_number": dewar.tracking_number,
"status": dewar.status,
@ -448,14 +446,10 @@ async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
"number_of_samples": sum(
len(puck.samples) for puck in dewar.pucks
), # Calculate total samples
"ready_date": dewar.ready_date,
"shipping_date": dewar.shipping_date,
"arrival_date": dewar.arrival_date,
"returning_date": dewar.returning_date,
"contact_person_id": dewar.contact_person.id if dewar.contact_person else None,
"contact_id": dewar.contact.id if dewar.contact else None,
"return_address_id": dewar.return_address.id if dewar.return_address else None,
"shipment_id": dewar.shipment_id,
"contact_person": dewar.contact_person,
"contact": dewar.contact,
"return_address": dewar.return_address,
"pucks": [
Puck(
@ -492,7 +486,7 @@ async def get_dewar_samples(dewar_id: int, db: Session = Depends(get_db)):
return Dewar(**dewar_data)
@router.get("/", response_model=List[DewarSchema])
@dewar_router.get("/", response_model=List[Dewar])
async def get_dewars(db: Session = Depends(get_db)):
try:
dewars = db.query(DewarModel).options(joinedload(DewarModel.pucks)).all()
@ -502,12 +496,12 @@ async def get_dewars(db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/dewar-types", response_model=List[DewarTypeSchema])
@dewar_router.get("/dewar-types", response_model=List[DewarTypeSchema])
def get_dewar_types(db: Session = Depends(get_db)):
return db.query(DewarTypeModel).all()
@router.get(
@dewar_router.get(
"/dewar-types/{type_id}/serial-numbers",
response_model=List[DewarSerialNumberSchema],
)
@ -519,7 +513,7 @@ def get_serial_numbers(type_id: int, db: Session = Depends(get_db)):
)
@router.post("/dewar-types", response_model=DewarTypeSchema)
@dewar_router.post("/dewar-types", response_model=DewarTypeSchema)
def create_dewar_type(dewar_type: DewarTypeCreate, db: Session = Depends(get_db)):
db_type = DewarTypeModel(**dewar_type.dict())
db.add(db_type)
@ -528,7 +522,7 @@ def create_dewar_type(dewar_type: DewarTypeCreate, db: Session = Depends(get_db)
return db_type
@router.post("/dewar-serial-numbers", response_model=DewarSerialNumberSchema)
@dewar_router.post("/dewar-serial-numbers", response_model=DewarSerialNumberSchema)
def create_dewar_serial_number(
serial_number: DewarSerialNumberCreate, db: Session = Depends(get_db)
):
@ -539,7 +533,7 @@ def create_dewar_serial_number(
return db_serial
@router.get("/dewar-serial-numbers", response_model=List[DewarSerialNumberSchema])
@dewar_router.get("/dewar-serial-numbers", response_model=List[DewarSerialNumberSchema])
def get_all_serial_numbers(db: Session = Depends(get_db)):
try:
serial_numbers = db.query(DewarSerialNumberModel).all()
@ -549,13 +543,13 @@ def get_all_serial_numbers(db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{dewar_id}", response_model=DewarSchema)
@dewar_router.get("/{dewar_id}", response_model=Dewar)
async def get_dewar(dewar_id: int, db: Session = Depends(get_db)):
dewar = (
db.query(DewarModel)
.options(
joinedload(DewarModel.pucks).joinedload(PuckModel.samples),
joinedload(DewarModel.contact_person),
joinedload(DewarModel.contact),
joinedload(DewarModel.return_address),
joinedload(DewarModel.shipment),
)
@ -569,10 +563,10 @@ async def get_dewar(dewar_id: int, db: Session = Depends(get_db)):
return DewarSchema.from_orm(dewar)
@router.put("/{dewar_id}", response_model=DewarSchema)
@dewar_router.put("/{dewar_id}", response_model=Dewar)
async def update_dewar(
dewar_id: int, dewar_update: DewarUpdate, db: Session = Depends(get_db)
) -> DewarSchema:
) -> Dewar:
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
@ -587,7 +581,7 @@ async def update_dewar(
return dewar
@router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT)
@dewar_router.delete("/{dewar_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
# Fetch the Dewar from the database
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
@ -642,18 +636,7 @@ async def delete_dewar(dewar_id: int, db: Session = Depends(get_db)):
return
# New routes for shipments
@router.get("/shipments", response_model=List[ShipmentSchema])
async def get_all_shipments(db: Session = Depends(get_db)):
try:
shipments = get_shipments(db)
return shipments
except SQLAlchemyError as e:
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/shipments/{id}", response_model=ShipmentSchema)
@dewar_router.get("/shipments/{id}", response_model=ShipmentSchema)
async def get_single_shipment(id: int, db: Session = Depends(get_db)):
try:
shipment = get_shipment_by_id(db, id)

View File

@ -263,9 +263,9 @@ async def get_all_slots(db: Session = Depends(get_db)):
# Correct the contact_person assignment
contact_person = None
if slot.dewar and slot.dewar.contact_person:
first_name = slot.dewar.contact_person.firstname
last_name = slot.dewar.contact_person.lastname
if slot.dewar and slot.dewar.contact:
first_name = slot.dewar.contact.firstname
last_name = slot.dewar.contact.lastname
contact_person = f"{first_name} {last_name}"
# Prepare the slot data for the response
@ -287,7 +287,7 @@ async def get_all_slots(db: Session = Depends(get_db)):
if slot.dewar and slot.dewar.shipment
else None
),
contact_person=contact_person,
contact=contact_person,
local_contact="local contact placeholder",
)
# Add updated slot data to the response list

View File

@ -4,6 +4,7 @@ from app.routers.auth import get_current_user
from app.routers.address import address_router
from app.routers.contact import contact_router
from app.routers.shipment import shipment_router
from app.routers.dewar import dewar_router
protected_router = APIRouter(
dependencies=[Depends(get_current_user)] # Applies to all routes
@ -14,3 +15,4 @@ protected_router.include_router(contact_router, prefix="/contacts", tags=["conta
protected_router.include_router(
shipment_router, prefix="/shipments", tags=["shipments"]
)
protected_router.include_router(dewar_router, prefix="/dewars", tags=["dewars"])

View File

@ -251,6 +251,9 @@ async def get_pucks_with_tell_position(db: Session = Depends(get_db)):
dewar_name=str(dewar.dewar_name)
if dewar and dewar.dewar_name
else None,
pgroup=str(dewar.pgroups)
if dewar.pgroups
else None, # will be replaced later by puck pgroup
samples=[
Sample(
id=sample.id,
@ -413,6 +416,7 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
dewar_ids = [dewar.id for dewar in dewars]
dewar_map = {dewar.id: dewar.dewar_name for dewar in dewars}
dewar_pgroups = {dewar.id: dewar.pgroups for dewar in dewars}
# Subquery to fetch the latest event for each puck (any type of event)
latest_event_subquery = (
@ -430,9 +434,9 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
PuckModel,
PuckEventModel.event_type,
PuckEventModel.tell_position,
DewarModel, # Include DewarModel
)
.join( # Join pucks with the latest event
# (outer join to include pucks without events)
latest_event_subquery,
PuckModel.id == latest_event_subquery.c.puck_id,
isouter=True,
@ -443,6 +447,11 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
& (PuckEventModel.timestamp == latest_event_subquery.c.latest_event_time),
isouter=True,
)
.join( # Join with DewarModel to get dewar details
DewarModel,
PuckModel.dewar_id == DewarModel.id,
isouter=True,
)
.filter(PuckModel.dewar_id.in_(dewar_ids)) # Restrict pucks to relevant dewars
.all()
)
@ -458,13 +467,16 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
# Prepare the final response
results = []
for puck, event_type, tell_position in pucks_with_latest_events:
for puck, event_type, dewar, tell_position in pucks_with_latest_events:
logger.debug(
f"Puck ID: {puck.id}, Name: {puck.puck_name}, Event Type: {event_type}, "
f"Tell Position: {tell_position}"
)
dewar_name = dewar_map.get(puck.dewar_id, "Unknown")
pgroup = dewar_pgroups.get(
puck.dewar_id
) # will be replaced later by puck pgroup
# For pucks with no events or whose latest event is `puck_removed`, set
# `tell_position` to None
@ -475,6 +487,7 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
results.append(
PuckWithTellPosition(
id=puck.id,
pgroup=pgroup,
puck_name=puck.puck_name,
puck_type=puck.puck_type,
puck_location_in_dewar=int(puck.puck_location_in_dewar)

View File

@ -111,7 +111,7 @@ async def upload_sample_images(
raise HTTPException(status_code=404, detail="Sample not found")
# 2. Define Directory Structure
username = "e16371" # Hardcoded username; replace with dynamic logic if applicable
pgroup = sample.puck.dewar.pgroups
today = datetime.now().strftime("%Y-%m-%d")
dewar_name = (
sample.puck.dewar.dewar_name
@ -120,7 +120,7 @@ async def upload_sample_images(
)
puck_name = sample.puck.puck_name if sample.puck else "default_puck"
position = sample.position if sample.position else "default_position"
base_dir = Path(f"images/{username}/{today}/{dewar_name}/{puck_name}/{position}")
base_dir = Path(f"images/{pgroup}/{today}/{dewar_name}/{puck_name}/{position}")
base_dir.mkdir(parents=True, exist_ok=True)
# 3. Process and Save Each File

View File

@ -3,6 +3,8 @@ from sqlalchemy.orm import Session
from typing import List
from datetime import date
import json
import logging
from sqlalchemy.exc import SQLAlchemyError
from app.models import (
Shipment as ShipmentModel,
@ -20,11 +22,11 @@ from app.schemas import (
Shipment as ShipmentSchema,
Contact as ContactSchema,
Sample as SampleSchema,
DewarSchema,
loginData,
Dewar,
)
from app.database import get_db
from app.crud import get_shipment_by_id
from app.crud import get_shipment_by_id, get_shipments
from app.routers.auth import get_current_user
shipment_router = APIRouter()
@ -59,7 +61,17 @@ async def fetch_shipments(
return shipments
@shipment_router.get("/{shipment_id}/dewars", response_model=List[DewarSchema])
@shipment_router.get("/shipments", response_model=List[ShipmentSchema])
async def get_all_shipments(db: Session = Depends(get_db)):
try:
shipments = get_shipments(db)
return shipments
except SQLAlchemyError as e:
logging.error(f"Database error occurred: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@shipment_router.get("/{shipment_id}/dewars", response_model=List[Dewar])
async def get_dewars_by_shipment_id(shipment_id: int, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.id == shipment_id).first()
if not shipment:

View File

@ -429,7 +429,7 @@ class Sample(BaseModel):
priority: Optional[int] = None
comments: Optional[str] = None
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
@ -497,6 +497,7 @@ class Puck(BaseModel):
class DewarBase(BaseModel):
pgroups: str
dewar_name: str
dewar_type_id: Optional[int] = None
dewar_serial_number_id: Optional[int] = None
@ -505,10 +506,6 @@ class DewarBase(BaseModel):
number_of_pucks: Optional[int] = None
number_of_samples: Optional[int] = None
status: str
ready_date: Optional[date]
shipping_date: Optional[date]
arrival_date: Optional[date]
returning_date: Optional[date]
contact_id: Optional[int]
return_address_id: Optional[int]
pucks: List[PuckCreate] = []
@ -534,22 +531,20 @@ class Dewar(DewarBase):
class DewarUpdate(BaseModel):
pgroups: str
dewar_name: Optional[str] = None
dewar_type_id: Optional[int] = None
dewar_serial_number_id: Optional[int] = None
unique_id: Optional[str] = None
tracking_number: Optional[str] = None
status: Optional[str] = None
ready_date: Optional[date] = None
shipping_date: Optional[date] = None
arrival_date: Optional[date] = None
returning_date: Optional[date] = None
contact_id: Optional[int] = None
address_id: Optional[int] = None
class DewarSchema(BaseModel):
id: int
pgroups: str
dewar_name: str
tracking_number: str
status: str
@ -559,6 +554,9 @@ class DewarSchema(BaseModel):
class Config:
from_attributes = True
# shipping status etc will become a logistics event.
# Tracking will also become an event
class Proposal(BaseModel):
id: int
@ -681,7 +679,7 @@ class PuckWithTellPosition(BaseModel):
dewar_name: Optional[
str
] # was changed to optional but probably needs to be not optional
user: str = "e16371"
pgroup: str
samples: Optional[List[Sample]] = None
tell_position: Optional[str]