**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:
parent
4630bcfac5
commit
6cde57f783
@ -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
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
7
backend/app/routers/protected_router.py
Normal file
7
backend/app/routers/protected_router.py
Normal 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
|
||||||
|
)
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"])
|
||||||
|
BIN
backend/tests/sample_image/IMG_1942.jpg
Normal file
BIN
backend/tests/sample_image/IMG_1942.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 576 KiB |
27
frontend/package-lock.json
generated
27
frontend/package-lock.json
generated
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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 />
|
||||||
|
@ -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}
|
||||||
|
@ -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 }}>
|
||||||
|
@ -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: '',
|
||||||
|
@ -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"
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
16
frontend/src/utils/auth.ts
Normal file
16
frontend/src/utils/auth.ts
Normal 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.");
|
||||||
|
};
|
@ -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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user