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(
|
Beamtime(
|
||||||
id=1,
|
id=1,
|
||||||
pgroups="p20001",
|
pgroups="p20001",
|
||||||
|
shift="morning",
|
||||||
beamtime_name="p20001-test",
|
beamtime_name="p20001-test",
|
||||||
beamline="X06DA",
|
beamline="X06DA",
|
||||||
start_date=datetime.strptime("06.02.2025", "%d.%m.%Y").date(),
|
start_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
|
||||||
end_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(),
|
end_date=datetime.strptime("06.05.2025", "%d.%m.%Y").date(),
|
||||||
status="confirmed",
|
status="confirmed",
|
||||||
comments="this is a test beamtime",
|
comments="this is a test beamtime",
|
||||||
proposal_id=1,
|
proposal_id=1,
|
||||||
@ -407,10 +408,11 @@ beamtimes = [
|
|||||||
Beamtime(
|
Beamtime(
|
||||||
id=2,
|
id=2,
|
||||||
pgroups="p20002",
|
pgroups="p20002",
|
||||||
|
shift="afternoon",
|
||||||
beamtime_name="p20001-test",
|
beamtime_name="p20001-test",
|
||||||
beamline="X06DA",
|
beamline="X06DA",
|
||||||
start_date=datetime.strptime("07.02.2025", "%d.%m.%Y").date(),
|
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
|
||||||
end_date=datetime.strptime("08.02.2025", "%d.%m.%Y").date(),
|
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
|
||||||
status="confirmed",
|
status="confirmed",
|
||||||
comments="this is a test beamtime",
|
comments="this is a test beamtime",
|
||||||
proposal_id=2,
|
proposal_id=2,
|
||||||
|
@ -8,6 +8,7 @@ from sqlalchemy import (
|
|||||||
DateTime,
|
DateTime,
|
||||||
Boolean,
|
Boolean,
|
||||||
func,
|
func,
|
||||||
|
Enum,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from .database import Base
|
from .database import Base
|
||||||
@ -235,11 +236,15 @@ class PuckEvent(Base):
|
|||||||
puck = relationship("Puck", back_populates="events")
|
puck = relationship("Puck", back_populates="events")
|
||||||
|
|
||||||
|
|
||||||
|
SHIFT_CHOICES = ("morning", "afternoon", "night")
|
||||||
|
|
||||||
|
|
||||||
class Beamtime(Base):
|
class Beamtime(Base):
|
||||||
__tablename__ = "beamtimes"
|
__tablename__ = "beamtimes"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
pgroups = Column(String(255), nullable=False)
|
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)
|
beamtime_name = Column(String(255), index=True)
|
||||||
beamline = Column(String(255), nullable=True)
|
beamline = Column(String(255), nullable=True)
|
||||||
start_date = Column(Date, nullable=True)
|
start_date = Column(Date, nullable=True)
|
||||||
@ -282,6 +287,7 @@ class Results(Base):
|
|||||||
__tablename__ = "results"
|
__tablename__ = "results"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
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
|
result = Column(JSON, nullable=False) # store the full result object as JSON
|
||||||
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
|
sample_id = Column(Integer, ForeignKey("samples.id"), nullable=False)
|
||||||
run_id = Column(Integer, ForeignKey("experiment_parameters.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):
|
class JobStatus(str, enum.Enum):
|
||||||
TODO = "todo"
|
TO_DO = "to_do"
|
||||||
SUBMITTED = "submitted"
|
SUBMITTED = "submitted"
|
||||||
DONE = "done"
|
DONE = "done"
|
||||||
TO_CANCEL = "to_cancel"
|
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.auth import get_current_user
|
||||||
from app.routers.address import address_router
|
from app.routers.address import address_router
|
||||||
|
from app.routers.beamtime import beamtime_router
|
||||||
from app.routers.contact import contact_router
|
from app.routers.contact import contact_router
|
||||||
from app.routers.shipment import shipment_router
|
from app.routers.shipment import shipment_router
|
||||||
from app.routers.dewar import dewar_router
|
from app.routers.dewar import dewar_router
|
||||||
@ -20,3 +21,6 @@ protected_router.include_router(
|
|||||||
shipment_router, prefix="/shipments", tags=["shipments"]
|
shipment_router, prefix="/shipments", tags=["shipments"]
|
||||||
)
|
)
|
||||||
protected_router.include_router(dewar_router, prefix="/dewars", tags=["dewars"])
|
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,
|
sample_id=sample_id,
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
experiment_parameters=exp, # adjust this line as appropriate
|
experiment_parameters=exp, # adjust this line as appropriate
|
||||||
status=JobStatus.TODO,
|
status=JobStatus.TO_DO,
|
||||||
)
|
)
|
||||||
db.add(new_job)
|
db.add(new_job)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
@ -772,6 +772,7 @@ class PuckWithTellPosition(BaseModel):
|
|||||||
class Beamtime(BaseModel):
|
class Beamtime(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
pgroups: str
|
pgroups: str
|
||||||
|
shift: str
|
||||||
beamtime_name: str
|
beamtime_name: str
|
||||||
beamline: str
|
beamline: str
|
||||||
start_date: date
|
start_date: date
|
||||||
@ -779,7 +780,6 @@ class Beamtime(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
comments: Optional[constr(max_length=200)] = None
|
comments: Optional[constr(max_length=200)] = None
|
||||||
proposal_id: Optional[int]
|
proposal_id: Optional[int]
|
||||||
proposal: Optional[Proposal]
|
|
||||||
local_contact_id: Optional[int]
|
local_contact_id: Optional[int]
|
||||||
local_contact: Optional[LocalContact]
|
local_contact: Optional[LocalContact]
|
||||||
|
|
||||||
@ -787,6 +787,19 @@ class Beamtime(BaseModel):
|
|||||||
from_attributes = True
|
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):
|
class ImageCreate(BaseModel):
|
||||||
pgroup: str
|
pgroup: str
|
||||||
sample_id: int
|
sample_id: int
|
||||||
@ -940,6 +953,7 @@ class SampleResult(BaseModel):
|
|||||||
|
|
||||||
class ResultCreate(BaseModel):
|
class ResultCreate(BaseModel):
|
||||||
sample_id: int
|
sample_id: int
|
||||||
|
status: str
|
||||||
run_id: int
|
run_id: int
|
||||||
result: Results
|
result: Results
|
||||||
|
|
||||||
@ -949,6 +963,7 @@ class ResultCreate(BaseModel):
|
|||||||
|
|
||||||
class ResultResponse(BaseModel):
|
class ResultResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
status: str
|
||||||
sample_id: int
|
sample_id: int
|
||||||
run_id: int
|
run_id: int
|
||||||
result: Results
|
result: Results
|
||||||
|
@ -13,14 +13,17 @@ services:
|
|||||||
- ./app:/app/app # Map app directory to /app/app
|
- ./app:/app/app # Map app directory to /app/app
|
||||||
- ./config_${ENVIRONMENT}.json:/app/backend/config_${ENVIRONMENT}.json # Explicitly map config_dev.json
|
- ./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
|
- ./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/
|
working_dir: /app/backend # Set working directory to backend/
|
||||||
command: python main.py # Command to run main.py
|
command: python main.py # Command to run main.py
|
||||||
depends_on: # ⬅️ New addition: wait until postgres is started
|
depends_on: # ⬅️ New addition: wait until postgres is started
|
||||||
- postgres
|
- postgres
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "curl -k -f https://localhost:${PORT}/openapi.json || exit 1" ]
|
test: [ "CMD-SHELL", "curl -k -f https://localhost:${PORT}/openapi.json || exit 1" ]
|
||||||
interval: 1m
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
environment: # ⬅️ Provide DB info to your backend
|
environment: # ⬅️ Provide DB info to your backend
|
||||||
ENVIRONMENT: ${ENVIRONMENT}
|
ENVIRONMENT: ${ENVIRONMENT}
|
||||||
@ -39,7 +42,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- ./db_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
@ -5,7 +5,7 @@ import './SampleImage.css';
|
|||||||
import './ResultGrid.css';
|
import './ResultGrid.css';
|
||||||
import { OpenAPI, SamplesService } from '../../openapi';
|
import { OpenAPI, SamplesService } from '../../openapi';
|
||||||
import ScheduleIcon from '@mui/icons-material/Schedule';
|
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 TaskAltIcon from '@mui/icons-material/TaskAlt';
|
||||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
@ -152,6 +152,8 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => {
|
|||||||
);
|
);
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return <ErrorOutlineIcon color="error" titleAccess="Failed" />;
|
return <ErrorOutlineIcon color="error" titleAccess="Failed" />;
|
||||||
|
case 'cancelled':
|
||||||
|
return <DoDisturbIcon color="disabled" titleAccess="Cancelled" />;
|
||||||
case 'no job':
|
case 'no job':
|
||||||
default:
|
default:
|
||||||
return <InfoOutlinedIcon color="disabled" titleAccess="No job" />;
|
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