From 201a07e33ee25ac32876e3063e085285bf9a53a4 Mon Sep 17 00:00:00 2001 From: Valentin Chanas Date: Fri, 6 Sep 2024 14:11:25 +0200 Subject: [PATCH] editoast: project work_schedules on path endpoint Signed-off-by: Valentin Chanas --- editoast/editoast_authz/src/builtin_role.rs | 5 +- editoast/openapi.yaml | 66 +++++ editoast/src/models/fixtures.rs | 19 +- editoast/src/views/work_schedules.rs | 239 ++++++++++++++++++- front/src/common/api/generatedEditoastApi.ts | 29 +++ front/src/config/openapi-editoast-config.ts | 1 + 6 files changed, 354 insertions(+), 5 deletions(-) diff --git a/editoast/editoast_authz/src/builtin_role.rs b/editoast/editoast_authz/src/builtin_role.rs index dff84fe7f48..c446ec17ee5 100644 --- a/editoast/editoast_authz/src/builtin_role.rs +++ b/editoast/editoast_authz/src/builtin_role.rs @@ -24,6 +24,8 @@ pub enum BuiltinRole { #[strum(serialize = "work_schedule:write")] WorkScheduleWrite, + #[strum(serialize = "work_schedule:read")] + WorkScheduleRead, #[strum(serialize = "map:read")] MapRead, @@ -55,7 +57,8 @@ impl BuiltinRoleSet for BuiltinRole { InfraWrite => vec![InfraRead], RollingStockCollectionRead => vec![], RollingStockCollectionWrite => vec![RollingStockCollectionRead], - WorkScheduleWrite => vec![], + WorkScheduleWrite => vec![WorkScheduleRead], + WorkScheduleRead => vec![], MapRead => vec![], Stdcm => vec![MapRead], TimetableRead => vec![], diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index a8c7fc4b63d..05ad3a8a8f0 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -2748,6 +2748,72 @@ paths: application/json: schema: $ref: '#/components/schemas/WorkScheduleCreateResponse' + /work_schedules/project_path: + post: + tags: + - work_schedules + requestBody: + content: + application/json: + schema: + type: object + required: + - work_schedule_group_id + - path_track_ranges + properties: + path_track_ranges: + type: array + items: + $ref: '#/components/schemas/TrackRange' + work_schedule_group_id: + type: integer + format: int64 + required: true + responses: + '201': + description: Returns a list of work schedules whose track ranges intersect the given path + content: + application/json: + schema: + type: array + items: + type: object + description: Represents the projection of a work schedule on a path. + required: + - type + - start_date_time + - end_date_time + - path_position_ranges + properties: + end_date_time: + type: string + format: date-time + description: The date and time when the work schedule ends. + path_position_ranges: + type: array + items: + type: array + items: + allOf: + - type: integer + format: int64 + minimum: 0 + - type: integer + format: int64 + minimum: 0 + description: |- + a list of intervals `(a, b)` that represent the projections of the work schedule track ranges: + - `a` is the distance from the beginning of the path to the beginning of the track range + - `b` is the distance from the beginning of the path to the end of the track range + start_date_time: + type: string + format: date-time + description: The date and time when the work schedule takes effect. + type: + type: string + enum: + - CATENARY + - TRACK components: schemas: AddOperation: diff --git a/editoast/src/models/fixtures.rs b/editoast/src/models/fixtures.rs index 562fcf440ba..a15b5c45048 100644 --- a/editoast/src/models/fixtures.rs +++ b/editoast/src/models/fixtures.rs @@ -13,13 +13,14 @@ use editoast_schemas::primitives::OSRDObject; use editoast_schemas::rolling_stock::RollingStock; use editoast_schemas::train_schedule::TrainScheduleBase; use postgis_diesel::types::LineString; +use serde_json::Value; use crate::infra_cache::operation::create::apply_create_operation; use crate::models::prelude::*; use crate::models::rolling_stock_livery::RollingStockLiveryModel; use crate::models::timetable::Timetable; use crate::models::train_schedule::TrainSchedule; -use crate::models::work_schedules::WorkScheduleGroup; +use crate::models::work_schedules::{WorkSchedule, WorkScheduleGroup}; use crate::models::Document; use crate::models::Infra; use crate::models::Project; @@ -308,3 +309,19 @@ pub async fn create_work_schedule_group(conn: &mut DbConnection) -> WorkSchedule .await .expect("Failed to create empty work schedule group") } + +pub async fn create_work_schedules_fixture_set( + conn: &mut DbConnection, + work_schedules: Vec>, +) -> (WorkScheduleGroup, Vec) { + let work_schedule_group = create_work_schedule_group(conn).await; + let work_schedules_changesets = work_schedules + .into_iter() + .map(|ws| ws.work_schedule_group_id(work_schedule_group.id)) + .collect::>(); + let work_schedules = WorkSchedule::create_batch(conn, work_schedules_changesets) + .await + .expect("Failed to create work test schedules"); + + (work_schedule_group, work_schedules) +} diff --git a/editoast/src/views/work_schedules.rs b/editoast/src/views/work_schedules.rs index e23612447d5..e8917ca9765 100644 --- a/editoast/src/views/work_schedules.rs +++ b/editoast/src/views/work_schedules.rs @@ -6,6 +6,7 @@ use chrono::Utc; use derivative::Derivative; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; +use editoast_models::DbConnectionPoolV2; use serde::de::Error as SerdeError; use serde::Deserialize; use serde::Serialize; @@ -13,19 +14,24 @@ use std::result::Result as StdResult; use thiserror::Error; use utoipa::ToSchema; +use crate::core::pathfinding::TrackRange as CoreTrackRange; use crate::error::InternalError; use crate::error::Result; use crate::models::prelude::*; use crate::models::work_schedules::WorkSchedule; use crate::models::work_schedules::WorkScheduleGroup; use crate::models::work_schedules::WorkScheduleType; +use crate::views::path::projection::PathProjection; use crate::views::AuthorizationError; use crate::views::AuthorizerExt; use crate::AppState; -use editoast_schemas::infra::TrackRange; +use editoast_schemas::infra::{Direction, TrackRange}; crate::routes! { - "/work_schedules" => create, + "/work_schedules" => { + create, + "/project_path" => project_path, + }, } editoast_common::schemas! { @@ -175,15 +181,111 @@ async fn create( })) } +#[derive(Serialize, Deserialize, ToSchema)] +struct WorkScheduleProjectForm { + work_schedule_group_id: i64, + #[schema(value_type = Vec)] + path_track_ranges: Vec, +} + +/// Represents the projection of a work schedule on a path. +#[derive(Serialize, Deserialize, ToSchema, PartialEq, Debug)] +struct WorkScheduleProjection { + #[serde(rename = "type")] + #[schema(inline)] + /// The type of the work schedule. + pub work_schedule_type: WorkScheduleType, + /// The date and time when the work schedule takes effect. + pub start_date_time: NaiveDateTime, + /// The date and time when the work schedule ends. + pub end_date_time: NaiveDateTime, + /// a list of intervals `(a, b)` that represent the projections of the work schedule track ranges: + /// - `a` is the distance from the beginning of the path to the beginning of the track range + /// - `b` is the distance from the beginning of the path to the end of the track range + pub path_position_ranges: Vec<(u64, u64)>, +} + +#[utoipa::path( + post, path = "", + tag = "work_schedules", + request_body = inline(WorkScheduleProjectForm), + responses( + ( + status = 201, + body = inline(Vec), + description = "Returns a list of work schedules whose track ranges intersect the given path" + ), + ) +)] +async fn project_path( + State(db_pool): State, + Extension(authorizer): AuthorizerExt, + Json(WorkScheduleProjectForm { + work_schedule_group_id, + path_track_ranges, + }): Json, +) -> Result>> { + let authorized = authorizer + .check_roles([BuiltinRole::WorkScheduleRead].into()) + .await + .map_err(AuthorizationError::AuthError)?; + if !authorized { + return Err(AuthorizationError::Unauthorized.into()); + } + + // get all work_schedule of the group + let conn = &mut db_pool.get().await?; + let settings: SelectionSettings = SelectionSettings::new() + .filter(move || WorkSchedule::WORK_SCHEDULE_GROUP_ID.eq(work_schedule_group_id)); + let work_schedules = WorkSchedule::list(conn, settings).await?; + + let projections = work_schedules + .into_iter() + .map(|ws| { + let ws_track_ranges: Vec<_> = ws + .clone() + .track_ranges + .into_iter() + .map(|tr| CoreTrackRange { + track_section: tr.track, + begin: (tr.begin * 1000.0) as u64, + end: (tr.end * 1000.0) as u64, + direction: Direction::StartToStop, + }) + .collect(); + + let path_projection = PathProjection::new(&ws_track_ranges); + // project this work_schedule on the path + (ws, path_projection.get_intersections(&path_track_ranges)) + }) + .filter_map(|(ws, path_position_ranges)| { + if path_position_ranges.is_empty() { + None + } else { + Some(WorkScheduleProjection { + work_schedule_type: ws.work_schedule_type, + start_date_time: ws.start_date_time, + end_date_time: ws.end_date_time, + path_position_ranges, + }) + } + }) + .collect(); + Ok(Json(projections)) +} + #[cfg(test)] pub mod test { use axum::http::StatusCode; + use chrono::NaiveDate; use pretty_assertions::assert_eq; use rstest::rstest; use serde_json::json; use super::*; - use crate::views::test_app::TestAppBuilder; + use crate::{ + models::fixtures::create_work_schedules_fixture_set, views::test_app::TestAppBuilder, + }; #[rstest] async fn work_schedule_create() { @@ -271,4 +373,135 @@ pub mod test { "editoast:work_schedule:NameAlreadyUsed" ); } + + #[rstest] + #[case::one_work_schedule_with_two_track_ranges( + vec![ + vec![ + TrackRange::new("a", 0.0, 100.0), + TrackRange::new("b", 0.0, 50.0), + ] + ], + vec![ + vec![(0, 150000)], + ] + )] + #[case::one_work_schedule_with_two_disjoint_track_ranges( + vec![ + vec![ + TrackRange::new("a", 0.0, 100.0), + TrackRange::new("d", 0.0, 100.0), + ] + ], + vec![ + vec![(0, 100000), (300000, 400000)], + ] + )] + #[case::one_work_schedule_but_no_intersection( + vec![ + vec![ + TrackRange::new("d", 100.0, 150.0), + ] + ], + vec![] + )] + #[case::two_work_schedules( + vec![ + vec![ + TrackRange::new("a", 0.0, 100.0), + TrackRange::new("c", 50.0, 100.0), + ], + vec![TrackRange::new("d", 50.0, 100.0)], + ], + vec![ + vec![(0, 100000), (250000, 300000)], + vec![(350000, 400000)] + ], + )] + async fn work_schedule_project_path_on_ws_group( + #[case] work_schedule_track_ranges: Vec>, + #[case] expected_path_position_ranges: Vec>, + ) { + // GIVEN + let app = TestAppBuilder::default_app(); + let pool = app.db_pool(); + let conn = &mut pool.get_ok(); + + // create work schedules + let working_schedules_changeset = work_schedule_track_ranges + .into_iter() + .enumerate() + .map(|(index, track_ranges)| { + let start_date_time = + NaiveDate::from_ymd_opt(2024, 1, (index + 1).try_into().unwrap()) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + let end_date_time = + NaiveDate::from_ymd_opt(2024, 1, (index + 2).try_into().unwrap()) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + WorkSchedule::changeset() + .start_date_time(start_date_time) + .end_date_time(end_date_time) + .track_ranges(track_ranges) + .obj_id(format!("work_schedule_{}", index)) + .work_schedule_type(WorkScheduleType::Track) + }) + .collect(); + + let (work_schedule_group, work_schedules) = + create_work_schedules_fixture_set(conn, working_schedules_changeset).await; + + let request = app.post("/work_schedules/project_path").json(&json!({ + "work_schedule_group_id": work_schedule_group.id, + "path_track_ranges": [ + { + "track_section": "a", + "begin": 0, + "end": 100000, + "direction": "START_TO_STOP" + }, + { + "track_section": "b", + "begin": 0, + "end": 100000, + "direction": "START_TO_STOP" + }, + { + "track_section": "c", + "begin": 0, + "end": 100000, + "direction": "START_TO_STOP" + }, + { + "track_section": "d", + "begin": 0, + "end": 100000, + "direction": "START_TO_STOP" + } + ] + })); + + // WHEN + let work_schedule_project_response = app + .fetch(request) + .assert_status(StatusCode::OK) + .json_into::>(); + + // THEN + let expected: Vec = expected_path_position_ranges + .into_iter() + .enumerate() + .map(|(index, position_ranges)| WorkScheduleProjection { + work_schedule_type: work_schedules[index].work_schedule_type, + start_date_time: work_schedules[index].start_date_time, + end_date_time: work_schedules[index].end_date_time, + path_position_ranges: position_ranges, + }) + .collect(); + + assert_eq!(work_schedule_project_response, expected); + } } diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index 5304f714c09..c8755940c19 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -811,6 +811,17 @@ const injectedRtkApi = api }), invalidatesTags: ['work_schedules'], }), + postWorkSchedulesProjectPath: build.query< + PostWorkSchedulesProjectPathApiResponse, + PostWorkSchedulesProjectPathApiArg + >({ + query: (queryArg) => ({ + url: `/work_schedules/project_path`, + method: 'POST', + body: queryArg.body, + }), + providesTags: ['work_schedules'], + }), }), overrideExisting: false, }); @@ -1479,6 +1490,24 @@ export type PostWorkSchedulesApiResponse = export type PostWorkSchedulesApiArg = { workScheduleCreateForm: WorkScheduleCreateForm; }; +export type PostWorkSchedulesProjectPathApiResponse = + /** status 201 Returns a list of work schedules whose track ranges intersect the given path */ { + /** The date and time when the work schedule ends. */ + end_date_time: string; + /** a list of intervals `(a, b)` that represent the projections of the work schedule track ranges: + - `a` is the distance from the beginning of the path to the beginning of the track range + - `b` is the distance from the beginning of the path to the end of the track range */ + path_position_ranges: (number & number)[][]; + /** The date and time when the work schedule takes effect. */ + start_date_time: string; + type: 'CATENARY' | 'TRACK'; + }[]; +export type PostWorkSchedulesProjectPathApiArg = { + body: { + path_track_ranges: TrackRange[]; + work_schedule_group_id: number; + }; +}; export type NewDocumentResponse = { document_key: number; }; diff --git a/front/src/config/openapi-editoast-config.ts b/front/src/config/openapi-editoast-config.ts index 87e8f7805bf..8cf109f257e 100644 --- a/front/src/config/openapi-editoast-config.ts +++ b/front/src/config/openapi-editoast-config.ts @@ -14,6 +14,7 @@ const config: ConfigFile = { 'postTrainSchedule', 'postTrainScheduleSimulationSummary', 'postTrainScheduleProjectPath', + 'postWorkSchedulesProjectPath', ], type: 'query', },