Add beamtime functionality to backend.
Introduce new endpoint and model for managing beamtimes, including shifts and user-specific access. Updated test scripts and data to reflect beamtime integration, along with minor fixes for job status enumeration and example notebook refinement.
This commit is contained in:
parent
db6474c86a
commit
102a11eed7
@ -395,10 +395,11 @@ beamtimes = [
|
||||
Beamtime(
|
||||
id=1,
|
||||
pgroups="p20001",
|
||||
shift="morning",
|
||||
beamtime_name="p20001-test",
|
||||
beamline="X06DA",
|
||||
start_date=datetime.strptime("06.02.2025", "%d.%m.%Y").date(),
|
||||
end_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(),
|
||||
start_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
|
||||
end_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
|
||||
status="confirmed",
|
||||
comments="this is a test beamtime",
|
||||
proposal_id=1,
|
||||
@ -407,10 +408,11 @@ beamtimes = [
|
||||
Beamtime(
|
||||
id=2,
|
||||
pgroups="p20002",
|
||||
shift="afternoon",
|
||||
beamtime_name="p20001-test",
|
||||
beamline="X06DA",
|
||||
start_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(),
|
||||
end_date=datetime.strptime("08.02.2025", "%d.%m.%Y").date(),
|
||||
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
|
||||
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
|
||||
status="confirmed",
|
||||
comments="this is a test beamtime",
|
||||
proposal_id=2,
|
||||
|
@ -8,6 +8,7 @@ from sqlalchemy import (
|
||||
DateTime,
|
||||
Boolean,
|
||||
func,
|
||||
Enum,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from .database import Base
|
||||
@ -235,11 +236,15 @@ class PuckEvent(Base):
|
||||
puck = relationship("Puck", back_populates="events")
|
||||
|
||||
|
||||
SHIFT_CHOICES = ("morning", "afternoon", "night")
|
||||
|
||||
|
||||
class Beamtime(Base):
|
||||
__tablename__ = "beamtimes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
pgroups = Column(String(255), nullable=False)
|
||||
shift = Column(Enum(*SHIFT_CHOICES, name="shift_enum"), nullable=False, index=True)
|
||||
beamtime_name = Column(String(255), index=True)
|
||||
beamline = Column(String(255), nullable=True)
|
||||
start_date = Column(Date, nullable=True)
|
||||
@ -282,6 +287,7 @@ class Results(Base):
|
||||
__tablename__ = "results"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
status = Column(String(255), nullable=False)
|
||||
result = Column(JSON, nullable=False) # store the full result object as JSON
|
||||
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
|
||||
run_id = Column(Integer, ForeignKey("experiment_parameters.id"), nullable=False)
|
||||
@ -310,7 +316,7 @@ class Results(Base):
|
||||
|
||||
|
||||
class JobStatus(str, enum.Enum):
|
||||
TODO = "todo"
|
||||
TO_DO = "to_do"
|
||||
SUBMITTED = "submitted"
|
||||
DONE = "done"
|
||||
TO_CANCEL = "to_cancel"
|
||||
|
72
backend/app/routers/beamtime.py
Normal file
72
backend/app/routers/beamtime.py
Normal file
@ -0,0 +1,72 @@
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app.models import Beamtime as BeamtimeModel
|
||||
from app.schemas import Beamtime as BeamtimeSchema, BeamtimeCreate, loginData
|
||||
from app.dependencies import get_db
|
||||
from app.routers.auth import get_current_user
|
||||
|
||||
beamtime_router = APIRouter()
|
||||
|
||||
|
||||
@beamtime_router.post("/", response_model=BeamtimeSchema)
|
||||
async def create_beamtime(
|
||||
beamtime: BeamtimeCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: loginData = Depends(get_current_user),
|
||||
):
|
||||
# Validate the pgroup belongs to the current user
|
||||
if beamtime.pgroups not in current_user.pgroups:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="You do not have permission to create a beamtime for this pgroup.",
|
||||
)
|
||||
|
||||
# Check for existing beamtime for this pgroup, date, and shift
|
||||
existing = (
|
||||
db.query(BeamtimeModel)
|
||||
.filter(
|
||||
BeamtimeModel.pgroups == beamtime.pgroups,
|
||||
BeamtimeModel.start_date == beamtime.start_date,
|
||||
BeamtimeModel.shift == beamtime.shift,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="A beamtime for this pgroup/shift/date already exists.",
|
||||
)
|
||||
|
||||
db_beamtime = BeamtimeModel(
|
||||
pgroups=beamtime.pgroups,
|
||||
shift=beamtime.shift,
|
||||
beamtime_name=beamtime.beamtime_name,
|
||||
beamline=beamtime.beamline,
|
||||
start_date=beamtime.start_date,
|
||||
end_date=beamtime.end_date,
|
||||
status=beamtime.status,
|
||||
comments=beamtime.comments,
|
||||
proposal_id=beamtime.proposal_id,
|
||||
local_contact_id=beamtime.local_contact_id,
|
||||
)
|
||||
|
||||
db.add(db_beamtime)
|
||||
db.commit()
|
||||
db.refresh(db_beamtime)
|
||||
return db_beamtime
|
||||
|
||||
|
||||
@beamtime_router.get(
|
||||
"/my-beamtimes",
|
||||
response_model=list[BeamtimeSchema],
|
||||
)
|
||||
async def get_my_beamtimes(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: loginData = Depends(get_current_user),
|
||||
):
|
||||
user_pgroups = current_user.pgroups
|
||||
filters = [BeamtimeModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
|
||||
beamtimes = db.query(BeamtimeModel).filter(or_(*filters)).all()
|
||||
return beamtimes
|
@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends
|
||||
|
||||
from app.routers.auth import get_current_user
|
||||
from app.routers.address import address_router
|
||||
from app.routers.beamtime import beamtime_router
|
||||
from app.routers.contact import contact_router
|
||||
from app.routers.shipment import shipment_router
|
||||
from app.routers.dewar import dewar_router
|
||||
@ -20,3 +21,6 @@ protected_router.include_router(
|
||||
shipment_router, prefix="/shipments", tags=["shipments"]
|
||||
)
|
||||
protected_router.include_router(dewar_router, prefix="/dewars", tags=["dewars"])
|
||||
protected_router.include_router(
|
||||
beamtime_router, prefix="/beamtimes", tags=["beamtimes"]
|
||||
)
|
||||
|
@ -399,7 +399,7 @@ def update_experiment_run_dataset(
|
||||
sample_id=sample_id,
|
||||
run_id=run_id,
|
||||
experiment_parameters=exp, # adjust this line as appropriate
|
||||
status=JobStatus.TODO,
|
||||
status=JobStatus.TO_DO,
|
||||
)
|
||||
db.add(new_job)
|
||||
db.commit()
|
||||
|
@ -772,6 +772,7 @@ class PuckWithTellPosition(BaseModel):
|
||||
class Beamtime(BaseModel):
|
||||
id: int
|
||||
pgroups: str
|
||||
shift: str
|
||||
beamtime_name: str
|
||||
beamline: str
|
||||
start_date: date
|
||||
@ -779,7 +780,6 @@ class Beamtime(BaseModel):
|
||||
status: str
|
||||
comments: Optional[constr(max_length=200)] = None
|
||||
proposal_id: Optional[int]
|
||||
proposal: Optional[Proposal]
|
||||
local_contact_id: Optional[int]
|
||||
local_contact: Optional[LocalContact]
|
||||
|
||||
@ -787,6 +787,19 @@ class Beamtime(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class BeamtimeCreate(BaseModel):
|
||||
pgroups: str # this should be changed to pgroup
|
||||
shift: str
|
||||
beamtime_name: str
|
||||
beamline: str
|
||||
start_date: date
|
||||
end_date: date
|
||||
status: str
|
||||
comments: Optional[constr(max_length=200)] = None
|
||||
proposal_id: Optional[int]
|
||||
local_contact_id: Optional[int]
|
||||
|
||||
|
||||
class ImageCreate(BaseModel):
|
||||
pgroup: str
|
||||
sample_id: int
|
||||
@ -940,6 +953,7 @@ class SampleResult(BaseModel):
|
||||
|
||||
class ResultCreate(BaseModel):
|
||||
sample_id: int
|
||||
status: str
|
||||
run_id: int
|
||||
result: Results
|
||||
|
||||
@ -949,6 +963,7 @@ class ResultCreate(BaseModel):
|
||||
|
||||
class ResultResponse(BaseModel):
|
||||
id: int
|
||||
status: str
|
||||
sample_id: int
|
||||
run_id: int
|
||||
result: Results
|
||||
|
@ -13,14 +13,17 @@ services:
|
||||
- ./app:/app/app # Map app directory to /app/app
|
||||
- ./config_${ENVIRONMENT}.json:/app/backend/config_${ENVIRONMENT}.json # Explicitly map config_dev.json
|
||||
- ./backend/ssl:/app/backend/ssl # clearly mount SSL files explicitly into Docker
|
||||
- ./uploads:/app/backend/uploads
|
||||
- ./uploads:/app/backend/images
|
||||
|
||||
working_dir: /app/backend # Set working directory to backend/
|
||||
command: python main.py # Command to run main.py
|
||||
depends_on: # ⬅️ New addition: wait until postgres is started
|
||||
- postgres
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "curl -k -f https://localhost:${PORT}/openapi.json || exit 1" ]
|
||||
interval: 1m
|
||||
timeout: 10s
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
environment: # ⬅️ Provide DB info to your backend
|
||||
ENVIRONMENT: ${ENVIRONMENT}
|
||||
@ -39,7 +42,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./db_data:/var/lib/postgresql/data
|
||||
|
||||
|
||||
frontend:
|
||||
|
@ -5,7 +5,7 @@ import './SampleImage.css';
|
||||
import './ResultGrid.css';
|
||||
import { OpenAPI, SamplesService } from '../../openapi';
|
||||
import ScheduleIcon from '@mui/icons-material/Schedule';
|
||||
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
||||
import DoDisturbIcon from '@mui/icons-material/DoDisturb';
|
||||
import TaskAltIcon from '@mui/icons-material/TaskAlt';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
@ -152,6 +152,8 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
|
||||
);
|
||||
case 'failed':
|
||||
return <ErrorOutlineIcon color="error" titleAccess="Failed" />;
|
||||
case 'cancelled':
|
||||
return <DoDisturbIcon color="disabled" titleAccess="Cancelled" />;
|
||||
case 'no job':
|
||||
default:
|
||||
return <InfoOutlinedIcon color="disabled" titleAccess="No job" />;
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user