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

View File

@ -46,10 +46,14 @@ class Address(Base):
__tablename__ = "addresses" __tablename__ = "addresses"
id = Column(Integer, primary_key=True, index=True, autoincrement=True) id = Column(Integer, primary_key=True, index=True, autoincrement=True)
street = Column(String(255)) status = Column(String(255), default="active")
city = Column(String(255)) pgroups = Column(String(255), nullable=False)
zipcode = Column(String(255)) street = Column(String(255), nullable=False)
country = Column(String(255)) 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") 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 .contact import router as contact_router
from .proposal import router as proposal_router from .proposal import router as proposal_router
from .dewar import router as dewar_router from .dewar import router as dewar_router
from .shipment import router as shipment_router from .shipment import router as shipment_router
from .auth import router as auth_router from .auth import router as auth_router
from .protected_router import protected_router as protected_router
__all__ = [ __all__ = [
"address_router", "address_router",
@ -12,4 +13,5 @@ __all__ = [
"dewar_router", "dewar_router",
"shipment_router", "shipment_router",
"auth_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.orm import Session
from sqlalchemy import or_
from typing import List 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.models import Address as AddressModel
from app.dependencies import get_db from app.dependencies import get_db
from app.routers.protected_router import protected_router
router = APIRouter()
@router.get("/", response_model=List[AddressSchema]) @protected_router.get("/", response_model=List[AddressSchema])
async def get_return_addresses(db: Session = Depends(get_db)): async def get_return_addresses(
return db.query(AddressModel).all() 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)): 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(): if db.query(AddressModel).filter(AddressModel.city == address.city).first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, 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( db_address = AddressModel(
pgroups=address.pgroups,
house_number=address.house_number,
street=address.street, street=address.street,
city=address.city, city=address.city,
state=address.state,
zipcode=address.zipcode, zipcode=address.zipcode,
country=address.country, country=address.country,
status="active",
) )
db.add(db_address) db.add(db_address)
@ -34,29 +81,74 @@ async def create_return_address(address: AddressCreate, db: Session = Depends(ge
return db_address return db_address
@router.put("/{address_id}", response_model=AddressSchema) @protected_router.put("/{address_id}", response_model=AddressSchema)
async def update_return_address( async def update_return_address(
address_id: int, address: AddressUpdate, db: Session = Depends(get_db) 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() db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address: if not db_address:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found." 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.commit()
db.refresh(db_address) 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)): async def delete_return_address(address_id: int, db: Session = Depends(get_db)):
db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first() db_address = db.query(AddressModel).filter(AddressModel.id == address_id).first()
if not db_address: if not db_address:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Address not found." 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() db.commit()
return return

View File

@ -14,8 +14,13 @@ mock_users_db = {
"testuser": { "testuser": {
"username": "testuser", "username": "testuser",
"password": "testpass", # In a real scenario, store the hash of the password "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: 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: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub") username: str = payload.get("sub")
pgroups = payload.get("pgroups") print(f"[DEBUG] Username decoded from token: {username}") # Add debug log here
return loginData(username=username, pgroups=payload.get("pgroups"))
if username is None:
raise credentials_exception
token_data = loginData(username=username, pgroups=pgroups)
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:
raise token_expired_exception print("[DEBUG] Token expired")
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
raise credentials_exception print("[DEBUG] Invalid token")
raise HTTPException(status_code=401, detail="Invalid token")
return token_data
@router.post("/token/login", response_model=loginToken) @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 ( from app.schemas import (
Puck as PuckSchema, Puck as PuckSchema,
Sample as SampleSchema, Sample as SampleSchema,
SampleEventResponse,
SampleEventCreate, SampleEventCreate,
Sample, Sample,
) )
@ -90,106 +89,69 @@ async def create_sample_event(
return sample # Return the sample, now including `mount_count` 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") @router.post("/samples/{sample_id}/upload-images")
async def upload_sample_images( async def upload_sample_images(
sample_id: int, sample_id: int,
uploaded_files: List[UploadFile] = File(...), # Accept multiple files uploaded_files: list[UploadFile] = File(...),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
""" logging.info(f"Received files: {[file.filename for file in uploaded_files]}")
Uploads images for a sample and stores them in a directory structure:
images/user/date/dewar_name/puck_name/position/.
"""
Uploads images for a given sample and saves them to a directory structure.
Args: Args:
sample_id (int): ID of the sample. sample_id (int): ID of the sample.
uploaded_files (List[UploadFile]): List of image files to be uploaded. uploaded_files (list[UploadFile]): A list of files uploaded with the request.
db (Session): SQLAlchemy database session. db (Session): Database session.
""" """
# Fetch sample details from the database
# 1. Validate Sample
sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first() sample = db.query(SampleModel).filter(SampleModel.id == sample_id).first()
if not sample: if not sample:
raise HTTPException(status_code=404, detail="Sample not found") raise HTTPException(status_code=404, detail="Sample not found")
# Retrieve associated dewar_name, puck_name and position # 2. Define Directory Structure
puck = sample.puck username = "e16371" # Hardcoded username; replace with dynamic logic if applicable
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
today = datetime.now().strftime("%Y-%m-%d") today = datetime.now().strftime("%Y-%m-%d")
dewar_name = (
# Generate the directory path based on the structure sample.puck.dewar.dewar_name
base_dir = ( if sample.puck and sample.puck.dewar
Path("images") / username / today / dewar_name / puck_name / str(position) else "default_dewar"
) )
puck_name = sample.puck.puck_name if sample.puck else "default_puck"
# Create directories if they don't exist 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) 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: for file in uploaded_files:
# Validate file content type # Validate MIME type
if not file.content_type.startswith("image/"): if not file.content_type.startswith("image/"):
raise HTTPException( raise HTTPException(
status_code=400, 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 file_path = base_dir / file.filename
# Save the file from the file stream
try: try:
# Save the file
with file_path.open("wb") as buffer: with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer) shutil.copyfileobj(file.file, buffer)
saved_files.append(str(file_path)) # Track saved file paths
except Exception as e: except Exception as e:
logging.error(f"Error saving file {file.filename}: {str(e)}")
raise HTTPException( raise HTTPException(
status_code=500, 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 { return {
"message": f"{len(uploaded_files)} images uploaded successfully.", "message": f"{len(saved_files)} images uploaded successfully.",
"path": str(base_dir), # Return the base directory for reference "files": saved_files,
} }

View File

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

View File

@ -18,6 +18,7 @@ from app.routers import (
sample, sample,
) )
from app.database import Base, engine, SessionLocal from app.database import Base, engine, SessionLocal
from app.routers.protected_router import protected_router
# Utility function to fetch metadata from pyproject.toml # Utility function to fetch metadata from pyproject.toml
@ -139,8 +140,8 @@ def on_startup():
load_slots_data(db) load_slots_data(db)
else: # dev or test environments else: # dev or test environments
print(f"{environment.capitalize()} environment: Regenerating database.") print(f"{environment.capitalize()} environment: Regenerating database.")
# Base.metadata.drop_all(bind=engine) Base.metadata.drop_all(bind=engine)
# Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
if environment == "dev": if environment == "dev":
from app.database import load_sample_data from app.database import load_sample_data
@ -154,9 +155,10 @@ def on_startup():
# Include routers with correct configuration # 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(auth.router, prefix="/auth", tags=["auth"])
app.include_router(contact.router, prefix="/contacts", tags=["contacts"]) 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(proposal.router, prefix="/proposals", tags=["proposals"])
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"]) app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"]) 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", "exceljs": "^4.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"jwt-decode": "^4.0.0",
"openapi-typescript-codegen": "^0.29.0", "openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-big-calendar": "^1.15.0", "react-big-calendar": "^1.15.0",
@ -485,6 +486,15 @@
"@devexpress/dx-core": "4.0.10" "@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": { "node_modules/@emotion/babel-plugin": {
"version": "11.13.5", "version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@ -4732,6 +4742,15 @@
"safe-buffer": "~5.1.0" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -5992,10 +6011,12 @@
} }
}, },
"node_modules/rrule": { "node_modules/rrule": {
"version": "2.7.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
"integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==", "integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }

View File

@ -35,6 +35,7 @@
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"jwt-decode": "^4.0.0",
"openapi-typescript-codegen": "^0.29.0", "openapi-typescript-codegen": "^0.29.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-big-calendar": "^1.15.0", "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 { 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 ResponsiveAppBar from './components/ResponsiveAppBar';
import ShipmentView from './pages/ShipmentView'; import ShipmentView from './pages/ShipmentView';
@ -16,38 +17,77 @@ const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false); const [openAddressManager, setOpenAddressManager] = useState(false);
const [openContactsManager, setOpenContactsManager] = useState(false); const [openContactsManager, setOpenContactsManager] = useState(false);
const handleOpenAddressManager = () => { const handleOpenAddressManager = () => setOpenAddressManager(true);
setOpenAddressManager(true); const handleCloseAddressManager = () => setOpenAddressManager(false);
}; const handleOpenContactsManager = () => setOpenContactsManager(true);
const handleCloseContactsManager = () => setOpenContactsManager(false);
const handleCloseAddressManager = () => { const [pgroups, setPgroups] = useState<string[]>([]);
setOpenAddressManager(false); const [activePgroup, setActivePgroup] = useState<string>('');
};
const handleOpenContactsManager = () => { // On app load, configure the token
setOpenContactsManager(true); useEffect(() => {
}; setUpToken(); // Ensure token is loaded into OpenAPI on app initialization
}, []);
const handleCloseContactsManager = () => { useEffect(() => {
setOpenContactsManager(false); 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 ( return (
<Router> <Router>
<ResponsiveAppBar <ResponsiveAppBar
activePgroup={activePgroup}
onOpenAddressManager={handleOpenAddressManager} onOpenAddressManager={handleOpenAddressManager}
onOpenContactsManager={handleOpenContactsManager} onOpenContactsManager={handleOpenContactsManager}
pgroups={pgroups || []} // Default to an empty array
currentPgroup={activePgroup}
onPgroupChange={handlePgroupChange}
/> />
<Routes> <Routes>
<Route path="/login" element={<LoginView />} /> <Route path="/login" element={<LoginView />} />
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} /> <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="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
<Route path="/results" element={<ProtectedRoute element={<ResultsView />} />} /> <Route path="/results" element={<ProtectedRoute element={<ResultsView />} />} />
{/* Other routes as necessary */}
</Routes> </Routes>
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management"> <Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
<AddressManager /> <AddressManager pgroups={pgroups} activePgroup={activePgroup} />
</Modal> </Modal>
<Modal open={openContactsManager} onClose={handleCloseContactsManager} title="Contacts Management"> <Modal open={openContactsManager} onClose={handleCloseContactsManager} title="Contacts Management">
<ContactsManager /> <ContactsManager />

View File

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

View File

@ -7,25 +7,43 @@ import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container'; import Container from '@mui/material/Container';
import Avatar from '@mui/material/Avatar'; import Avatar from '@mui/material/Avatar';
import Tooltip from '@mui/material/Tooltip'; 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 { Link } from 'react-router-dom';
import logo from '../assets/icons/psi_01_sn.svg'; import logo from '../assets/icons/psi_01_sn.svg';
import '../App.css'; import '../App.css';
import { clearToken } from '../utils/auth';
interface ResponsiveAppBarProps { interface ResponsiveAppBarProps {
activePgroup: string;
onOpenAddressManager: () => void; onOpenAddressManager: () => void;
onOpenContactsManager: () => 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 navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null); const [anchorElNav, setAnchorElNav] = useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = 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 = [ const pages = [
{ name: 'Home', path: '/' }, { name: 'Home', path: '/' },
@ -59,8 +77,8 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
const handleLogout = () => { const handleLogout = () => {
console.log("Performing logout..."); console.log("Performing logout...");
localStorage.removeItem('token'); clearToken(); // Clear the token from localStorage and OpenAPI
navigate('/login'); navigate('/login'); // Redirect to login page
}; };
const handleMenuItemClick = (action: string) => { const handleMenuItemClick = (action: string) => {
@ -123,7 +141,31 @@ const ResponsiveAppBar: React.FC<ResponsiveAppBarProps> = ({ onOpenAddressManage
</Button> </Button>
))} ))}
</Box> </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 }}> <Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings"> <Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>

View File

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

View File

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

View File

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

View File

@ -2,26 +2,57 @@ import React from 'react';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { CountryList } from '../components/CountryList'; import { CountryList } from '../components/CountryList';
import { 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'; } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import { AddressesService } from '../../openapi'; import {Address, AddressCreate, AddressesService, AddressUpdate} from '../../openapi';
import type { Address, AddressCreate, AddressUpdate } from '../models/Address';
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, { const fuse = new Fuse(CountryList, {
threshold: 0.3, // Lower threshold for stricter matches threshold: 0.3, // Lower threshold for stricter matches
includeScore: true, 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 [countrySuggestions, setCountrySuggestions] = React.useState<string[]>([]);
const [addresses, setAddresses] = React.useState<Address[]>([]); const [addresses, setAddresses] = React.useState<Address[]>([]);
const [newAddress, setNewAddress] = React.useState<Partial<Address>>({ const [newAddress, setNewAddress] = React.useState<Partial<Address>>({
house_number: '',
street: '', street: '',
city: '', city: '',
state: '',
zipcode: '', zipcode: '',
country: '', country: '',
}); });
@ -45,18 +76,28 @@ const AddressManager: React.FC = () => {
}; };
React.useEffect(() => { React.useEffect(() => {
const fetchAddresses = async () => { const fetchAllData = async () => {
try { try {
const response = await AddressesService.getReturnAddressesAddressesGet(); const response = await AddressesService.getAllAddressesAddressesAllGet();
setAddresses(response);
// 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) { } catch (error) {
console.error('Failed to fetch addresses', 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.');
} }
}; };
fetchAllData();
fetchAddresses(); }, [pgroups]);
}, []);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target; const { name, value } = event.target;
@ -66,30 +107,40 @@ const AddressManager: React.FC = () => {
const handleAddOrUpdateAddress = async () => { const handleAddOrUpdateAddress = async () => {
try { try {
if (editAddressId !== null) { if (editAddressId !== null) {
// Update address // Update address (mark old one obsolete, create a new one)
await AddressesService.updateReturnAddressAddressesAddressIdPut(editAddressId, newAddress as AddressUpdate); const updatedAddress = await AddressesService.updateReturnAddressAddressesAddressIdPut(
setAddresses(addresses.map(address => address.id === editAddressId ? { ...address, ...newAddress } : address)); 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); setEditAddressId(null);
} else { } else {
// Add new address // Add new address
const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate); const response = await AddressesService.createReturnAddressAddressesPost(newAddress as AddressCreate);
setAddresses([...addresses, response]); setAddresses([...addresses, response]);
} }
setNewAddress({ street: '', city: '', zipcode: '', country: '' }); setNewAddress({ house_number:'', street: '', city: '', state: '', zipcode: '', country: '' });
setErrorMessage(null); setErrorMessage(null);
} catch (error) { } catch (error) {
console.error('Failed to add/update address', 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) => { const handleDeleteAddress = async (id: number) => {
try { try {
// Delete (inactivate) the address
await AddressesService.deleteReturnAddressAddressesAddressIdDelete(id); 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) { } catch (error) {
console.error('Failed to delete address', 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 ( return (
<Container> <Container>
<Typography variant="h4" gutterBottom> <Typography variant="h4" gutterBottom>
Addresses Management Addresses Management
</Typography> </Typography>
<Box display="flex" justifyContent="center" alignItems="center" mb={3}> <Box display="flex" justifyContent="center" alignItems="center" mb={3} gap={2}>
<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} />
<TextField <TextField
label="Country" label="pgroup"
name="country" name="pgroup"
value={newAddress.country || ''} value={newAddress.activePgroup || ''}
onChange={handleCountryInputChange} disabled
fullWidth sx={{ width: '120px' }} // Small fixed-size for non-editable field
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'
: ''
}
/> />
<TextField
{/* Render suggestions dynamically */} label="Number"
<Box sx={{ position: 'relative' }}> 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 <TextField
label="Country" label="Country"
name="country" name="country"
value={newAddress.country || ''} value={newAddress.country || ''}
onChange={handleCountryInputChange} onChange={handleCountryInputChange}
fullWidth 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 && ( {countrySuggestions.length > 0 && (
<Box <Box
sx={{ sx={{
border: '1px solid #ccc', border: '1px solid #ccc',
borderRadius: '4px', borderRadius: '4px',
background: 'white', background: 'white',
marginTop: '4px', /* Add space below the input */
position: 'absolute', position: 'absolute',
width: '100%', /* Match the TextField width */ top: '100%', // Place below the TextField
zIndex: 10, /* Ensure it is above other UI elements */ left: 0,
zIndex: 10,
width: '100%', // Match the width of the TextField
marginTop: '4px', // Small spacing below the TextField
}} }}
> >
{countrySuggestions.map((suggestion, index) => ( {countrySuggestions.map((suggestion, index) => (
@ -172,7 +320,6 @@ const AddressManager: React.FC = () => {
'&:hover': { background: '#f5f5f5' }, '&:hover': { background: '#f5f5f5' },
}} }}
onClick={() => { onClick={() => {
// Update country field with the clicked suggestion
setNewAddress({ ...newAddress, country: suggestion }); setNewAddress({ ...newAddress, country: suggestion });
setCountrySuggestions([]); // Clear suggestions setCountrySuggestions([]); // Clear suggestions
}} }}
@ -193,14 +340,18 @@ const AddressManager: React.FC = () => {
addresses.map((address) => ( addresses.map((address) => (
<ListItem key={address.id} button> <ListItem key={address.id} button>
<ListItemText <ListItemText
primary={`${address.street}, ${address.city}`} primary={`${address.house_number}, ${address.street}, ${address.city}`}
secondary={`${address.zipcode} - ${address.country}`} secondary={
<Box display="flex" flexWrap="wrap">
{renderPgroupChips(address)}
</Box>
}
/> />
<ListItemSecondaryAction> <ListItemSecondaryAction>
<IconButton edge="end" color="primary" onClick={() => handleEditAddress(address)}> <IconButton edge="end" color="primary" onClick={() => handleEditAddress(address)}>
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
<IconButton edge="end" color="secondary" onClick={() => openDialog(address)}> <IconButton edge="end" color="secondary" onClick={() => handleDeleteAddress(address.id)}>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</ListItemSecondaryAction> </ListItemSecondaryAction>

View File

@ -89,10 +89,25 @@ const LoginView: React.FC = () => {
password: password, password: password,
}); });
// Save the token
localStorage.setItem('token', response.access_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) { } catch (err) {
setError('Login failed. Please check your credentials.'); 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 useShipments from '../hooks/useShipments';
import { Grid, Container } from '@mui/material'; 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 { shipments, error, defaultContactPerson, fetchAndSetShipments } = useShipments();
const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null); const [selectedShipment, setSelectedShipment] = useState<Shipment | null>(null);
const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null); const [selectedDewar, setSelectedDewar] = useState<Dewar | null>(null);
const [isCreatingShipment, setIsCreatingShipment] = useState(false); const [isCreatingShipment, setIsCreatingShipment] = useState(false);
useEffect(() => {
fetchAndSetShipments();
}, [activePgroup]);
useEffect(() => { useEffect(() => {
// Detect the current environment // Detect the current environment
const mode = import.meta.env.MODE; const mode = import.meta.env.MODE;
@ -52,6 +58,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (isCreatingShipment) { if (isCreatingShipment) {
return ( return (
<ShipmentForm <ShipmentForm
activePgroup={activePgroup}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
onCancel={handleCancelShipmentForm} onCancel={handleCancelShipmentForm}
refreshShipments={fetchAndSetShipments} refreshShipments={fetchAndSetShipments}
@ -61,6 +68,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
if (selectedShipment) { if (selectedShipment) {
return ( return (
<ShipmentDetails <ShipmentDetails
activePgroup={activePgroup}
isCreatingShipment={isCreatingShipment} isCreatingShipment={isCreatingShipment}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
selectedShipment={selectedShipment} selectedShipment={selectedShipment}
@ -89,6 +97,7 @@ const ShipmentView: React.FC<ShipmentViewProps> = () => {
}} }}
> >
<ShipmentPanel <ShipmentPanel
activePgroup={activePgroup}
setCreatingShipment={setIsCreatingShipment} setCreatingShipment={setIsCreatingShipment}
selectShipment={handleSelectShipment} selectShipment={handleSelectShipment}
shipments={shipments} 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": { "metadata": {
"collapsed": true, "collapsed": true,
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-01-17T14:03:52.460891Z", "end_time": "2025-01-20T14:44:37.526742Z",
"start_time": "2025-01-17T14:03:52.454842Z" "start_time": "2025-01-20T14:44:37.522704Z"
} }
}, },
"source": [ "source": [
"import json\n", "import json\n",
"\n", "\n",
"from nbclient.client import timestamp\n", "#from nbclient.client import timestamp\n",
"\n", "\n",
"import backend.aareDBclient as aareDBclient\n", "import backend.aareDBclient as aareDBclient\n",
"from aareDBclient.rest import ApiException\n", "from aareDBclient.rest import ApiException\n",
"from pprint import pprint\n", "from pprint import pprint\n",
"\n", "\n",
"from app.data.data import sample\n", "#from app.data.data import sample\n",
"\n", "\n",
"#from aareDBclient import SamplesApi, ShipmentsApi, PucksApi\n", "#from aareDBclient import SamplesApi, ShipmentsApi, PucksApi\n",
"#from aareDBclient.models import SampleEventCreate, SetTellPosition\n", "#from aareDBclient.models import SampleEventCreate, SetTellPosition\n",
@ -47,13 +47,13 @@
] ]
} }
], ],
"execution_count": 40 "execution_count": 2
}, },
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-01-17T14:13:16.559702Z", "end_time": "2025-01-20T14:44:58.875370Z",
"start_time": "2025-01-17T14:13:16.446021Z" "start_time": "2025-01-20T14:44:58.805520Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -152,7 +152,7 @@
] ]
} }
], ],
"execution_count": 44 "execution_count": 3
}, },
{ {
"metadata": { "metadata": {
@ -456,8 +456,8 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-01-20T11:10:20.017201Z", "end_time": "2025-01-20T14:45:11.812597Z",
"start_time": "2025-01-20T11:10:19.953940Z" "start_time": "2025-01-20T14:45:11.793309Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -503,7 +503,9 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Payload being sent to API:\n", "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", "/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" " 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": { "metadata": {
@ -575,6 +557,126 @@
} }
], ],
"execution_count": 7 "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": { "metadata": {