From 7f59ae8999da8f1de96fdc6985e55c9ce7cf7ea5 Mon Sep 17 00:00:00 2001 From: Youness Chrifi Alaoui Date: Fri, 9 Feb 2024 00:22:46 +0100 Subject: [PATCH] editoast: crud scenario --- editoast/src/modelsv2/scenario.rs | 135 +++++++- editoast/src/modelsv2/timetable.rs | 61 ++-- editoast/src/modelsv2/trainschedule.rs | 1 - editoast/src/schema/v2/trainschedulev2.rs | 6 +- editoast/src/views/mod.rs | 1 + editoast/src/views/scenario.rs | 4 +- editoast/src/views/timetable.rs | 2 +- editoast/src/views/train_schedule/mod.rs | 2 +- editoast/src/views/v2/mod.rs | 8 + editoast/src/views/v2/scenariov2.rs | 394 ++++++++++------------ editoast/src/views/v2/timetablev2.rs | 118 +++---- editoast/src/views/v2/trainschedulev2.rs | 54 ++- 12 files changed, 460 insertions(+), 326 deletions(-) diff --git a/editoast/src/modelsv2/scenario.rs b/editoast/src/modelsv2/scenario.rs index fc1952757e5..5f12f20cdbe 100644 --- a/editoast/src/modelsv2/scenario.rs +++ b/editoast/src/modelsv2/scenario.rs @@ -1,13 +1,24 @@ -use actix_web::web::Data; +use crate::error::Result; +use crate::models::List; +use crate::models::Ordering; +use crate::modelsv2::Model; +use crate::modelsv2::Row; +use crate::views::pagination::Paginate; +use crate::views::pagination::PaginatedResponse; +use crate::views::v2::scenariov2::ScenarioV2WithCountTrains; +use async_trait::async_trait; use chrono::NaiveDateTime; -use diesel::sql_types::{BigInt, Text}; +use diesel::sql_query; +use diesel::sql_types::{Array, BigInt, Text}; +use diesel::{ExpressionMethods, QueryDsl}; +use diesel_async::{AsyncPgConnection as PgConnection, RunQueryDsl}; use editoast_derive::ModelV2; use serde_derive::{Deserialize, Serialize}; use utoipa::ToSchema; -#[derive(Debug, Default, Clone, ModelV2, Deserialize, Serialize)] -#[model(changeset(derive(Deserialize)))] +#[derive(Debug, Clone, ModelV2, Deserialize, Serialize)] #[model(table = crate::tables::scenariov2)] +#[model(changeset(public))] pub struct ScenarioV2 { pub id: i64, pub infra_id: i64, @@ -19,3 +30,119 @@ pub struct ScenarioV2 { pub timetable_id: i64, pub study_id: i64, } + +#[derive(Debug, Clone, Deserialize, Serialize, QueryableByName, ToSchema)] +pub struct ScenarioV2WithDetails { + #[serde(flatten)] + #[diesel(embed)] + pub scenario: ScenarioV2, + #[diesel(sql_type = Text)] + pub infra_name: String, + // #[diesel(sql_type = Nullable)] + // pub electrical_profile_set_name: Option, + #[diesel(sql_type = Array)] + pub train_schedule_ids: Vec, + #[diesel(sql_type = BigInt)] + pub trains_count: i64, +} + +impl ScenarioV2 { + pub async fn with_details_conn(self, conn: &mut PgConnection) -> Result { + use crate::tables::infra::dsl as infra_dsl; + use crate::tables::trainschedulev2::dsl::*; + + let infra_name = infra_dsl::infra + .filter(infra_dsl::id.eq(self.infra_id)) + .select(infra_dsl::name) + .first::(conn) + .await?; + + // let electrical_profile_set_name = match self.electrical_profile_set_id.unwrap() { + // Some(electrical_profile_set) => Some( + // elec_dsl::electrical_profile_set + // .filter(elec_dsl::id.eq(electrical_profile_set)) + // .select(elec_dsl::name) + // .first::(conn) + // .await?, + // ), + // None => None, + // }; + + let train_schedule_ids = trainschedulev2 + .filter(timetable_id.eq(self.timetable_id)) + .select(id) + .load::(conn) + .await?; + + let trains_count = train_schedule_ids.len() as i64; + + Ok(ScenarioV2WithDetails { + scenario: self, + infra_name, + train_schedule_ids, + trains_count, + }) + } + + pub async fn with_trains_count( + self, + conn: &mut PgConnection, + ) -> Result { + use crate::tables::infra::dsl as infra_dsl; + use crate::tables::trainschedulev2::dsl as schedule_dsl; + let trains_count = schedule_dsl::trainschedulev2 + .filter(schedule_dsl::timetable_id.eq(self.timetable_id)) + .count() + .get_result(conn) + .await?; + let infra_name = infra_dsl::infra + .filter(infra_dsl::id.eq(self.infra_id)) + .select(infra_dsl::name) + .get_result(conn) + .await?; + Ok(ScenarioV2WithCountTrains::new_from_scenario( + self, + trains_count, + infra_name, + )) + } +} + +#[async_trait] +impl List<(i64, Ordering)> for ScenarioV2 { + /// List all scenarios with the number of trains. + /// This functions takes a study_id to filter scenarios. + async fn list_conn( + conn: &mut PgConnection, + page: i64, + page_size: i64, + params: (i64, Ordering), + ) -> Result> { + let study_id = params.0; + let ordering = params.1.to_sql(); + let scenario_rows = sql_query(format!("WITH scenarios_with_train_counts AS ( + SELECT t.*, COUNT(train_schedule.id) as trains_count + FROM scenariov2 as t + LEFT JOIN train_schedule ON t.timetable_id = train_schedule.timetable_id WHERE t.study_id = $1 + GROUP BY t.id ORDER BY {ordering} + ) + SELECT scenarios_with_train_counts.*, infra.name as infra_name + FROM scenarios_with_train_counts + JOIN infra ON infra.id = infra_id")) + .bind::(study_id) + .paginate(page, page_size) + .load_and_count::>(conn).await?; + + let results: Vec = scenario_rows + .results + .into_iter() + .map(Self::from_row) + .collect(); + Ok(PaginatedResponse { + count: scenario_rows.count, + previous: scenario_rows.previous, + next: scenario_rows.next, + results, + }) + } +} diff --git a/editoast/src/modelsv2/timetable.rs b/editoast/src/modelsv2/timetable.rs index 3d087447fc3..1d5f44ed3cf 100644 --- a/editoast/src/modelsv2/timetable.rs +++ b/editoast/src/modelsv2/timetable.rs @@ -1,33 +1,50 @@ -use std::collections::HashMap; - -use crate::diesel::QueryDsl; +use crate::diesel::query_dsl::methods::DistinctDsl; use crate::error::Result; -use crate::models::LightRollingStockModel; -use crate::models::Retrieve; -use crate::models::{ - train_schedule::{ - LightTrainSchedule, MechanicalEnergyConsumedBaseEco, TrainSchedule, TrainScheduleSummary, - }, - SimulationOutput, -}; -use crate::tables::timetable; -use crate::DbPool; -use actix_web::web::Data; -use derivative::Derivative; -use diesel::prelude::*; -use diesel::result::Error as DieselError; -use diesel::ExpressionMethods; +use crate::models::List; +use crate::models::NoParams; +use crate::modelsv2::Model; +use crate::modelsv2::Row; +use crate::tables::timetablev2::dsl; +use crate::views::pagination::Paginate; +use crate::views::pagination::PaginatedResponse; +use async_trait::async_trait; use diesel_async::AsyncPgConnection as PgConnection; -use diesel_async::RunQueryDsl; -use editoast_derive::Model; use editoast_derive::ModelV2; -use futures::future::try_join_all; use serde::{Deserialize, Serialize}; -#[derive(Debug, Default, Clone, Serialize, Deserialize, ModelV2)] +#[derive(Debug, Default, Clone, ModelV2, Serialize, Deserialize)] #[model(table = crate::tables::timetablev2)] #[model(changeset(public))] pub struct TimetableV2 { pub id: i64, pub electrical_profile_set_id: Option, } + +#[async_trait] +impl List for TimetableV2 { + async fn list_conn( + conn: &mut PgConnection, + page: i64, + page_size: i64, + _: NoParams, + ) -> Result> { + let timetable_rows = dsl::timetablev2 + .distinct() + .paginate(page, page_size) + .load_and_count::>(conn) + .await?; + + let results: Vec = timetable_rows + .results + .into_iter() + .map(Self::from_row) + .collect(); + + Ok(PaginatedResponse { + count: timetable_rows.count, + previous: timetable_rows.previous, + next: timetable_rows.next, + results, + }) + } +} diff --git a/editoast/src/modelsv2/trainschedule.rs b/editoast/src/modelsv2/trainschedule.rs index e19fe170042..eb735cc458a 100644 --- a/editoast/src/modelsv2/trainschedule.rs +++ b/editoast/src/modelsv2/trainschedule.rs @@ -3,7 +3,6 @@ use crate::schema::v2::trainschedulev2::{ }; use crate::DieselJson; use chrono::NaiveDateTime; -use diesel_async::RunQueryDsl; use editoast_derive::ModelV2; use serde::{Deserialize, Serialize}; diff --git a/editoast/src/schema/v2/trainschedulev2.rs b/editoast/src/schema/v2/trainschedulev2.rs index 75ed88f44c0..8f2565a62c1 100644 --- a/editoast/src/schema/v2/trainschedulev2.rs +++ b/editoast/src/schema/v2/trainschedulev2.rs @@ -84,7 +84,11 @@ impl<'de> Deserialize<'de> for MarginValue { })?; return Ok(Self::Percentage(float_value)); } - return Err(serde::de::Error::custom("Invalid margin value")); + if f64::from_str(&value).is_ok() { + let float_value = value.parse::().unwrap(); + return Ok(Self::MinPerKm(float_value)); + } + Err(serde::de::Error::custom("Invalid margin value")) } } diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 27e0ab31e84..e1ee908136c 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -49,6 +49,7 @@ fn routes_v2() -> Routes { documents::routes(), sprites::routes(), projects::routes(), + v2::routes(), search::routes(), electrical_profiles::routes(), layers::routes(), diff --git a/editoast/src/views/scenario.rs b/editoast/src/views/scenario.rs index 08a8c99846f..9c0c086337e 100644 --- a/editoast/src/views/scenario.rs +++ b/editoast/src/views/scenario.rs @@ -124,7 +124,7 @@ impl ScenarioResponse { } /// Check if project and study exist given a study ID and a project ID -async fn check_project_study( +pub async fn check_project_study( db_pool: Data, project_id: i64, study_id: i64, @@ -133,7 +133,7 @@ async fn check_project_study( check_project_study_conn(&mut conn, project_id, study_id).await } -async fn check_project_study_conn( +pub async fn check_project_study_conn( conn: &mut PgConnection, project_id: i64, study_id: i64, diff --git a/editoast/src/views/timetable.rs b/editoast/src/views/timetable.rs index 9a91895ebcd..20f06468bd4 100644 --- a/editoast/src/views/timetable.rs +++ b/editoast/src/views/timetable.rs @@ -37,7 +37,7 @@ crate::schemas! { #[derive(Debug, Error, EditoastError)] #[editoast_error(base_id = "timetable")] -enum TimetableError { +pub enum TimetableError { #[error("Timetable '{timetable_id}', could not be found")] #[editoast_error(status = 404)] NotFound { timetable_id: i64 }, diff --git a/editoast/src/views/train_schedule/mod.rs b/editoast/src/views/train_schedule/mod.rs index 107c878665c..e94e5c058bb 100644 --- a/editoast/src/views/train_schedule/mod.rs +++ b/editoast/src/views/train_schedule/mod.rs @@ -95,7 +95,7 @@ pub enum TrainScheduleError { #[derive(IntoParams)] #[allow(unused)] -struct TrainScheduleIdParam { +pub struct TrainScheduleIdParam { /// A train schedule ID id: i64, } diff --git a/editoast/src/views/v2/mod.rs b/editoast/src/views/v2/mod.rs index c36cc1d4b08..e4aab1055f6 100644 --- a/editoast/src/views/v2/mod.rs +++ b/editoast/src/views/v2/mod.rs @@ -1,3 +1,11 @@ pub mod scenariov2; pub mod timetablev2; pub mod trainschedulev2; + +crate::routes! { + + trainschedulev2::routes(), + timetablev2::routes(), + scenariov2::routes() + +} diff --git a/editoast/src/views/v2/scenariov2.rs b/editoast/src/views/v2/scenariov2.rs index 96363ad6fa1..0a56c95abf6 100644 --- a/editoast/src/views/v2/scenariov2.rs +++ b/editoast/src/views/v2/scenariov2.rs @@ -1,37 +1,34 @@ use crate::decl_paginated_response; -use crate::error::{InternalError, Result}; +use crate::error::Result; use crate::models::train_schedule::LightTrainSchedule; -use crate::models::{ - List, Project, Retrieve, ScenarioWithCountTrains, ScenarioWithDetails, Study, Timetable, Update, -}; -use crate::modelsv2::scenario::ScenarioV2; +use crate::models::{List, Project, ScenarioWithCountTrains, Study}; +use crate::modelsv2::scenario::{ScenarioV2, ScenarioV2WithDetails}; +use crate::modelsv2::timetable::TimetableV2Changeset; +use crate::modelsv2::{Changeset, Model, TimetableV2}; use crate::views::pagination::{PaginatedResponse, PaginationQueryParam}; -use crate::views::projects::{ProjectError, ProjectIdParam}; -use crate::views::study::{StudyError, StudyIdParam}; -use crate::{models::Scenario, DbPool}; +use crate::views::projects::ProjectIdParam; +use crate::views::projects::QueryParams; +use crate::views::scenario::{check_project_study, ScenarioIdParam}; +use crate::views::scenario::{check_project_study_conn, ScenarioError}; +use crate::views::study::StudyIdParam; +use crate::DbPool; +use actix_web::web::Query; use actix_web::{delete, HttpResponse}; use actix_web::get; use actix_web::patch; -use actix_web::web::Query; use actix_web::{ post, web::{Data, Json, Path}, }; -use chrono::Utc; use derivative::Derivative; -use diesel_async::scoped_futures::ScopedFutureExt; -use diesel_async::AsyncConnection; -use diesel_async::AsyncPgConnection as PgConnection; -use editoast_derive::EditoastError; +use diesel::sql_types::BigInt; +use diesel::sql_types::Text; use serde::{Deserialize, Serialize}; -use thiserror::Error; -use utoipa::{IntoParams, ToSchema}; - -use super::projects::QueryParams; +use utoipa::ToSchema; crate::routes! { - "/scenarios" => { + "/v2/scenarios" => { create, "/{scenario_id}" => { get, @@ -42,79 +39,59 @@ crate::routes! { } crate::schemas! { - Scenario, - ScenarioCreateForm, - ScenarioPatchForm, - ScenarioWithCountTrains, - ScenarioWithDetails, - PaginatedResponseOfScenarioWithCountTrains, - ScenarioResponse, + ScenarioV2, + ScenarioV2CreateForm, + ScenarioV2PatchForm, + ScenarioV2WithCountTrains, + ScenarioV2WithDetails, + PaginatedResponseOfScenarioV2WithCountTrains, + ScenarioV2Response, LightTrainSchedule, // TODO: remove from here once train schedule is migrated } -#[derive(Debug, Error, EditoastError)] -#[editoast_error(base_id = "scenario")] -#[allow(clippy::enum_variant_names)] -enum ScenarioError { - /// Couldn't found the scenario with the given scenario ID - - #[error("Scenario '{scenario_id}', could not be found")] - #[editoast_error(status = 404)] - NotFound { scenario_id: i64 }, -} - /// This structure is used by the post endpoint to create a scenario #[derive(Serialize, Deserialize, Derivative, ToSchema)] #[derivative(Default)] -struct ScenarioCreateForm { +struct ScenarioV2CreateForm { pub name: String, #[serde(default)] pub description: String, pub infra_id: i64, - pub electrical_profile_set_id: Option, #[serde(default)] - pub tags: Vec, + pub tags: Vec>, } -impl ScenarioCreateForm { - pub fn into_scenario(self, study_id: i64, timetable_id: i64) -> Scenario { - Scenario { - name: Some(self.name), - study_id: Some(study_id), - infra_id: Some(self.infra_id), - electrical_profile_set_id: Some(self.electrical_profile_set_id), - timetable_id: Some(timetable_id), - description: Some(self.description), - tags: Some(self.tags), - creation_date: Some(Utc::now().naive_utc()), - ..Default::default() - } +impl From for Changeset { + fn from(scenario: ScenarioV2CreateForm) -> Self { + ScenarioV2::changeset() + .name(scenario.name) + .description(scenario.description) + .infra_id(scenario.infra_id) + .tags(scenario.tags) } } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] -pub struct ScenarioResponse { +pub struct ScenarioV2Response { #[serde(flatten)] - pub scenario: Scenario, + pub scenario: ScenarioV2, pub infra_name: String, - pub electrical_profile_set_name: Option, - pub train_schedules: Vec, + pub train_schedule_ids: Vec, pub trains_count: i64, pub project: Project, pub study: Study, } -impl ScenarioResponse { +impl ScenarioV2Response { pub fn new( - scenarios_with_details: ScenarioWithDetails, + scenarios_with_details: ScenarioV2WithDetails, project: Project, study: Study, ) -> Self { Self { scenario: scenarios_with_details.scenario, infra_name: scenarios_with_details.infra_name, - electrical_profile_set_name: scenarios_with_details.electrical_profile_set_name, - train_schedules: scenarios_with_details.train_schedules, + train_schedule_ids: scenarios_with_details.train_schedule_ids, trains_count: scenarios_with_details.trains_count, project, study, @@ -122,105 +99,88 @@ impl ScenarioResponse { } } -/// Check if project and study exist given a study ID and a project ID -async fn check_project_study( - db_pool: Data, - project_id: i64, - study_id: i64, -) -> Result<(Project, Study)> { - let mut conn = db_pool.get().await?; - check_project_study_conn(&mut conn, project_id, study_id).await +#[derive(Debug, Clone, Serialize, QueryableByName, ToSchema)] +pub struct ScenarioV2WithCountTrains { + #[serde(flatten)] + #[diesel(embed)] + pub scenario: ScenarioV2, + #[diesel(sql_type = BigInt)] + pub trains_count: i64, + #[diesel(sql_type = Text)] + pub infra_name: String, } -async fn check_project_study_conn( - conn: &mut PgConnection, - project_id: i64, - study_id: i64, -) -> Result<(Project, Study)> { - let project = match Project::retrieve_conn(conn, project_id).await? { - None => return Err(ProjectError::NotFound { project_id }.into()), - Some(project) => project, - }; - let study = match Study::retrieve_conn(conn, study_id).await? { - None => return Err(StudyError::NotFound { study_id }.into()), - Some(study) => study, - }; - - if study.project_id.unwrap() != project_id { - return Err(StudyError::NotFound { study_id }.into()); +impl ScenarioV2WithCountTrains { + pub fn new_from_scenario(scenario: ScenarioV2, trains_count: i64, infra_name: String) -> Self { + Self { + scenario, + trains_count, + infra_name, + } } - Ok((project, study)) } /// Create a scenario #[utoipa::path( - tag = "scenarios", + tag = "scenariosv2", params(ProjectIdParam, StudyIdParam), - request_body = ScenarioCreateForm, + request_body = ScenarioV2CreateForm, responses( - (status = 201, body = ScenarioResponse, description = "The created scenario"), + (status = 201, body = ScenarioV2Response, description = "The created scenario"), ) )] #[post("")] async fn create( db_pool: Data, - data: Json, + data: Json, path: Path<(i64, i64)>, -) -> Result> { +) -> Result> { + use crate::modelsv2::Create; let (project_id, study_id) = path.into_inner(); + let scenario: Changeset = data.into_inner().into(); + + let conn = &mut db_pool.get().await?; - let mut conn = db_pool.get().await?; // Check if the project and the study exist - let (project, study) = check_project_study_conn(&mut conn, project_id, study_id).await?; - let (project, study, scenarios_with_details) = conn - .transaction::<_, InternalError, _>(|conn| { - async { - // Create timetable - let timetable = Timetable { - id: None, - name: Some("timetable".into()), - }; - let timetable = timetablev2.create(db_pool.clone()).await?; - let timetable_id = timetable.id.unwrap(); - - // Create Scenario - let scenario: ScenarioV2 = data.into_inner().into_scenario(study_id, timetable_id); - let scenario = scenario.create().await?; - - // Update study last_modification field - let study = study - .clone() - .update_last_modified_conn(conn) - .await? - .expect("Study should exist"); - - // Update project last_modification field - let project = project - .clone() - .update_last_modified_conn(conn) - .await? - .expect("Project should exist"); - - // Return study with list of scenarios - Ok((project, study, scenario.with_details_conn(conn).await?)) - } - .scope_boxed() - }) + let (project, study) = check_project_study_conn(conn, project_id, study_id).await?; + + // Create timetable + + let timetable = TimetableV2Changeset::create( + TimetableV2::changeset().electrical_profile_set_id(None), + conn, + ) + .await?; + // Create Scenario + let scenario = scenario + .timetable_id(timetable.id) + .study_id(study_id) + .create(conn) .await?; - let scenarios_response = ScenarioResponse::new(scenarios_with_details, project, study); - Ok(Json(scenarios_response)) -} + // Update study last_modification field + let study = study + .clone() + .update_last_modified_conn(conn) + .await? + .expect("Study should exist"); + + // Update project last_modification field + let project = project + .clone() + .update_last_modified_conn(conn) + .await? + .expect("Project should exist"); + + let scenarios_with_details = scenario.with_details_conn(conn).await?; -#[derive(IntoParams)] -#[allow(unused)] -pub struct ScenarioIdParam { - scenario_id: i64, + let scenarios_response = ScenarioV2Response::new(scenarios_with_details, project, study); + Ok(Json(scenarios_response)) } /// Delete a scenario #[utoipa::path( - tag = "scenarios", + tag = "scenariosv2", params(ProjectIdParam, StudyIdParam, ScenarioIdParam), responses( (status = 204, description = "The scenario was deleted successfully"), @@ -229,6 +189,7 @@ pub struct ScenarioIdParam { )] #[delete("")] async fn delete(path: Path<(i64, i64, i64)>, db_pool: Data) -> Result { + use crate::modelsv2::DeleteStatic; let (project_id, study_id, scenario_id) = path.into_inner(); // Check if the project and the study exist @@ -236,10 +197,13 @@ async fn delete(path: Path<(i64, i64, i64)>, db_pool: Data) -> Result, db_pool: Data) -> Result, pub description: Option, - pub tags: Option>, + pub tags: Option>>, } -impl From for Scenario { - fn from(form: ScenarioPatchForm) -> Self { - Scenario { - name: form.name, - description: form.description, - tags: form.tags, +impl From for ::Changeset { + fn from(scenario: ScenarioV2PatchForm) -> Self { + Self { + name: scenario.name, + description: scenario.description, + tags: scenario.tags, ..Default::default() } } @@ -274,99 +238,102 @@ impl From for Scenario { /// Update a scenario #[utoipa::path( - tag = "scenarios", + tag = "scenariosv2", params(ProjectIdParam, StudyIdParam, ScenarioIdParam), - request_body = ScenarioPatchForm, + request_body = ScenarioPatchV2Form, responses( - (status = 204, body = ScenarioResponse, description = "The scenario was updated successfully"), + (status = 204, body = ScenarioV2Response, description = "The scenario was updated successfully"), (status = 404, body = InternalError, description = "The requested scenario was not found"), ) )] #[patch("")] async fn patch( - data: Json, + data: Json, path: Path<(i64, i64, i64)>, db_pool: Data, -) -> Result> { +) -> Result> { + use crate::modelsv2::Update; + let (project_id, study_id, scenario_id) = path.into_inner(); - let mut conn = db_pool.get().await?; - - let (project, study, scenario) = conn - .transaction::<_, InternalError, _>(|conn| { - async { - // Check if project and study exist - let (project, study) = check_project_study_conn(conn, project_id, study_id) - .await - .unwrap(); - // Update the scenario - let scenario: Scenario = data.into_inner().into(); - let scenario = match scenario.update_conn(conn, scenario_id).await? { - Some(scenario) => scenario, - None => return Err(ScenarioError::NotFound { scenario_id }.into()), - }; - // Update study last_modification field - let study = study - .clone() - .update_last_modified_conn(conn) - .await? - .expect("Study should exist"); - // Update project last_modification field - let project = project - .clone() - .update_last_modified_conn(conn) - .await? - .expect("Project should exist"); - Ok((project, study, scenario.with_details_conn(conn).await?)) - } - .scope_boxed() + let conn = &mut db_pool.get().await?; + + // Check if project and study exist + let (project, study) = check_project_study_conn(conn, project_id, study_id) + .await + .unwrap(); + // Update the scenario + let scenario: Changeset = data.into_inner().into(); + let scenario = scenario + .update_or_fail(conn, scenario_id, || ScenarioError::NotFound { + scenario_id, }) .await?; + // Update study last_modification field + let study = study + .clone() + .update_last_modified_conn(conn) + .await? + .expect("Study should exist"); + // Update project last_modification field + let project = project + .clone() + .update_last_modified_conn(conn) + .await? + .expect("Project should exist"); + + let scenario_with_details = scenario.with_details_conn(conn).await?; - let scenarios_response = ScenarioResponse::new(scenario, project, study); + let scenarios_response = ScenarioV2Response::new(scenario_with_details, project, study); Ok(Json(scenarios_response)) } /// Return a specific scenario #[utoipa::path( - tag = "scenarios", + tag = "scenariosv2", params(ProjectIdParam, StudyIdParam, ScenarioIdParam), responses( - (status = 200, body = ScenarioResponse, description = "The requested scenario"), + (status = 200, body = ScenarioV2Response, description = "The requested scenario"), (status = 404, body = InternalError, description = "The requested scenario was not found"), ) )] #[get("")] -async fn get(db_pool: Data, path: Path<(i64, i64, i64)>) -> Result> { +async fn get( + db_pool: Data, + path: Path<(i64, i64, i64)>, +) -> Result> { + use crate::modelsv2::Retrieve; + let (project_id, study_id, scenario_id) = path.into_inner(); let (project, study) = check_project_study(db_pool.clone(), project_id, study_id).await?; - + let conn = &mut db_pool.get().await?; // Return the scenarios - let scenario = match Scenario::retrieve(db_pool.clone(), scenario_id).await? { - Some(scenario) => scenario, - None => return Err(ScenarioError::NotFound { scenario_id }.into()), - }; + let scenario = ScenarioV2::retrieve_or_fail(conn, scenario_id, || ScenarioError::NotFound { + scenario_id, + }) + .await?; // Check if the scenario belongs to the study - if scenario.study_id.unwrap() != study_id { + if scenario.study_id != study_id { return Err(ScenarioError::NotFound { scenario_id }.into()); } - let scenarios_with_details = scenario.with_details(db_pool).await?; - let scenarios_response = ScenarioResponse::new(scenarios_with_details, project, study); + let scenarios_with_details = scenario.with_details_conn(conn).await?; + let scenarios_response = ScenarioV2Response::new(scenarios_with_details, project, study); Ok(Json(scenarios_response)) } decl_paginated_response!( - PaginatedResponseOfScenarioWithCountTrains, - ScenarioWithCountTrains + PaginatedResponseOfScenarioV2WithCountTrains, + ScenarioV2WithCountTrains ); + /// Return a list of scenarios #[utoipa::path( - tag = "scenarios", + tag = "scenariosv2", params(ProjectIdParam, StudyIdParam, PaginationQueryParam, QueryParams), responses( - (status = 200, body = PaginatedResponseOfScenarioWithCountTrains, description = "The list of scenarios"), + (status = 200, body = PaginatedResponseOfScenarioV2WithCountTrains, description = "The list of scenarios"), ) )] #[get("")] @@ -375,18 +342,31 @@ async fn list( pagination_params: Query, path: Path<(i64, i64)>, params: Query, -) -> Result>> { +) -> Result>> { let (page, per_page) = pagination_params .validate(1000)? .warn_page_size(100) .unpack(); let (project_id, study_id) = path.into_inner(); + let _ = check_project_study(db_pool.clone(), project_id, study_id).await?; let ordering = params.ordering.clone(); - let scenarios = - ScenarioWithCountTrains::list(db_pool, page, per_page, (study_id, ordering)).await?; - - Ok(Json(scenarios)) + let scenarios = ScenarioV2::list(db_pool.clone(), page, per_page, (study_id, ordering)).await?; + let results: Vec<_> = scenarios + .results + .into_iter() + .map(|scenario| async { + let conn = &mut db_pool.clone().get().await?; + scenario.with_trains_count(conn).await + }) + .collect(); + let results = futures::future::try_join_all(results).await?; + Ok(Json(PaginatedResponse { + count: scenarios.count, + previous: scenarios.previous, + next: scenarios.next, + results, + })) } #[cfg(test)] @@ -444,9 +424,9 @@ mod test { let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let scenario_response: ScenarioResponse = read_body_json(response).await; - let scenario = TestFixture::::new(scenario_response.scenario, db_pool); - assert_eq!(scenario.model.name.clone().unwrap(), "scenario_test"); + let scenario_response: ScenarioV2Response = read_body_json(response).await; + let scenario = TestFixture::::new(scenario_response.scenario, db_pool); + assert_eq!(scenario.model.name.clone(), "scenario_test"); } #[rstest] @@ -528,7 +508,7 @@ mod test { let response = call_service(&app, req).await; assert_eq!(response.status(), StatusCode::OK); - let scenario: ScenarioWithDetails = read_body_json(response).await; - assert_eq!(scenario.scenario.name.unwrap(), new_name); + let scenario: ScenarioV2WithDetails = read_body_json(response).await; + assert_eq!(scenario.scenario.name, new_name); } } diff --git a/editoast/src/views/v2/timetablev2.rs b/editoast/src/views/v2/timetablev2.rs index 6fc8ac3875d..a99b0789da5 100644 --- a/editoast/src/views/v2/timetablev2.rs +++ b/editoast/src/views/v2/timetablev2.rs @@ -1,26 +1,30 @@ +use crate::decl_paginated_response; use crate::error::Result; +use crate::models::List; +use crate::models::NoParams; use crate::modelsv2::scenario::ScenarioV2; -use crate::modelsv2::timetable::TimetableV2Changeset; use crate::modelsv2::trainschedule::TrainScheduleV2; use crate::modelsv2::{Changeset, DeleteBatch, DeleteStatic, Model, TimetableV2}; +use crate::views::pagination::PaginatedResponse; +use crate::views::pagination::PaginationQueryParam; use crate::views::scenario::ScenarioError; +use crate::views::timetable::TimetableError; use crate::views::train_schedule::TrainScheduleError; use crate::DbPool; -use actix_web::web::{Data, Json, Path}; +use actix_web::web::{Data, Json, Path, Query}; use actix_web::{delete, get, patch, post, HttpResponse}; use derivative::Derivative; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -use editoast_derive::EditoastError; use serde::{Deserialize, Serialize}; -use thiserror::Error; use utoipa::ToSchema; crate::routes! { - "/v2/timetable/{id}" => { + "/v2/timetable" => { post, + list, "/{id}" => { delete, get, @@ -29,37 +33,31 @@ crate::routes! { }, } -// crate::schemas! { -// Conflict, -// ConflictType, -// import::schemas(), -// } - -#[derive(Debug, Error, EditoastError)] -#[editoast_error(base_id = "timetable")] -enum TimetableError { - #[error("Timetable '{timetable_id}', could not be found")] - #[editoast_error(status = 404)] - NotFound { timetable_id: i64 }, - #[error("Infra '{infra_id}' is not loaded")] - #[editoast_error(status = 400)] - InfraNotLoaded { infra_id: i64 }, +crate::schemas! { + PaginatedResponseOfTimetableV2, + TimetableV2WithTrains, + TimetableV2, } /// Return a specific timetable with its associated schedules #[utoipa::path( - tag = "timetable", + tag = "timetablev2", params( ("id" = u64, Path, description = "Timetable id"), ), responses( - (status = 200, description = "Timetable with schedules", body = TimetableWithSchedulesDetails), + (status = 200, description = "Timetable with schedules", body = TimetableV2WithTrains), (status = 404, description = "Timetable not found"), ), )] #[get("")] -async fn get(db_pool: Data, timetable_id: Path) -> Result> { +async fn get( + db_pool: Data, + timetable_id: Path, +) -> Result> { use crate::modelsv2::Retrieve; + use crate::tables::trainschedulev2::dsl; + let timetable_id = timetable_id.into_inner(); // Return the timetable @@ -68,28 +66,39 @@ async fn get(db_pool: Data, timetable_id: Path) -> Result = dsl::trainschedulev2 + .filter(dsl::timetable_id.eq(timetable.id)) + .select(dsl::id) + .get_results::(conn) + .await?; + Ok(Json(TimetableV2WithTrains { + timetable, + train_ids, + })) } -/// Return all timetables with its associated schedules -// #[utoipa::path( -// tag = "timetable", -// params( -// ("id" = u64, Path, description = "Timetable id"), -// ), -// responses( -// (status = 200, description = "Timetable with schedules", body = TimetableWithSchedulesDetails), -// (status = 404, description = "Timetable not found"), -// ), -// )] -// #[get("")] -// async fn list(db_pool: Data) -> Result> { - -// // Return the timetable -// let conn = &mut db_pool.get().await?; -// .await?; -// Ok(Json(timetable)) -// } +decl_paginated_response!(PaginatedResponseOfTimetableV2, TimetableV2); +/// Return all timetables +#[utoipa::path( + tag = "timetablev2", + params(PaginationQueryParam), + responses( + (status = 200, description = "list Timetable", body = PaginatedResponseOfTimetableV2), + ), +)] +#[get("")] +async fn list( + db_pool: Data, + pagination_params: Query, +) -> Result>> { + let (page, per_page) = pagination_params + .validate(1000)? + .warn_page_size(100) + .unpack(); + let conn = &mut db_pool.get().await?; + let timetable = TimetableV2::list_conn(conn, page, per_page, NoParams).await?; + Ok(Json(timetable)) +} /// Creation form for a project #[derive(Serialize, Deserialize, Derivative, ToSchema)] @@ -115,9 +124,9 @@ impl From for Changeset { /// Return a specific timetable with its associated schedules #[utoipa::path( - tag = "timetable", + tag = "timetablev2", responses( - (status = 200, description = "Timetable with schedules", body = TimetableV2), + (status = 200, description = "Timetable with trains", body = TimetableV2WithTrains), (status = 404, description = "Timetable not found"), ), )] @@ -127,22 +136,16 @@ async fn post( data: Json, ) -> Result> { use crate::modelsv2::Create; - use crate::tables::trainschedulev2::dsl; // Return the timetable let timetable: Changeset = data.into_inner().into(); let conn = &mut db_pool.get().await?; let timetable = timetable.create(conn).await?; - let train_ids: Vec = dsl::trainschedulev2 - .filter(dsl::timetable_id.eq(timetable.id)) - .select(dsl::id) - .get_results::(conn) - .await?; Ok(Json(TimetableV2WithTrains { timetable, - train_ids, + train_ids: vec![], })) } @@ -163,9 +166,9 @@ impl From for ::Cha /// Return a specific timetable with its associated schedules #[utoipa::path( - tag = "timetable", + tag = "timetablev2", responses( - (status = 200, description = "Timetable with schedules", body = TimetableV2), + (status = 200, description = "Timetable with schedules", body = TimetableV2WithTrains), (status = 404, description = "Timetable not found"), ), )] @@ -178,7 +181,7 @@ async fn patch( use crate::modelsv2::Update; use crate::tables::trainschedulev2::dsl; - let timetable_id = timetable_id.into_inner().into(); + let timetable_id = timetable_id.into_inner(); let timetable: Changeset = data.into_inner().into(); let conn = &mut db_pool.get().await?; @@ -200,7 +203,7 @@ async fn patch( /// Return a specific timetable with its associated schedules #[utoipa::path( - tag = "timetable", + tag = "timetablev2", responses( (status = 200, description = "Timetable with schedules", body = TimetableV2), (status = 404, description = "Timetable not found"), @@ -221,7 +224,8 @@ async fn delete(db_pool: Data, timetable_id: Path) -> Result { + "/v2/train_schedule" => { post, "/{id}" => { delete, @@ -31,13 +29,6 @@ crate::schemas! { TrainScheduleV2PatchForm, } -#[derive(IntoParams)] -#[allow(unused)] -struct TrainScheduleIdParam { - /// A train schedule ID - id: i64, -} - #[derive(Debug, Default, Clone, Deserialize, ToSchema)] struct TrainScheduleV2CreateForm { train_name: String, @@ -60,6 +51,7 @@ impl From for Changeset { fn from(value: TrainScheduleV2CreateForm) -> Self { TrainScheduleV2::changeset() .train_name(value.train_name) + .labels(value.labels) .rolling_stock_name(value.rolling_stock_name) .timetable_id(value.timetable_id) .start_time(value.start_time) @@ -75,9 +67,10 @@ impl From for Changeset { } } -/// Return a specific timetable with its associated schedules +/// Create a timetable #[utoipa::path( - tag = "train_schedule", + tag = "train_schedulev2", + request_body = TrainScheduleV2CreateForm, responses( (status = 200, description = "The train schedule", body = TrainScheduleV2) ) @@ -100,10 +93,10 @@ async fn post( /// Return a specific timetable with its associated schedules #[utoipa::path( - tag = "train_schedule", + tag = "train_schedulev2", params(TrainScheduleIdParam), responses( - (status = 200, description = "The train schedule", body = TrainSchedule) + (status = 200, description = "The train schedule", body = TrainScheduleV2) ) )] #[get("")] @@ -122,8 +115,9 @@ async fn get(db_pool: Data, train_schedule_id: Path) -> Result for