Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion backend/app/features/simulation/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,43 @@
from app.common.dependencies import get_database_session
from app.core.database import transaction
from app.features.simulation.models import Artifact, ExternalLink, Simulation
from app.features.simulation.schemas import SimulationCreate, SimulationOut
from app.features.simulation.schemas import (
SimulationCreate,
SimulationOut,
SimulationUpdate,
)
from app.features.user.manager import current_active_user
from app.features.user.models import User

router = APIRouter(prefix="/simulations", tags=["Simulations"])


@router.patch("/{sim_id}", response_model=SimulationOut)
def update_simulation(
sim_id: UUID,
payload: SimulationUpdate,
db: Session = Depends(get_database_session),
user: User = Depends(current_active_user),
):
"""Update simulation details by ID."""
sim = db.query(Simulation).filter(Simulation.id == sim_id).first()
if not sim:
raise HTTPException(status_code=404, detail="Simulation not found")

update_data = payload.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(sim, field, value)

sim.last_updated_by = user.id
sim.updated_at = datetime.now(timezone.utc)

with transaction(db):
db.add(sim)
db.flush()

return SimulationOut.model_validate(sim, from_attributes=True)


@router.post("", response_model=SimulationOut, status_code=status.HTTP_201_CREATED)
def create_simulation(
payload: SimulationCreate,
Expand Down
13 changes: 7 additions & 6 deletions backend/app/features/simulation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import uuid
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -50,7 +51,7 @@ class Simulation(Base, IDMixin, TimestampMixin):
compset_alias: Mapped[str] = mapped_column(String(120))
grid_name: Mapped[str] = mapped_column(String(200))
grid_resolution: Mapped[str] = mapped_column(String(50))
parent_simulation_id: Mapped[UUID | None] = mapped_column(
parent_simulation_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("simulations.id")
)

Expand All @@ -68,7 +69,7 @@ class Simulation(Base, IDMixin, TimestampMixin):

# Model timeline
# ~~~~~~~~~~~~~~
machine_id: Mapped[UUID] = mapped_column(
machine_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("machines.id"), index=True
)
simulation_start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True))
Expand All @@ -94,10 +95,10 @@ class Simulation(Base, IDMixin, TimestampMixin):

# Provenance & submission
# ~~~~~~~~~~~~~~~~~~~~~~~
created_by: Mapped[UUID] = mapped_column(
created_by: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id"), index=True
)
last_updated_by: Mapped[UUID] = mapped_column(
last_updated_by: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id"), index=True
)

Expand Down Expand Up @@ -133,7 +134,7 @@ class Simulation(Base, IDMixin, TimestampMixin):
class Artifact(Base, IDMixin, TimestampMixin):
__tablename__ = "artifacts"

simulation_id: Mapped[UUID] = mapped_column(
simulation_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("simulations.id", ondelete="CASCADE"),
index=True,
Expand Down Expand Up @@ -165,7 +166,7 @@ class Artifact(Base, IDMixin, TimestampMixin):
class ExternalLink(Base, IDMixin, TimestampMixin):
__tablename__ = "external_links"

simulation_id: Mapped[UUID] = mapped_column(
simulation_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("simulations.id", ondelete="CASCADE")
)

Expand Down
125 changes: 86 additions & 39 deletions backend/app/features/simulation/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,38 +114,36 @@ class ArtifactOut(CamelOutBaseModel):
]


class SimulationCreate(CamelInBaseModel):
"""Schema for creating a new Simulation."""
class SimulationBase(CamelInBaseModel):
"""Base schema for Simulation shared fields."""

# Configuration
# --------------
name: Annotated[str, Field(..., description="Name of the simulation")]
name: Annotated[str | None, Field(None, description="Name of the simulation")]
case_name: Annotated[
str, Field(..., description="Case name associated with the simulation")
str | None, Field(None, description="Case name associated with the simulation")
]
description: Annotated[
str | None, Field(None, description="Optional description of the simulation")
]
compset: Annotated[
str, Field(..., description="Component set used in the simulation")
str | None, Field(None, description="Component set used in the simulation")
]
compset_alias: Annotated[
str | None, Field(None, description="Alias for the component set")
]
compset_alias: Annotated[str, Field(..., description="Alias for the component set")]
grid_name: Annotated[
str, Field(..., description="Grid name used in the simulation")
str | None, Field(None, description="Grid name used in the simulation")
]
grid_resolution: Annotated[
str, Field(..., description="Grid resolution used in the simulation")
str | None, Field(None, description="Grid resolution used in the simulation")
]
parent_simulation_id: Annotated[
UUID | None, Field(None, description="Optional ID of the parent simulation")
]

# Model setup/context
# -------------------
# TODO: Make simulation_type an Enum once we have a fixed set of types.
simulation_type: Annotated[str, Field(..., description="Type of the simulation")]
simulation_type: Annotated[
str | None, Field(None, description="Type of the simulation")
]
status: Annotated[
SimulationStatus, Field(..., description="Current status of the simulation")
str | None, Field(None, description="Current status of the simulation")
]
campaign_id: Annotated[
str | None, Field(None, description="Optional ID of the associated campaign")
Expand All @@ -154,20 +152,18 @@ class SimulationCreate(CamelInBaseModel):
str | None, Field(None, description="Optional ID of the experiment type")
]
initialization_type: Annotated[
str, Field(..., description="Initialization type for the simulation")
str | None, Field(None, description="Initialization type for the simulation")
]
group_name: Annotated[
str | None,
Field(None, description="Optional group name associated with the simulation"),
]

