**Commit Message:**

Enhance app with active pgroup handling and token updates

Added active pgroup state management across the app for user-specific settings. Improved token handling with decoding, saving user data, and setting OpenAPI authorization. Updated components, API calls, and forms to support dynamic pgroup selection and user-specific features.
This commit is contained in:
GotthardG 2025-01-22 13:55:26 +01:00
parent 4630bcfac5
commit 6cde57f783
23 changed files with 806 additions and 250 deletions

View File

@ -110,36 +110,56 @@ contacts = [
return_addresses = [
Address(
id=1,
street="123 Hobbiton St",
city="Shire",
pgroups="p20000, p20002",
status="active",
house_number="123",
street="Hobbiton St",
city="Hobbitbourg",
state="Shire",
zipcode="12345",
country="Middle Earth",
),
Address(
id=2,
street="456 Rohan Rd",
pgroups="p20000, p20001",
status="active",
house_number="456",
street="Rohan Rd",
city="Edoras",
state="Rohan",
zipcode="67890",
country="Middle Earth",
),
Address(
id=3,
street="789 Greenwood Dr",
pgroups="p20001, p20002",
status="active",
house_number="789",
street="Greenwood Dr",
city="Mirkwood",
state="Greenwood",
zipcode="13579",
country="Middle Earth",
),
Address(
id=4,
street="321 Gondor Ave",
pgroups="p20001, p20002, p20003",
status="active",
house_number="321",
street="Gondor Ave",
city="Minas Tirith",
state="Gondor",
zipcode="24680",
country="Middle Earth",
),
Address(
id=5,
street="654 Falgorn Pass",
pgroups="p20004, p20005",
status="active",
house_number="654",
street="Falgorn Pass",
city="Rivendell",
state="Rivendell",
zipcode="11223",
country="Middle Earth",
),
@ -234,11 +254,11 @@ dewars = [
# Define proposals
proposals = [
Proposal(id=1, number="p20000"),
Proposal(id=2, number="p20001"),
Proposal(id=3, number="p20002"),
Proposal(id=4, number="p20003"),
Proposal(id=5, number="p20004"),
Proposal(id=1, number="202400125"),
Proposal(id=2, number="202400235"),
Proposal(id=3, number="202400237"),
Proposal(id=4, number="202400336"),
Proposal(id=5, number="202400255"),
]
# Define shipment specific dewars

View File

@ -46,10 +46,14 @@ class Address(Base):
__tablename__ = "addresses"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
street = Column(String(255))
city = Column(String(255))
zipcode = Column(String(255))
country = Column(String(255))
status = Column(String(255), default="active")
pgroups = Column(String(255), nullable=False)
street = Column(String(255), nullable=False)
house_number = Column(String(255), nullable=True)
city = Column(String(255), nullable=False)
state = Column(String(255), nullable=True)
zipcode = Column(String(255), nullable=False)
country = Column(String(255), nullable=False)
shipments = relationship("Shipment", back_populates="return_address")

View File

@ -1,9 +1,10 @@
from .address import router as address_router
from .address import protected_router as address_router
from .contact import router as contact_router
from .proposal import router as proposal_router
from .dewar import router as dewar_router
from .shipment import router as shipment_router
from .auth import router as auth_router
from .protected_router import protected_router as protected_router
__all__ = [
"address_router",
@ -12,4 +13,5 @@ __all__ = [
"dewar_router",
"shipment_router",
"auth_router",
"protected_router",
]

View File

@ -1,20 +1,63 @@
from fastapi import APIRouter, HTTPException, status, Depends
from fastapi import Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import List
from app.schemas import Address as AddressSchema, AddressCreate, AddressUpdate
from app.routers.auth import get_current_user
from app.schemas import (
Address as AddressSchema,
AddressCreate,
AddressUpdate,
loginData,
)
from app.models import Address as AddressModel
from app.dependencies import get_db
router = APIRouter()
from app.routers.protected_router import protected_router
@router.get("/", response_model=List[AddressSchema])
async def get_return_addresses(db: Session = Depends(get_db)):
return db.query(AddressModel).all()
@protected_router.get("/", response_model=List[AddressSchema])
async def get_return_addresses(
active_pgroup: str = Query(...),
db: Session = Depends(get_db),
current_user: loginData = Depends(get_current_user),
):
if active_pgroup not in current_user.pgroups:
raise HTTPException(status_code=400, detail="Invalid pgroup provided.")
# Return only active addresses
user_addresses = (
db.query(AddressModel)
.filter(
AddressModel.pgroups.like(f"%{active_pgroup}%"),
AddressModel.status == "active",
)
.all()
)
return user_addresses
@router.post("/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED)
@protected_router.get("/all", response_model=List[AddressSchema])
async def get_all_addresses(
db: Session = Depends(get_db),
current_user: loginData = Depends(get_current_user),
):
# Fetch all active addresses associated with the user's pgroups
user_pgroups = current_user.pgroups
filters = [AddressModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
user_addresses = (
db.query(AddressModel)
.filter(AddressModel.status == "active", or_(*filters))
.all()
)
return user_addresses
@protected_router.post(
"/", response_model=AddressSchema, status_code=status.HTTP_201_CREATED
)
async def create_return_address(address: AddressCreate, db: Session = Depends(get_db)):
print("Payload received by backend:", address.dict()) # Log incoming payload
if db.query(AddressModel).filter(AddressModel.city == address.city).first():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@ -22,10 +65,14 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
)
db_address = AddressModel(
pgroups=address.pgroups,
house_number=address.house_number,
street=address.street,
city=address.city,
state=address.state,
zipcode=address.zipcode,
country=address.country,
status="active",
)
db.add(db_address)
@ -34,29 +81,74 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
return db_address
@router.put("/{address_id}", response_model=AddressSchema)
@protected_router.put("/{address_id}", response_model=AddressSchema)
async def update_return_address(
address_id: int, address: AddressUpdate, db: Session = Depends(get_db)
):
# Retrieve the existing address
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
)
for key, value in address.dict(exclude_unset=True).items():
setattr(db_address, key, value)
# Normalize existing and new pgroups (remove whitespace, handle case
# sensitivity if needed)
existing_pgroups = (
set(p.strip() for p in db_address.pgroups.split(",") if p.strip())
if db_address.pgroups
else set()
)
new_pgroups = (
set(p.strip() for p in address.pgroups.split(",") if p.strip())
if address.pgroups
else set()
)
# Check if any old pgroups are being removed (strict validation against removal)
if not new_pgroups.issuperset(existing_pgroups):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Modifying pgroups to remove existing ones is not allowed.",
)
# Combine existing and new pgroups (only additions are allowed)
combined_pgroups = existing_pgroups.union(new_pgroups)
# Mark the current address as obsolete
db_address.status = "inactive"
db.commit()
db.refresh(db_address)
return db_address
# Create a new address with updated values and the combined pgroups
new_address = AddressModel(
pgroups=",".join(combined_pgroups), # Join set back into comma-separated string
house_number=address.house_number or db_address.house_number,
street=address.street or db_address.street,
city=address.city or db_address.city,
state=address.state or db_address.state,
zipcode=address.zipcode or db_address.zipcode,
country=address.country or db_address.country,
status="active", # Newly created address will be active
)
# Save the new address
db.add(new_address)
db.commit()
db.refresh(new_address)
return new_address
@router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
@protected_router.delete("/{address_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_return_address(address_id: int, db: Session = Depends(get_db)):
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found."
)
db.delete(db_address)
# Mark the address as obsolete instead of deleting it
db_address.status = "inactive"
db.commit()
return

View File

@ -14,8 +14,13 @@ mock_users_db = {
"testuser": {
"username": "testuser",
"password": "testpass", # In a real scenario, store the hash of the password
"pgroups": [20000, 20001, 20003],
}
"pgroups": ["p20000", "p20001", "p20002", "p20003"],
},
"testuser2": {
"username": "testuser2",
"password": "testpass2", # In a real scenario, store the hash of the password
"pgroups": ["p20004", "p20005", "p20006"],
},
}
@ -39,30 +44,17 @@ def create_access_token(data: dict) -> str:
async def get_current_user(token: str = Depends(oauth2_scheme)) -> loginData:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token_expired_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token expired",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
pgroups = payload.get("pgroups")
if username is None:
raise credentials_exception
token_data = loginData(username=username, pgroups=pgroups)
print(f"[DEBUG] Username decoded from token: {username}") # Add debug log here
return loginData(username=username, pgroups=payload.get("pgroups"))
except jwt.ExpiredSignatureError:
raise token_expired_exception
print("[DEBUG] Token expired")
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise credentials_exception
return token_data
print("[DEBUG] Invalid token")
raise HTTPException(status_code=401, detail="Invalid token")
@router.post("/token/login", response_model=loginToken)

View File

@ -0,0 +1,7 @@
from fastapi import APIRouter, Depends
from app.routers.auth import get_current_user
protected_router = APIRouter(
dependencies=[Depends(get_current_user)] # Applies to all routes
)

View File

@ -7,7 +7,6 @@ import shutil
from app.schemas import (
Puck as PuckSchema,
Sample as SampleSchema,
SampleEventResponse,
SampleEventCreate,
Sample,
)
@ -90,106 +89,69 @@ async def create_sample_event(
return sample # Return the sample, now including `mount_count`
# Route to fetch the last (most recent) sample event
@router.get("/samples/{sample_id}/events/last", response_model=SampleEventResponse)
async def get_last_sample_event(sample_id: int, db: Session = Depends(get_db)):
# Ensure the sample exists
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample:
raise HTTPException(status_code=404, detail="Sample not found")
# Get the most recent event for the sample
last_event = (
db.query(SampleEventModel)
.filter(SampleEventModel.sample_id == sample_id)
.order_by(SampleEventModel.timestamp.desc())
.first()
)
if not last_event:
raise HTTPException(status_code=404, detail="No events found for the sample")
return SampleEventResponse(
id=last_event.id,
sample_id=last_event.sample_id,
event_type=last_event.event_type,
timestamp=last_event.timestamp,
) # Response will automatically use the SampleEventResponse schema
@router.post("/samples/{sample_id}/upload-images")
async def upload_sample_images(
sample_id: int,
uploaded_files: List[UploadFile] = File(...), # Accept multiple files
uploaded_files: list[UploadFile] = File(...),
db: Session = Depends(get_db),
):
"""
Uploads images for a sample and stores them in a directory structure:
images/user/date/dewar_name/puck_name/position/.
logging.info(f"Received files: {[file.filename for file in uploaded_files]}")
"""
Uploads images for a given sample and saves them to a directory structure.
Args:
sample_id (int): ID of the sample.
uploaded_files (List[UploadFile]): List of image files to be uploaded.
db (Session): SQLAlchemy database session.
uploaded_files (list[UploadFile]): A list of files uploaded with the request.
db (Session): Database session.
"""
# Fetch sample details from the database
# 1. Validate Sample
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample:
raise HTTPException(status_code=404, detail="Sample not found")
# Retrieve associated dewar_name, puck_name and position
puck = sample.puck
if not puck:
raise HTTPException(
status_code=404, detail=f"No puck associated with sample ID {sample_id}"
)
dewar_name = puck.dewar.dewar_name if puck.dewar else None
if not dewar_name:
raise HTTPException(
status_code=404, detail=f"No dewar associated with puck ID {puck.id}"
)
puck_name = puck.puck_name
position = sample.position
# Retrieve username (hardcoded for now—can be fetched dynamically if needed)
username = "e16371"
# Today's date in the format YYYY-MM-DD
# 2. Define Directory Structure
username = "e16371" # Hardcoded username; replace with dynamic logic if applicable
today = datetime.now().strftime("%Y-%m-%d")
# Generate the directory path based on the structure
base_dir = (
Path("images") / username / today / dewar_name / puck_name / str(position)
dewar_name = (
sample.puck.dewar.dewar_name
if sample.puck and sample.puck.dewar
else "default_dewar"
)
# Create directories if they don't exist
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.mkdir(parents=True, exist_ok=True)
# Save each uploaded image to the directory
# 3. Process and Save Each File
saved_files = []
for file in uploaded_files:
# Validate file content type
# Validate MIME type
if not file.content_type.startswith("image/"):
raise HTTPException(
status_code=400,
detail=f"Invalid file type: {file.filename}. Must be an image.",
detail=f"Invalid file type: {file.filename}. Only images are accepted.",
)
# Create a file path for storing the uploaded file
# Save file to the base directory
file_path = base_dir / file.filename
# Save the file from the file stream
try:
# Save the file
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
saved_files.append(str(file_path)) # Track saved file paths
except Exception as e:
logging.error(f"Error saving file {file.filename}: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Error saving file {file.filename}: {str(e)}",
detail=f"Could not save file {file.filename}."
f" Ensure the server has correct permissions.",
)
# 4. Return Saved Files Information
logging.info(f"Uploaded {len(saved_files)} files for sample {sample_id}.")
return {
"message": f"{len(uploaded_files)} images uploaded successfully.",
"path": str(base_dir), # Return the base directory for reference
"message": f"{len(saved_files)} images uploaded successfully.",
"files": saved_files,
}

View File

@ -16,7 +16,7 @@ class loginToken(BaseModel):
class loginData(BaseModel):
username: str
pgroups: List[int]
pgroups: List[str]
class DewarTypeBase(BaseModel):
@ -392,22 +392,29 @@ class ContactPersonUpdate(BaseModel):
class AddressCreate(BaseModel):
pgroups: str
house_number: Optional[str] = None
street: str
city: str
state: Optional[str] = None
zipcode: str
country: str
class Address(AddressCreate):
id: int
status: str = "active"
class Config:
from_attributes = True
class AddressUpdate(BaseModel):
pgroups: str
house_number: Optional[str] = None
street: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
zipcode: Optional[str] = None
country: Optional[str] = None

View File

@ -18,6 +18,7 @@ from app.routers import (
sample,
)
from app.database import Base, engine, SessionLocal
from app.routers.protected_router import protected_router
# Utility function to fetch metadata from pyproject.toml
@ -139,8 +140,8 @@ def on_startup():
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)
if environment == "dev":
from app.database import load_sample_data
@ -154,9 +155,10 @@ def on_startup():
# Include routers with correct configuration
app.include_router(protected_router, prefix="/protected", tags=["protected"])
app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(contact.router, prefix="/contacts", tags=["contacts"])
app.include_router(address.router, prefix="/addresses", tags=["addresses"])
app.include_router(address.protected_router, prefix="/addresses", tags=["addresses"])
app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

View File

@ -29,6 +29,7 @@
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"jwt-decode": "^4.0.0",
"openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1",
"react-big-calendar": "^1.15.0",
@ -485,6 +486,15 @@
"@devexpress/dx-core": "4.0.10"
}
},
"node_modules/@devexpress/dx-scheduler-core/node_modules/rrule": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz",
"integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@ -4732,6 +4742,15 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -5992,10 +6011,12 @@
}
},
"node_modules/rrule": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz",
"integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
"integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}

View File

@ -35,6 +35,7 @@
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"jwt-decode": "^4.0.0",
"openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1",
"react-big-calendar": "^1.15.0",

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { setUpToken, clearToken } from './utils/auth'; // Import the token utilities
import ResponsiveAppBar from './components/ResponsiveAppBar';
import ShipmentView from './pages/ShipmentView';
@ -16,38 +17,77 @@ const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false);
const [openContactsManager, setOpenContactsManager] = useState(false);
const handleOpenAddressManager = () => {
setOpenAddressManager(true);
};
const handleOpenAddressManager = () => setOpenAddressManager(true);
const handleCloseAddressManager = () => setOpenAddressManager(false);
const handleOpenContactsManager = () => setOpenContactsManager(true);
const handleCloseContactsManager = () => setOpenContactsManager(false);
const handleCloseAddressManager = () => {
setOpenAddressManager(false);
};
const [pgroups, setPgroups] = useState<string[]>([]);
const [activePgroup, setActivePgroup] = useState<string>('');
const handleOpenContactsManager = () => {
setOpenContactsManager(true);
};
// On app load, configure the token
useEffect(() => {
setUpToken(); // Ensure token is loaded into OpenAPI on app initialization
}, []);
const handleCloseContactsManager = () => {
setOpenContactsManager(false);
useEffect(() => {
const updateStateFromLocalStorage = () => {
const user = localStorage.getItem('user');
console.log("[DEBUG] User data in localStorage (update):", user); // Debug
if (user) {
try {
const parsedUser = JSON.parse(user);
if (parsedUser.pgroups && Array.isArray(parsedUser.pgroups)) {
setPgroups(parsedUser.pgroups);
setActivePgroup(parsedUser.pgroups[0] || '');
console.log("[DEBUG] Pgroups updated in state:", parsedUser.pgroups);
} else {
console.warn("[DEBUG] No pgroups found in user data");
}
} catch (error) {
console.error("[DEBUG] Error parsing user data:", error);
}
} else {
console.warn("[DEBUG] No user in localStorage");
}
};
// Run on component mount
updateStateFromLocalStorage();
// Listen for localStorage changes
window.addEventListener('storage', updateStateFromLocalStorage);
// Cleanup listener on unmount
return () => {
window.removeEventListener('storage', updateStateFromLocalStorage);
};
}, []);
const handlePgroupChange = (newPgroup: string) => {
setActivePgroup(newPgroup);
console.log(`pgroup changed to: ${newPgroup}`);
};
return (
<Router>
<ResponsiveAppBar
activePgroup={activePgroup}
onOpenAddressManager={handleOpenAddressManager}
onOpenContactsManager={handleOpenContactsManager}
pgroups={pgroups || []} // Default to an empty array
currentPgroup={activePgroup}
onPgroupChange={handlePgroupChange}
/>
<Routes>
<Route path="/login" element={<LoginView />} />
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView activePgroup={activePgroup} />} />} />
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
<Route path="/results" element={<ProtectedRoute element={<ResultsView />} />} />
{/* Other routes as necessary */}
</Routes>
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
<AddressManager />
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />
</Modal>
<Modal open={openContactsManager} onClose={handleCloseContactsManager} title="Contacts Management">
<ContactsManager />

View File

@ -10,7 +10,7 @@ interface ModalProps {
const Modal: React.FC<ModalProps> = ({ open, onClose, title, children }) => {
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
<Dialog open={open} onClose={onClose} fullWidth maxWidth="lg">
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{children}

View File

@ -7,25 +7,43 @@ import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip';
import { Button } from '@mui/material';
import { Button, Select, FormControl, InputLabel, } from '@mui/material';
import { Link } from 'react-router-dom';
import logo from '../assets/icons/psi_01_sn.svg';
import '../App.css';
import { clearToken } from '../utils/auth';
interface ResponsiveAppBarProps {
activePgroup: string;
onOpenAddressManager: () => void;
onOpenContactsManager: () => void;
pgroups: string[]; // Pass the pgroups from the server
currentPgroup: string; // Currently selected pgroup
onPgroupChange: (pgroup: string) => void; // Callback when selected pgroup changes
}
const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManager, onOpenContactsManager }) => {
const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({
activePgroup,
onOpenAddressManager,
onOpenContactsManager,
pgroups,
currentPgroup,
onPgroupChange }) => {
const navigate = useNavigate();
const location = useLocation();
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null);
const [selectedPgroup, setSelectedPgroup] = useState(currentPgroup);
console.log('Active Pgroup:', activePgroup);
const handlePgroupChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const newPgroup = event.target.value as string;
setSelectedPgroup(newPgroup);
onPgroupChange(newPgroup); // Inform parent about the change
};
const pages = [
{ name: 'Home', path: '/' },
@ -59,8 +77,8 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
const handleLogout = () => {
console.log("Performing logout...");
localStorage.removeItem('token');
navigate('/login');
clearToken(); // Clear the token from localStorage and OpenAPI
navigate('/login'); // Redirect to login page
};
const handleMenuItemClick = (action: string) => {
@ -123,7 +141,31 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
</Button>
))}
</Box>
<Box>
<FormControl variant="outlined" size="small">
<InputLabel sx={{ color: 'white' }}>pgroup</InputLabel>
<Select
value={selectedPgroup}
onChange={handlePgroupChange}
sx={{
color: 'white',
borderColor: 'white',
minWidth: 150,
'&:focus': { borderColor: '#f0db4f' },
}}
>
{pgroups && pgroups.length > 0 ? (
pgroups.map((pgroup) => (
<MenuItem key={pgroup} value={pgroup}>
{pgroup}
</MenuItem>
))
) : (
<MenuItem disabled>No pgroups available</MenuItem>
)}
</Select>
</FormControl>
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>

View File

@ -12,6 +12,7 @@ import DewarDetails from './DewarDetails';
const MAX_COMMENTS_LENGTH = 200;
interface ShipmentDetailsProps {
activePgroup: string;
isCreatingShipment: boolean;
sx?: SxProps;
selectedShipment: Shipment | null;
@ -23,6 +24,7 @@ interface ShipmentDetailsProps {
}
const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
activePgroup,
sx,
selectedShipment,
setSelectedDewar,
@ -34,6 +36,8 @@ const ShipmentDetails: React.FC<ShipmentDetailsProps> = ({
const [comments, setComments] = useState<string>(selectedShipment?.comments || '');
const [initialComments, setInitialComments] = useState<string>(selectedShipment?.comments || '');
console.log('Active Pgroup:', activePgroup); // Debugging or use it where required
const initialNewDewarState: Partial<Dewar> = {
dewar_name: '',
tracking_number: '',

View File

@ -6,15 +6,18 @@ import {
import { SelectChangeEvent } from '@mui/material';
import { SxProps } from '@mui/system';
import {
ContactPersonCreate, ContactPerson, Address, Proposal, ContactsService, AddressesService, AddressCreate, ProposalsService,
ContactPersonCreate, ContactPerson, Address, AddressCreate, Proposal, ContactsService, AddressesService, ProposalsService,
OpenAPI, ShipmentCreate, ShipmentsService
} from '../../openapi';
import { useEffect } from 'react';
import { CountryList } from './CountryList'; // Import the list of countries
import { jwtDecode } from 'jwt-decode';
const MAX_COMMENTS_LENGTH = 200;
interface ShipmentFormProps {
activePgroup: string;
sx?: SxProps;
onCancel: () => void;
refreshShipments: () => void;
@ -26,7 +29,17 @@ const fuse = new Fuse(CountryList, {
includeScore: true,
});
const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshShipments }) => {
const token = localStorage.getItem('token');
if (token) {
OpenAPI.TOKEN = token; // Ensure OpenAPI client uses this token
}
const ShipmentForm: React.FC<ShipmentFormProps> = ({
activePgroup,
sx = {},
onCancel,
refreshShipments }) => {
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [contactPersons, setContactPersons] = React.useState<ContactPerson[]>([]);
const [returnAddresses, setReturnAddresses] = React.useState<Address[]>([]);
@ -37,7 +50,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
firstname: '', lastname: '', phone_number: '', email: ''
});
const [newReturnAddress, setNewReturnAddress] = React.useState<Omit<Address, 'id'>>({
street: '', city: '', zipcode: '', country: ''
pgroup:'', house_number: '', street: '', city: '', state: '', zipcode: '', country: ''
});
const [newShipment, setNewShipment] = React.useState<Partial<ShipmentCreate>>({
shipment_name: '', shipment_status: 'In preparation', comments: ''
@ -80,12 +93,23 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
};
const getAddresses = async () => {
if (!activePgroup) {
console.error("Active pgroup is missing.");
setErrorMessage("Active pgroup is missing. Unable to load addresses.");
return;
}
try {
// Pass activePgroup directly as a string (not as an object)
const fetchedAddresses: Address[] =
await AddressesService.getReturnAddressesAddressesGet();
await AddressesService.getReturnAddressesAddressesGet(activePgroup);
setReturnAddresses(fetchedAddresses);
} catch {
setErrorMessage('Failed to load return addresses.');
} catch (error) {
console.error("Error fetching addresses:", error);
// Extract and log meaningful information from OpenAPI errors (if available)
setErrorMessage("Failed to load return addresses due to API error.");
}
};
@ -102,7 +126,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
getContacts();
getAddresses();
getProposals();
}, []);
}, [activePgroup]);
const handleCountryInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
@ -170,13 +194,14 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
const payload: ShipmentCreate = {
shipment_name: newShipment.shipment_name || '',
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required at all
shipment_date: new Date().toISOString().split('T')[0], // Remove if date is not required
shipment_status: newShipment.shipment_status || 'In preparation',
comments: newShipment.comments || '',
contact_person_id: selectedContactPersonId!,
return_address_id: selectedReturnAddressId!,
proposal_id: selectedProposalId!,
dewars: newShipment.dewars || []
dewars: newShipment.dewars || [],
//pgroup: activePgroup,
};
console.log('Shipment Payload being sent:', payload);
@ -249,32 +274,44 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
};
const handleSaveNewReturnAddress = async () => {
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city ||
!newReturnAddress.country) {
// Validate address form data
if (!validateZipCode(newReturnAddress.zipcode) || !newReturnAddress.street || !newReturnAddress.city || !newReturnAddress.country) {
setErrorMessage('Please fill in all new return address fields correctly.');
return;
}
// Ensure activePgroup is available
if (!activePgroup) {
setErrorMessage('Active pgroup is missing. Please try again.');
return;
}
// Construct the payload
const payload: AddressCreate = {
pgroups: activePgroup, // Use the activePgroup prop directly
house_number: newReturnAddress.house_number,
street: newReturnAddress.street,
city: newReturnAddress.city,
state: newReturnAddress.state,
zipcode: newReturnAddress.zipcode,
country: newReturnAddress.country,
};
console.log('Return Address Payload being sent:', payload);
// Call the API with the completed payload
try {
const response: Address = await AddressesService.createReturnAddressAddressesPost(payload);
setReturnAddresses([...returnAddresses, response]);
setReturnAddresses([...returnAddresses, response]); // Update the address state
setErrorMessage(null);
setSelectedReturnAddressId(response.id);
setSelectedReturnAddressId(response.id); // Set the newly created address ID to the form
} catch (error) {
console.error('Failed to create a new return address:', error);
setErrorMessage('Failed to create a new return address. Please try again later.');
}
setNewReturnAddress({ street: '', city: '', zipcode: '', country: '' });
// Reset form inputs and close the "Create New Address" form
setNewReturnAddress({ pgroup: '', house_number: '', street: '', city: '', state: '', zipcode: '', country: '' });
setIsCreatingReturnAddress(false);
};
@ -390,11 +427,22 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
onChange={handleReturnAddressChange}
displayEmpty
>
{returnAddresses.map((address) => (
<MenuItem key={address.id} value={address.id.toString()}>
{`${address.street}, ${address.city}, ${address.zipcode}, ${address.country}`}
</MenuItem>
))}
{returnAddresses.map((address) => {
const addressParts = [
address.house_number,
address.street,
address.city,
address.zipcode,
address.state,
address.country
].filter(part => part); // Remove falsy (null/undefined/empty) values
return (
<MenuItem key={address.id} value={address.id.toString()}>
{addressParts.join(', ')} {/* Join the valid address parts with a comma */}
</MenuItem>
);
})}
<MenuItem value="new">
<em>Create New Return Address</em>
</MenuItem>
@ -402,6 +450,7 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
</FormControl>
{isCreatingReturnAddress && (
<>
<TextField
label="Street"
name="street"
@ -410,6 +459,13 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
fullWidth
required
/>
<TextField
label="Number"
name="number"
value={newReturnAddress.house_number}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, house_number: e.target.value })}
fullWidth
/>
<TextField
label="City"
name="city"
@ -418,6 +474,13 @@ const ShipmentForm: React.FC<ShipmentFormProps> = ({ sx = {}, onCancel, refreshS
fullWidth
required
/>
<TextField
label="State"
name="state"
value={newReturnAddress.state}
onChange={(e) => setNewReturnAddress({ ...newReturnAddress, state: e.target.value })}
fullWidth
/>
<TextField
label="Zip Code"
name="zipcode"

View File

@ -11,6 +11,7 @@ import bottleRed from '/src/assets/icons/bottle-svgrepo-com-red.svg';
interface ShipmentPanelProps {
setCreatingShipment: (value: boolean) => void;
activePgroup: string;
selectShipment: (shipment: Shipment | null) => void;
selectedShipment: Shipment | null;
sx?: SxProps;
@ -27,6 +28,7 @@ const statusIconMap: Record<string, string> = {
};
const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
activePgroup,
setCreatingShipment,
selectShipment,
selectedShipment,
@ -37,6 +39,8 @@ const ShipmentPanel: React.FC<ShipmentPanelProps> = ({
}) => {
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
console.log('Active Pgroup:', activePgroup);
const handleDeleteShipment = async () => {
if (selectedShipment) {
const confirmDelete = window.confirm(

View File

@ -2,26 +2,57 @@ import React from 'react';
import Fuse from 'fuse.js';
import { CountryList } from '../components/CountryList';
import {
Container, Typography, List, ListItem, IconButton, TextField, Box, ListItemText, ListItemSecondaryAction, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button
Container,
Typography,
List,
ListItem,
IconButton,
TextField,
Box,
ListItemText,
ListItemSecondaryAction,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Button,
Chip
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import AddIcon from '@mui/icons-material/Add';
import { AddressesService } from '../../openapi';
import type { Address, AddressCreate, AddressUpdate } from '../models/Address';
import {Address, AddressCreate, AddressesService, AddressUpdate} from '../../openapi';
interface AddressManagerProps {
pgroups: string[];
activePgroup: string;
}
// Extend the generated Address type
interface AddressWithPgroups extends Address {
associatedPgroups: string[]; // Dynamically added pgroups
}
const fuse = new Fuse(CountryList, {
threshold: 0.3, // Lower threshold for stricter matches
includeScore: true,
});
const AddressManager: React.FC = () => {
const AddressManager: React.FC<AddressManagerProps> = ({ pgroups, activePgroup }) => {
// Use pgroups and activePgroup directly
console.log('User pgroups:', pgroups);
console.log('Active pgroup:', activePgroup);
const [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [addresses, setAddresses] = React.useState<Address[]>([]);
const [newAddress, setNewAddress] = React.useState<Partial<Address>>({
house_number: '',
street: '',
city: '',
state: '',
zipcode: '',
country: '',
});
@ -45,18 +76,28 @@ const AddressManager: React.FC = () => {
};
React.useEffect(() => {
const fetchAddresses = async () => {
const fetchAllData = async () => {
try {
const response = await AddressesService.getReturnAddressesAddressesGet();
setAddresses(response);
const response = await AddressesService.getAllAddressesAddressesAllGet();
// Preprocess: Add associated and unassociated pgroups
const transformedAddresses = response.map((address) => {
const addressPgroups = address.pgroups?.split(',').map((p) => p.trim()) || [];
const associatedPgroups = pgroups.filter((pgroup) => addressPgroups.includes(pgroup));
return {
...address,
associatedPgroups, // pgroups linked to the address
};
});
setAddresses(transformedAddresses);
} catch (error) {
console.error('Failed to fetch addresses', error);
setErrorMessage('Failed to load addresses. Please try again later.');
setErrorMessage('Failed to load addresses. Please try again.');
}
};
fetchAddresses();
}, []);
fetchAllData();
}, [pgroups]);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
@ -66,30 +107,40 @@ const AddressManager: React.FC = () => {
const handleAddOrUpdateAddress = async () => {
try {
if (editAddressId !== null) {
// Update address
await AddressesService.updateReturnAddressAddressesAddressIdPut(editAddressId, newAddress as AddressUpdate);
setAddresses(addresses.map(address => address.id === editAddressId ? { ...address, ...newAddress } : address));
// Update address (mark old one obsolete, create a new one)
const updatedAddress = await AddressesService.updateReturnAddressAddressesAddressIdPut(
editAddressId,
newAddress as AddressUpdate
);
// Replace old address with the new one in the list
setAddresses(addresses.map(address =>
address.id === editAddressId ? updatedAddress : address
).filter(address => address.status === "active")); // Keep only active addresses
setEditAddressId(null);
} else {
// Add new address
const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate);
setAddresses([...addresses, response]);
}
setNewAddress({ street: '', city: '', zipcode: '', country: '' });
setNewAddress({ house_number:'', street: '', city: '', state: '', zipcode: '', country: '' });
setErrorMessage(null);
} catch (error) {
console.error('Failed to add/update address', error);
setErrorMessage('Failed to add/update address. Please try again later.');
setErrorMessage('Failed to add/update address. Please try again.');
}
};
const handleDeleteAddress = async (id: number) => {
try {
// Delete (inactivate) the address
await AddressesService.deleteReturnAddressAddressesAddressIdDelete(id);
setAddresses(addresses.filter(address => address.id !== id));
// Remove the obsolete address from the active list in the UI
setAddresses(addresses.filter(address => address.id !== id && address.status === "active"));
} catch (error) {
console.error('Failed to delete address', error);
setErrorMessage('Failed to delete address. Please try again later.');
setErrorMessage('Failed to delete address. Please try again.');
}
};
@ -115,52 +166,149 @@ const AddressManager: React.FC = () => {
}
};
const togglePgroupAssociation = async (addressId: number, pgroup: string) => {
try {
const address = addresses.find((addr) => addr.id === addressId);
if (!address) return;
const isAssociated = address.associatedPgroups.includes(pgroup);
// Only allow adding a pgroup
if (isAssociated) {
console.warn('Removing a pgroup is not allowed.');
return;
}
const updatedPgroups = [...address.associatedPgroups, pgroup]; // Add the pgroup
// Update the backend
await AddressesService.updateReturnAddressAddressesAddressIdPut(addressId, {
...address,
pgroups: updatedPgroups.join(','), // Sync updated pgroups
});
// Update address in local state
setAddresses((prevAddresses) =>
prevAddresses.map((addr) =>
addr.id === addressId
? { ...addr, associatedPgroups: updatedPgroups }
: addr
)
);
} catch (error) {
console.error('Failed to add pgroup association', error);
setErrorMessage('Failed to add pgroup association. Please try again.');
}
};
const renderPgroupChips = (address: AddressWithPgroups) => {
return pgroups.map((pgroup) => {
const isAssociated = address.associatedPgroups.includes(pgroup);
return (
<Chip
key={pgroup}
label={pgroup}
onClick={
!isAssociated // Only allow adding a new pgroup, no removal
? () => togglePgroupAssociation(address.id, pgroup)
: undefined
}
sx={{
backgroundColor: isAssociated ? '#19d238' : '#b0b0b0',
color: 'white',
borderRadius: '8px',
fontWeight: 'bold',
height: '20px',
fontSize: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
cursor: isAssociated ? 'default' : 'pointer', // Disable pointer for associated chips
'&:hover': { opacity: isAssociated ? 1 : 0.8 }, // Disable hover effect for associated chips
mr: 1,
mb: 1,
}}
/>
);
});
};
return (
<Container>
<Typography variant="h4" gutterBottom>
Addresses Management
</Typography>
<Box display="flex" justifyContent="center" alignItems="center" mb={3}>
<TextField label="Street" name="street" value={newAddress.street || ''} onChange={handleInputChange} />
<TextField label="City" name="city" value={newAddress.city || ''} onChange={handleInputChange} />
<TextField label="Zipcode" name="zipcode" value={newAddress.zipcode || ''} onChange={handleInputChange} />
<Box display="flex" justifyContent="center" alignItems="center" mb={3} gap={2}>
<TextField
label="Country"
name="country"
value={newAddress.country || ''}
onChange={handleCountryInputChange}
fullWidth
required
error={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
} // Show an error only if in add/edit mode and country is empty
helperText={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
? 'Country is required'
: ''
}
label="pgroup"
name="pgroup"
value={newAddress.activePgroup || ''}
disabled
sx={{ width: '120px' }} // Small fixed-size for non-editable field
/>
{/* Render suggestions dynamically */}
<Box sx={{ position: 'relative' }}>
<TextField
label="Number"
name="house_number"
value={newAddress.house_number || ''}
onChange={handleInputChange}
sx={{ width: '100px' }} // Small size for Number field
/>
<TextField
label="Street"
name="street"
value={newAddress.street || ''}
onChange={handleInputChange}
sx={{ flex: 1 }} // Street field takes the most space
/>
<TextField
label="City"
name="city"
value={newAddress.city || ''}
onChange={handleInputChange}
sx={{ width: '150px' }} // Medium size for City
/>
<TextField
label="State"
name="state"
value={newAddress.state || ''}
onChange={handleInputChange}
sx={{ width: '100px' }} // Small size
/>
<TextField
label="Zipcode"
name="zipcode"
value={newAddress.zipcode || ''}
onChange={handleInputChange}
sx={{ width: '120px' }} // Medium size for Zipcode
/>
<Box sx={{ position: 'relative', flex: 1 }}> {/* Country field dynamically takes available space */}
<TextField
label="Country"
name="country"
value={newAddress.country || ''}
onChange={handleCountryInputChange}
fullWidth
required
error={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
}
helperText={
!!((editAddressId !== null || newAddress.street || newAddress.city || newAddress.zipcode) && !newAddress.country)
? 'Country is required'
: ''
}
/>
{/* Render suggestions dynamically */}
{countrySuggestions.length > 0 && (
<Box
sx={{
border: '1px solid #ccc',
borderRadius: '4px',
background: 'white',
marginTop: '4px', /* Add space below the input */
position: 'absolute',
width: '100%', /* Match the TextField width */
zIndex: 10, /* Ensure it is above other UI elements */
top: '100%', // Place below the TextField
left: 0,
zIndex: 10,
width: '100%', // Match the width of the TextField
marginTop: '4px', // Small spacing below the TextField
}}
>
{countrySuggestions.map((suggestion, index) => (
@ -172,7 +320,6 @@ const AddressManager: React.FC = () => {
'&:hover': { background: '#f5f5f5' },
}}
onClick={() => {
// Update country field with the clicked suggestion
setNewAddress({ ...newAddress, country: suggestion });
setCountrySuggestions([]); // Clear suggestions
}}
@ -193,14 +340,18 @@ const AddressManager: React.FC = () => {
addresses.map((address) => (
<ListItem key={address.id} button>
<ListItemText
primary={`${address.street}, ${address.city}`}
secondary={`${address.zipcode} - ${address.country}`}
primary={`${address.house_number}, ${address.street}, ${address.city}`}
secondary={
<Box display="flex" flexWrap="wrap">
{renderPgroupChips(address)}
</Box>
}
/>
<ListItemSecondaryAction>
<IconButton edge="end" color="primary" onClick={() => handleEditAddress(address)}>
<EditIcon />
</IconButton>
<IconButton edge="end" color="secondary" onClick={() => openDialog(address)}>
<IconButton edge="end" color="secondary" onClick={() => handleDeleteAddress(address.id)}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>

View File

@ -89,10 +89,25 @@ const LoginView: React.FC = () => {
password: password,
});
// Save the token
localStorage.setItem('token', response.access_token);
navigate('/'); // Redirect post-login
OpenAPI.TOKEN = response.access_token;
// Decode token to extract user data (e.g., pgroups)
const decodedToken = JSON.parse(atob(response.access_token.split('.')[1])); // Decode JWT payload
const userData = {
username: decodedToken.sub,
pgroups: decodedToken.pgroups || [], // Ensure pgroups is an array
};
localStorage.setItem('user', JSON.stringify(userData)); // Save user data in localStorage
console.log("Token updated successfully:", response.access_token);
console.log("User data saved:", userData);
navigate('/'); // Redirect after successful login
} catch (err) {
setError('Login failed. Please check your credentials.');
console.error("Error during login:", err);
}
};

View File

@ -6,14 +6,20 @@ import { Dewar, OpenAPI, Shipment } from '../../openapi';
import useShipments from '../hooks/useShipments';
import { Grid, Container } from '@mui/material';
type ShipmentViewProps = React.PropsWithChildren<Record<string, never>>;
type ShipmentViewProps = {
activePgroup: string;
};
const ShipmentView: React.FC<ShipmentViewProps> = () => {
const ShipmentView: React.FC<ShipmentViewProps> = ( { activePgroup }) => {
const { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [isCreatingShipment, setIsCreatingShipment] = useState(false);
useEffect(() => {
fetchAndSetShipments();
}, [activePgroup]);
useEffect(() => {
// Detect the current environment
const mode = import.meta.env.MODE;
@ -52,6 +58,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (isCreatingShipment) {
return (
<ShipmentForm
activePgroup={activePgroup}
sx={{ flexGrow: 1 }}
onCancel={handleCancelShipmentForm}
refreshShipments={fetchAndSetShipments}
@ -61,6 +68,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (selectedShipment) {
return (
<ShipmentDetails
activePgroup={activePgroup}
isCreatingShipment={isCreatingShipment}
sx={{ flexGrow: 1 }}
selectedShipment={selectedShipment}
@ -89,6 +97,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
}}
>
<ShipmentPanel
activePgroup={activePgroup}
setCreatingShipment={setIsCreatingShipment}
selectShipment={handleSelectShipment}
shipments={shipments}

View File

@ -0,0 +1,16 @@
import {OpenAPI} from "../../openapi";
export const setUpToken = (): void => {
const token = localStorage.getItem('token');
if (token) {
OpenAPI.TOKEN = token; // Assign the token to OpenAPI client
} else {
console.warn("No token found in localStorage.");
}
};
export const clearToken = (): void => {
localStorage.removeItem('token');
OpenAPI.TOKEN = ''; // Clear the token from the OpenAPI client
console.log("Token cleared from OpenAPI and localStorage.");
};

View File

@ -6,20 +6,20 @@
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2025-01-17T14:03:52.460891Z",
"start_time": "2025-01-17T14:03:52.454842Z"
"end_time": "2025-01-20T14:44:37.526742Z",
"start_time": "2025-01-20T14:44:37.522704Z"
}
},
"source": [
"import json\n",
"\n",
"from nbclient.client import timestamp\n",
"#from nbclient.client import timestamp\n",
"\n",
"import backend.aareDBclient as aareDBclient\n",
"from aareDBclient.rest import ApiException\n",
"from pprint import pprint\n",
"\n",
"from app.data.data import sample\n",
"#from app.data.data import sample\n",
"\n",
"#from aareDBclient import SamplesApi, ShipmentsApi, PucksApi\n",
"#from aareDBclient.models import SampleEventCreate, SetTellPosition\n",
@ -47,13 +47,13 @@
]
}
],
"execution_count": 40
"execution_count": 2
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-17T14:13:16.559702Z",
"start_time": "2025-01-17T14:13:16.446021Z"
"end_time": "2025-01-20T14:44:58.875370Z",
"start_time": "2025-01-20T14:44:58.805520Z"
}
},
"cell_type": "code",
@ -152,7 +152,7 @@
]
}
],
"execution_count": 44
"execution_count": 3
},
{
"metadata": {
@ -456,8 +456,8 @@
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-20T11:10:20.017201Z",
"start_time": "2025-01-20T11:10:19.953940Z"
"end_time": "2025-01-20T14:45:11.812597Z",
"start_time": "2025-01-20T14:45:11.793309Z"
}
},
"cell_type": "code",
@ -503,7 +503,9 @@
"output_type": "stream",
"text": [
"Payload being sent to API:\n",
"{\"event_type\":\"Mounted\"}\n"
"{\"event_type\":\"Mounted\"}\n",
"API response:\n",
"Sample(id=433, sample_name='Dtpase_1', position=1, puck_id=44, crystalname=None, proteinname=None, positioninpuck=None, priority=1, comments=None, data_collection_parameters=DataCollectionParameters(directory='{sgPuck}/{sgPosition}', oscillation=None, exposure=None, totalrange=None, transmission=None, targetresolution=None, aperture=None, datacollectiontype=None, processingpipeline='', spacegroupnumber=None, cellparameters=None, rescutkey=None, rescutvalue=None, userresolution=None, pdbid='', autoprocfull=False, procfull=False, adpenabled=False, noano=False, ffcscampaign=False, trustedhigh=None, autoprocextraparams=None, chiphiangles=None, dose=None), events=[SampleEventResponse(id=386, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 35, 38)), SampleEventResponse(id=387, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 40, 11)), SampleEventResponse(id=388, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 45, 4)), SampleEventResponse(id=389, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 45, 24)), SampleEventResponse(id=390, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 50, 38)), SampleEventResponse(id=391, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 11, 52, 28)), SampleEventResponse(id=392, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 12, 10, 20)), SampleEventResponse(id=393, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 13, 39, 24)), SampleEventResponse(id=394, sample_id=433, event_type='Mounted', timestamp=datetime.datetime(2025, 1, 20, 15, 45, 12))], mount_count=9, unmount_count=0)\n"
]
},
{
@ -513,29 +515,9 @@
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: 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"
]
},
{
"ename": "ValidationError",
"evalue": "3 validation errors for SampleEventResponse\nsample_id\n Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/int_type\nevent_type\n Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/string_type\ntimestamp\n Input should be a valid datetime [type=datetime_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/datetime_type",
"output_type": "error",
"traceback": [
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
"\u001B[0;31mValidationError\u001B[0m Traceback (most recent call last)",
"Cell \u001B[0;32mIn[110], line 21\u001B[0m\n\u001B[1;32m 18\u001B[0m \u001B[38;5;28mprint\u001B[39m(sample_event_create\u001B[38;5;241m.\u001B[39mjson()) \u001B[38;5;66;03m# Ensure it matches `SampleEventCreate`\u001B[39;00m\n\u001B[1;32m 20\u001B[0m \u001B[38;5;66;03m# Call the API\u001B[39;00m\n\u001B[0;32m---> 21\u001B[0m api_response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mcreate_sample_event_samples_samples_sample_id_events_post\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 22\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;241;43m433\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;66;43;03m# Ensure this matches a valid sample ID in the database\u001B[39;49;00m\n\u001B[1;32m 23\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_event_create\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_event_create\u001B[49m\n\u001B[1;32m 24\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 26\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mAPI response:\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 27\u001B[0m pprint(api_response)\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/validate_call_decorator.py:60\u001B[0m, in \u001B[0;36mvalidate_call.<locals>.validate.<locals>.wrapper_function\u001B[0;34m(*args, **kwargs)\u001B[0m\n\u001B[1;32m 58\u001B[0m \u001B[38;5;129m@functools\u001B[39m\u001B[38;5;241m.\u001B[39mwraps(function)\n\u001B[1;32m 59\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21mwrapper_function\u001B[39m(\u001B[38;5;241m*\u001B[39margs, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[0;32m---> 60\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mvalidate_call_wrapper\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py:96\u001B[0m, in \u001B[0;36mValidateCallWrapper.__call__\u001B[0;34m(self, *args, **kwargs)\u001B[0m\n\u001B[1;32m 95\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21m__call__\u001B[39m(\u001B[38;5;28mself\u001B[39m, \u001B[38;5;241m*\u001B[39margs: Any, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs: Any) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Any:\n\u001B[0;32m---> 96\u001B[0m res \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__pydantic_validator__\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalidate_python\u001B[49m\u001B[43m(\u001B[49m\u001B[43mpydantic_core\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mArgsKwargs\u001B[49m\u001B[43m(\u001B[49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 97\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__:\n\u001B[1;32m 98\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__(res)\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:109\u001B[0m, in \u001B[0;36mSamplesApi.create_sample_event_samples_samples_sample_id_events_post\u001B[0;34m(self, sample_id, sample_event_create, _request_timeout, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 104\u001B[0m response_data \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mapi_client\u001B[38;5;241m.\u001B[39mcall_api(\n\u001B[1;32m 105\u001B[0m \u001B[38;5;241m*\u001B[39m_param,\n\u001B[1;32m 106\u001B[0m _request_timeout\u001B[38;5;241m=\u001B[39m_request_timeout\n\u001B[1;32m 107\u001B[0m )\n\u001B[1;32m 108\u001B[0m response_data\u001B[38;5;241m.\u001B[39mread()\n\u001B[0;32m--> 109\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mapi_client\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mresponse_deserialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 110\u001B[0m \u001B[43m \u001B[49m\u001B[43mresponse_data\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mresponse_data\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 111\u001B[0m \u001B[43m \u001B[49m\u001B[43mresponse_types_map\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_response_types_map\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 112\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\u001B[38;5;241m.\u001B[39mdata\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:319\u001B[0m, in \u001B[0;36mApiClient.response_deserialize\u001B[0;34m(self, response_data, response_types_map)\u001B[0m\n\u001B[1;32m 317\u001B[0m encoding \u001B[38;5;241m=\u001B[39m match\u001B[38;5;241m.\u001B[39mgroup(\u001B[38;5;241m1\u001B[39m) \u001B[38;5;28;01mif\u001B[39;00m match \u001B[38;5;28;01melse\u001B[39;00m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mutf-8\u001B[39m\u001B[38;5;124m\"\u001B[39m\n\u001B[1;32m 318\u001B[0m response_text \u001B[38;5;241m=\u001B[39m response_data\u001B[38;5;241m.\u001B[39mdata\u001B[38;5;241m.\u001B[39mdecode(encoding)\n\u001B[0;32m--> 319\u001B[0m return_data \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mdeserialize\u001B[49m\u001B[43m(\u001B[49m\u001B[43mresponse_text\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mresponse_type\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcontent_type\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 320\u001B[0m \u001B[38;5;28;01mfinally\u001B[39;00m:\n\u001B[1;32m 321\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;241m200\u001B[39m \u001B[38;5;241m<\u001B[39m\u001B[38;5;241m=\u001B[39m response_data\u001B[38;5;241m.\u001B[39mstatus \u001B[38;5;241m<\u001B[39m\u001B[38;5;241m=\u001B[39m \u001B[38;5;241m299\u001B[39m:\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:420\u001B[0m, in \u001B[0;36mApiClient.deserialize\u001B[0;34m(self, response_text, response_type, content_type)\u001B[0m\n\u001B[1;32m 414\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[1;32m 415\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m ApiException(\n\u001B[1;32m 416\u001B[0m status\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m,\n\u001B[1;32m 417\u001B[0m reason\u001B[38;5;241m=\u001B[39m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnsupported content type: \u001B[39m\u001B[38;5;132;01m{0}\u001B[39;00m\u001B[38;5;124m\"\u001B[39m\u001B[38;5;241m.\u001B[39mformat(content_type)\n\u001B[1;32m 418\u001B[0m )\n\u001B[0;32m--> 420\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__deserialize\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mresponse_type\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:467\u001B[0m, in \u001B[0;36mApiClient.__deserialize\u001B[0;34m(self, data, klass)\u001B[0m\n\u001B[1;32m 465\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__deserialize_enum(data, klass)\n\u001B[1;32m 466\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[0;32m--> 467\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__deserialize_model\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mklass\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:788\u001B[0m, in \u001B[0;36mApiClient.__deserialize_model\u001B[0;34m(self, data, klass)\u001B[0m\n\u001B[1;32m 780\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21m__deserialize_model\u001B[39m(\u001B[38;5;28mself\u001B[39m, data, klass):\n\u001B[1;32m 781\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Deserializes list or dict to model.\u001B[39;00m\n\u001B[1;32m 782\u001B[0m \n\u001B[1;32m 783\u001B[0m \u001B[38;5;124;03m :param data: dict, list.\u001B[39;00m\n\u001B[1;32m 784\u001B[0m \u001B[38;5;124;03m :param klass: class literal.\u001B[39;00m\n\u001B[1;32m 785\u001B[0m \u001B[38;5;124;03m :return: model object.\u001B[39;00m\n\u001B[1;32m 786\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m--> 788\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mklass\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mfrom_dict\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdata\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/models/sample_event_response.py:86\u001B[0m, in \u001B[0;36mSampleEventResponse.from_dict\u001B[0;34m(cls, obj)\u001B[0m\n\u001B[1;32m 83\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;28misinstance\u001B[39m(obj, \u001B[38;5;28mdict\u001B[39m):\n\u001B[1;32m 84\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mcls\u001B[39m\u001B[38;5;241m.\u001B[39mmodel_validate(obj)\n\u001B[0;32m---> 86\u001B[0m _obj \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mcls\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mmodel_validate\u001B[49m\u001B[43m(\u001B[49m\u001B[43m{\u001B[49m\n\u001B[1;32m 87\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mid\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mid\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 88\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43msample_id\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43msample_id\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 89\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mevent_type\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mevent_type\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 90\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mtimestamp\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mget\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[38;5;124;43mtimestamp\u001B[39;49m\u001B[38;5;124;43m\"\u001B[39;49m\u001B[43m)\u001B[49m\n\u001B[1;32m 91\u001B[0m \u001B[43m\u001B[49m\u001B[43m}\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 92\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m _obj\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/main.py:596\u001B[0m, in \u001B[0;36mBaseModel.model_validate\u001B[0;34m(cls, obj, strict, from_attributes, context)\u001B[0m\n\u001B[1;32m 594\u001B[0m \u001B[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001B[39;00m\n\u001B[1;32m 595\u001B[0m __tracebackhide__ \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;01mTrue\u001B[39;00m\n\u001B[0;32m--> 596\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mcls\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__pydantic_validator__\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalidate_python\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 597\u001B[0m \u001B[43m \u001B[49m\u001B[43mobj\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mstrict\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mstrict\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mfrom_attributes\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mfrom_attributes\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcontext\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mcontext\u001B[49m\n\u001B[1;32m 598\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
"\u001B[0;31mValidationError\u001B[0m: 3 validation errors for SampleEventResponse\nsample_id\n Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/int_type\nevent_type\n Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/string_type\ntimestamp\n Input should be a valid datetime [type=datetime_type, input_value=None, input_type=NoneType]\n For further information visit https://errors.pydantic.dev/2.9/v/datetime_type"
]
}
],
"execution_count": 110
"execution_count": 4
},
{
"metadata": {
@ -575,6 +557,126 @@
}
],
"execution_count": 7
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-20T15:43:54.575154Z",
"start_time": "2025-01-20T15:43:54.539295Z"
}
},
"cell_type": "code",
"source": [
"from aareDBclient import ApiClient, SamplesApi # Import the appropriate client\n",
"from aareDBclient.rest import ApiException\n",
"import mimetypes\n",
"\n",
"# File path to the image\n",
"file_path = \"backend/tests/sample_image/IMG_1942.jpg\"\n",
"\n",
"# Sample ID\n",
"sample_id = 433 # Replace with a valid sample_id from your FastAPI backend\n",
"\n",
"# Initialize the API client\n",
"with ApiClient(configuration) as api_client:\n",
" api_instance = SamplesApi(api_client) # Adjust as per your API structure\n",
"\n",
" try:\n",
" # Open the file and read as binary\n",
" with open(file_path, \"rb\") as file:\n",
" # Get the MIME type for the file\n",
" mime_type, _ = mimetypes.guess_type(file_path)\n",
"\n",
" # Call the API method for uploading sample images\n",
" response = api_instance.upload_sample_images_samples_samples_sample_id_upload_images_post(\n",
" sample_id=sample_id,\n",
" uploaded_files=[file.read()] # Pass raw bytes as a list\n",
" )\n",
"\n",
" # Print the response from the API\n",
" print(\"API Response:\")\n",
" print(response)\n",
"\n",
" except ApiException as e:\n",
" # Handle API exception gracefully\n",
" print(\"Exception occurred while uploading the file:\")\n",
" print(f\"Status Code: {e.status}\")\n",
" if e.body:\n",
" print(f\"Error Details: {e.body}\")"
],
"id": "40404614d1a63f95",
"outputs": [
{
"ename": "ValueError",
"evalue": "Unsupported file value",
"output_type": "error",
"traceback": [
"\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
"\u001B[0;31mValueError\u001B[0m Traceback (most recent call last)",
"Cell \u001B[0;32mIn[77], line 22\u001B[0m\n\u001B[1;32m 19\u001B[0m mime_type, _ \u001B[38;5;241m=\u001B[39m mimetypes\u001B[38;5;241m.\u001B[39mguess_type(file_path)\n\u001B[1;32m 21\u001B[0m \u001B[38;5;66;03m# Call the API method for uploading sample images\u001B[39;00m\n\u001B[0;32m---> 22\u001B[0m response \u001B[38;5;241m=\u001B[39m \u001B[43mapi_instance\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mupload_sample_images_samples_samples_sample_id_upload_images_post\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 23\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_id\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 24\u001B[0m \u001B[43m \u001B[49m\u001B[43muploaded_files\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m[\u001B[49m\u001B[43mfile\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mread\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[43m]\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;66;43;03m# Pass raw bytes as a list\u001B[39;49;00m\n\u001B[1;32m 25\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 27\u001B[0m \u001B[38;5;66;03m# Print the response from the API\u001B[39;00m\n\u001B[1;32m 28\u001B[0m \u001B[38;5;28mprint\u001B[39m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mAPI Response:\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/validate_call_decorator.py:60\u001B[0m, in \u001B[0;36mvalidate_call.<locals>.validate.<locals>.wrapper_function\u001B[0;34m(*args, **kwargs)\u001B[0m\n\u001B[1;32m 58\u001B[0m \u001B[38;5;129m@functools\u001B[39m\u001B[38;5;241m.\u001B[39mwraps(function)\n\u001B[1;32m 59\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21mwrapper_function\u001B[39m(\u001B[38;5;241m*\u001B[39margs, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[0;32m---> 60\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mvalidate_call_wrapper\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pydantic/_internal/_validate_call.py:96\u001B[0m, in \u001B[0;36mValidateCallWrapper.__call__\u001B[0;34m(self, *args, **kwargs)\u001B[0m\n\u001B[1;32m 95\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21m__call__\u001B[39m(\u001B[38;5;28mself\u001B[39m, \u001B[38;5;241m*\u001B[39margs: Any, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs: Any) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Any:\n\u001B[0;32m---> 96\u001B[0m res \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m__pydantic_validator__\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalidate_python\u001B[49m\u001B[43m(\u001B[49m\u001B[43mpydantic_core\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mArgsKwargs\u001B[49m\u001B[43m(\u001B[49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 97\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__:\n\u001B[1;32m 98\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m__return_pydantic_validator__(res)\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:875\u001B[0m, in \u001B[0;36mSamplesApi.upload_sample_images_samples_samples_sample_id_upload_images_post\u001B[0;34m(self, sample_id, uploaded_files, _request_timeout, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 827\u001B[0m \u001B[38;5;129m@validate_call\u001B[39m\n\u001B[1;32m 828\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;21mupload_sample_images_samples_samples_sample_id_upload_images_post\u001B[39m(\n\u001B[1;32m 829\u001B[0m \u001B[38;5;28mself\u001B[39m,\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 843\u001B[0m _host_index: Annotated[StrictInt, Field(ge\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m, le\u001B[38;5;241m=\u001B[39m\u001B[38;5;241m0\u001B[39m)] \u001B[38;5;241m=\u001B[39m \u001B[38;5;241m0\u001B[39m,\n\u001B[1;32m 844\u001B[0m ) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mobject\u001B[39m:\n\u001B[1;32m 845\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Upload Sample Images\u001B[39;00m\n\u001B[1;32m 846\u001B[0m \n\u001B[1;32m 847\u001B[0m \u001B[38;5;124;03m Uploads images for a sample and stores them in a directory structure: images/user/date/dewar_name/puck_name/position/. Args: sample_id (int): ID of the sample. uploaded_files (Union[List[UploadFile], List[bytes]]): List of image files (as UploadFile or raw bytes). db (Session): SQLAlchemy database session.\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 872\u001B[0m \u001B[38;5;124;03m :return: Returns the result object.\u001B[39;00m\n\u001B[1;32m 873\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m \u001B[38;5;66;03m# noqa: E501\u001B[39;00m\n\u001B[0;32m--> 875\u001B[0m _param \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m_upload_sample_images_samples_samples_sample_id_upload_images_post_serialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 876\u001B[0m \u001B[43m \u001B[49m\u001B[43msample_id\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43msample_id\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 877\u001B[0m \u001B[43m \u001B[49m\u001B[43muploaded_files\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43muploaded_files\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 878\u001B[0m \u001B[43m \u001B[49m\u001B[43m_request_auth\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_request_auth\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 879\u001B[0m \u001B[43m \u001B[49m\u001B[43m_content_type\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_content_type\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 880\u001B[0m \u001B[43m \u001B[49m\u001B[43m_headers\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_headers\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 881\u001B[0m \u001B[43m \u001B[49m\u001B[43m_host_index\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_host_index\u001B[49m\n\u001B[1;32m 882\u001B[0m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 884\u001B[0m _response_types_map: Dict[\u001B[38;5;28mstr\u001B[39m, Optional[\u001B[38;5;28mstr\u001B[39m]] \u001B[38;5;241m=\u001B[39m {\n\u001B[1;32m 885\u001B[0m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124m200\u001B[39m\u001B[38;5;124m'\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mobject\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 886\u001B[0m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124m422\u001B[39m\u001B[38;5;124m'\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mHTTPValidationError\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 887\u001B[0m }\n\u001B[1;32m 888\u001B[0m response_data \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mapi_client\u001B[38;5;241m.\u001B[39mcall_api(\n\u001B[1;32m 889\u001B[0m \u001B[38;5;241m*\u001B[39m_param,\n\u001B[1;32m 890\u001B[0m _request_timeout\u001B[38;5;241m=\u001B[39m_request_timeout\n\u001B[1;32m 891\u001B[0m )\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api/samples_api.py:1099\u001B[0m, in \u001B[0;36mSamplesApi._upload_sample_images_samples_samples_sample_id_upload_images_post_serialize\u001B[0;34m(self, sample_id, uploaded_files, _request_auth, _content_type, _headers, _host_index)\u001B[0m\n\u001B[1;32m 1095\u001B[0m \u001B[38;5;66;03m# authentication setting\u001B[39;00m\n\u001B[1;32m 1096\u001B[0m _auth_settings: List[\u001B[38;5;28mstr\u001B[39m] \u001B[38;5;241m=\u001B[39m [\n\u001B[1;32m 1097\u001B[0m ]\n\u001B[0;32m-> 1099\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mapi_client\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mparam_serialize\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 1100\u001B[0m \u001B[43m \u001B[49m\u001B[43mmethod\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43mPOST\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1101\u001B[0m \u001B[43m \u001B[49m\u001B[43mresource_path\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43m/samples/samples/\u001B[39;49m\u001B[38;5;132;43;01m{sample_id}\u001B[39;49;00m\u001B[38;5;124;43m/upload-images\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1102\u001B[0m \u001B[43m \u001B[49m\u001B[43mpath_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_path_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1103\u001B[0m \u001B[43m \u001B[49m\u001B[43mquery_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_query_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1104\u001B[0m \u001B[43m \u001B[49m\u001B[43mheader_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_header_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1105\u001B[0m \u001B[43m \u001B[49m\u001B[43mbody\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_body_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1106\u001B[0m \u001B[43m \u001B[49m\u001B[43mpost_params\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_form_params\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1107\u001B[0m \u001B[43m \u001B[49m\u001B[43mfiles\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_files\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1108\u001B[0m \u001B[43m \u001B[49m\u001B[43mauth_settings\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_auth_settings\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1109\u001B[0m \u001B[43m \u001B[49m\u001B[43mcollection_formats\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_collection_formats\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1110\u001B[0m \u001B[43m \u001B[49m\u001B[43m_host\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_host\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 1111\u001B[0m \u001B[43m \u001B[49m\u001B[43m_request_auth\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m_request_auth\u001B[49m\n\u001B[1;32m 1112\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:214\u001B[0m, in \u001B[0;36mApiClient.param_serialize\u001B[0;34m(self, method, resource_path, path_params, query_params, header_params, body, post_params, files, auth_settings, collection_formats, _host, _request_auth)\u001B[0m\n\u001B[1;32m 209\u001B[0m post_params \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mparameters_to_tuples(\n\u001B[1;32m 210\u001B[0m post_params,\n\u001B[1;32m 211\u001B[0m collection_formats\n\u001B[1;32m 212\u001B[0m )\n\u001B[1;32m 213\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m files:\n\u001B[0;32m--> 214\u001B[0m post_params\u001B[38;5;241m.\u001B[39mextend(\u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mfiles_parameters\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfiles\u001B[49m\u001B[43m)\u001B[49m)\n\u001B[1;32m 216\u001B[0m \u001B[38;5;66;03m# auth setting\u001B[39;00m\n\u001B[1;32m 217\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mupdate_params_for_auth(\n\u001B[1;32m 218\u001B[0m header_params,\n\u001B[1;32m 219\u001B[0m query_params,\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 224\u001B[0m request_auth\u001B[38;5;241m=\u001B[39m_request_auth\n\u001B[1;32m 225\u001B[0m )\n",
"File \u001B[0;32m~/PycharmProjects/heidi-v2/backend/aareDBclient/api_client.py:554\u001B[0m, in \u001B[0;36mApiClient.files_parameters\u001B[0;34m(self, files)\u001B[0m\n\u001B[1;32m 552\u001B[0m filedata \u001B[38;5;241m=\u001B[39m v\n\u001B[1;32m 553\u001B[0m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[0;32m--> 554\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mUnsupported file value\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[1;32m 555\u001B[0m mimetype \u001B[38;5;241m=\u001B[39m (\n\u001B[1;32m 556\u001B[0m mimetypes\u001B[38;5;241m.\u001B[39mguess_type(filename)[\u001B[38;5;241m0\u001B[39m]\n\u001B[1;32m 557\u001B[0m \u001B[38;5;129;01mor\u001B[39;00m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mapplication/octet-stream\u001B[39m\u001B[38;5;124m'\u001B[39m\n\u001B[1;32m 558\u001B[0m )\n\u001B[1;32m 559\u001B[0m params\u001B[38;5;241m.\u001B[39mappend(\n\u001B[1;32m 560\u001B[0m \u001B[38;5;28mtuple\u001B[39m([k, \u001B[38;5;28mtuple\u001B[39m([filename, filedata, mimetype])])\n\u001B[1;32m 561\u001B[0m )\n",
"\u001B[0;31mValueError\u001B[0m: Unsupported file value"
]
}
],
"execution_count": 77
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2025-01-20T15:14:51.219091Z",
"start_time": "2025-01-20T15:14:51.216755Z"
}
},
"cell_type": "code",
"source": "help(api_instance.upload_sample_images_samples_samples_sample_id_upload_images_post)",
"id": "8dd70634ffa5f37e",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Help on method upload_sample_images_samples_samples_sample_id_upload_images_post in module aareDBclient.api.samples_api:\n",
"\n",
"upload_sample_images_samples_samples_sample_id_upload_images_post(sample_id: Annotated[int, Strict(strict=True)], uploaded_files: List[Union[Annotated[bytes, Strict(strict=True)], Annotated[str, Strict(strict=True)]]], _request_timeout: Union[NoneType, Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])], Tuple[Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])], Annotated[float, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Gt(gt=0)])]]] = None, _request_auth: Optional[Dict[Annotated[str, Strict(strict=True)], Any]] = None, _content_type: Optional[Annotated[str, Strict(strict=True)]] = None, _headers: Optional[Dict[Annotated[str, Strict(strict=True)], Any]] = None, _host_index: Annotated[int, Strict(strict=True), FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0), Le(le=0)])] = 0) -> object method of aareDBclient.api.samples_api.SamplesApi instance\n",
" Upload Sample Images\n",
"\n",
" Uploads images for a sample and stores them in a directory structure: images/user/date/dewar_name/puck_name/position/. Args: sample_id (int): ID of the sample. uploaded_files (Union[List[UploadFile], List[bytes]]): List of image files (as UploadFile or raw bytes). db (Session): SQLAlchemy database session.\n",
"\n",
" :param sample_id: (required)\n",
" :type sample_id: int\n",
" :param uploaded_files: (required)\n",
" :type uploaded_files: List[bytearray]\n",
" :param _request_timeout: timeout setting for this request. If one\n",
" number provided, it will be total request\n",
" timeout. It can also be a pair (tuple) of\n",
" (connection, read) timeouts.\n",
" :type _request_timeout: int, tuple(int, int), optional\n",
" :param _request_auth: set to override the auth_settings for an a single\n",
" request; this effectively ignores the\n",
" authentication in the spec for a single request.\n",
" :type _request_auth: dict, optional\n",
" :param _content_type: force content-type for the request.\n",
" :type _content_type: str, Optional\n",
" :param _headers: set to override the headers for a single\n",
" request; this effectively ignores the headers\n",
" in the spec for a single request.\n",
" :type _headers: dict, optional\n",
" :param _host_index: set to override the host_index for a single\n",
" request; this effectively ignores the host_index\n",
" in the spec for a single request.\n",
" :type _host_index: int, optional\n",
" :return: Returns the result object.\n",
"\n"
]
}
],
"execution_count": 51
}
],
"metadata": {