# Model timeline
# --------------
machine_id: Annotated[
UUID, Field(..., description="ID of the machine used for the simulation")
UUID | None,
Field(None, description="ID of the machine used for the simulation"),
]
simulation_start_date: Annotated[
datetime, Field(..., description="Start date of the simulation")
datetime | None, Field(None, description="Start date of the simulation")
]
simulation_end_date: Annotated[
datetime | None, Field(None, description="Optional end date of the simulation")
Expand All @@ -183,9 +179,6 @@ class SimulationCreate(CamelInBaseModel):
compiler: Annotated[
str | None, Field(None, description="Optional compiler used for the simulation")
]

# Metadata & audit
# -----------------
key_features: Annotated[
str | None, Field(None, description="Optional key features of the simulation")
]
Expand All @@ -196,11 +189,8 @@ class SimulationCreate(CamelInBaseModel):
str | None,
Field(None, description="Optional additional notes in markdown format"),
]

# Version control
# ---------------
git_repository_url: Annotated[
HttpUrl | None, Field(None, description="Optional Git repository URL")
str | None, Field(None, description="Optional Git repository URL")
]
git_branch: Annotated[
str | None,
Expand All @@ -218,25 +208,62 @@ class SimulationCreate(CamelInBaseModel):
),
]

# Provenance & submission
# -----------------------
created_by: Annotated[
UUID | None,
extra: Annotated[
dict | None,
Field(
None,
description="User ID who created the simulation, defined at creation time.",
description="Optional extra metadata in flexible dictionary/JSON format",
),
]
last_updated_by: Annotated[
UUID | None,
artifacts: Annotated[
list[ArtifactCreate] | None,
Field(
None,
description="User ID who last updated the simulation, defined at update time.",
description="Optional list of artifacts associated with the simulation",
),
]
links: Annotated[
list[ExternalLinkCreate] | None,
Field(
None,
description="Optional list of external links associated with the simulation",
),
]

# Miscellaneous
# -----------------

class SimulationCreate(SimulationBase):
"""Schema for creating a new Simulation."""

name: Annotated[str, Field(..., description="Name of the simulation")]
case_name: Annotated[
str, Field(..., description="Case name associated with the simulation")
]
compset: Annotated[
str, Field(..., description="Component set used in the simulation")
]
compset_alias: Annotated[str, Field(..., description="Alias for the component set")]
grid_name: Annotated[
str, Field(..., description="Grid name used in the simulation")
]
grid_resolution: Annotated[
str, Field(..., description="Grid resolution used in the simulation")
]
simulation_type: Annotated[str, Field(..., description="Type of the simulation")]
status: Annotated[
SimulationStatus, Field(..., description="Current status of the simulation")
]
initialization_type: Annotated[
str, Field(..., description="Initialization type for the simulation")
]
machine_id: Annotated[
UUID, Field(..., description="ID of the machine used for the simulation")
]
simulation_start_date: Annotated[
datetime, Field(..., description="Start date of the simulation")
]

# Optional/nullable fields remain as in base, but override to provide default_factory
# for creation
extra: Annotated[
dict,
Field(
Expand All @@ -261,6 +288,26 @@ class SimulationCreate(CamelInBaseModel):
description="Optional list of external links associated with the simulation",
),
]
created_by: Annotated[
UUID | None,
Field(
None,
description="User ID who created the simulation, defined at creation time.",
),
]
last_updated_by: Annotated[
UUID | None,
Field(
None,
description="User ID who last updated the simulation, defined at update time.",
),
]


class SimulationUpdate(SimulationBase):
"""Schema for updating a Simulation (partial fields allowed)."""

pass


class SimulationOut(CamelOutBaseModel):
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/features/simulations/SimulationDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useParams } from 'react-router-dom';

import { useAuth } from '@/auth/hooks/useAuth';
import { SimulationDetailsView } from '@/features/simulations/components/SimulationDetailsView';
import { useSimulation } from '@/features/simulations/hooks/useSimulation';

export const SimulationDetailsPage = () => {
const { user, isAuthenticated } = useAuth();
const { id } = useParams<{ id: string }>();
const { data: simulation, loading, error } = useSimulation(id ?? '');

Expand Down Expand Up @@ -39,5 +41,12 @@ export const SimulationDetailsPage = () => {
);
}

return <SimulationDetailsView simulation={simulation} />;
const canEdit = !!(
isAuthenticated &&
user &&
simulation.createdBy &&
user.id === simulation.createdBy
);

return <SimulationDetailsView simulation={simulation} canEdit={canEdit} />;
};
9 changes: 9 additions & 0 deletions frontend/src/features/simulations/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { api } from '@/api/api';
import type { SimulationCreate, SimulationOut } from '@/types';
import type { SimulationUpdate } from '@/types/simulation';

export const SIMULATIONS_URL = '/simulations';

Expand All @@ -24,3 +25,11 @@ export const getSimulationById = async (id: string): Promise<SimulationOut> => {

return res.data;
};

export const updateSimulation = async (
id: string,
data: SimulationUpdate
): Promise<SimulationOut> => {
const res = await api.patch<SimulationOut>(`${SIMULATIONS_URL}/${id}`, data);
return res.data;
};
Loading
Loading