From e4f583d123cd26386e9add136824d0d3a9342298 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 --- editoast/editoast_authz/src/builtin_role.rs | 5 +- editoast/openapi.yaml | 72 ++++++ editoast/src/models/fixtures.rs | 28 ++- editoast/src/views/work_schedules.rs | 237 ++++++++++++++++++- front/src/common/api/generatedEditoastApi.ts | 30 +++ 5 files changed, 366 insertions(+), 6 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 3bbee07c15f..92450927f9c 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -2748,6 +2748,23 @@ paths: application/json: schema: $ref: '#/components/schemas/WorkScheduleCreateResponse' + /work_schedules/project: + post: + tags: + - work_schedules + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/WorkScheduleProjectForm' + required: true + responses: + '201': + description: Returns a list of work schedule whose track ranges intersect the given path + content: + application/json: + schema: + $ref: '#/components/schemas/WorkScheduleProjectResponse' components: schemas: AddOperation: @@ -9959,6 +9976,61 @@ components: enum: - CATENARY - TRACK + WorkScheduleProjectForm: + 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 + WorkScheduleProjectResponse: + type: object + required: + - projections + properties: + projections: + type: array + items: + $ref: '#/components/schemas/WorkScheduleProjection' + WorkScheduleProjection: + type: object + required: + - type + - start_date_time + - end_date_time + - path_position_ranges + properties: + end_date_time: + type: string + format: date-time + path_position_ranges: + type: array + items: + type: array + items: + allOf: + - type: integer + format: int64 + minimum: 0 + - type: integer + format: int64 + minimum: 0 + start_date_time: + type: string + format: date-time + type: + $ref: '#/components/schemas/WorkScheduleType' + WorkScheduleType: + type: string + enum: + - CATENARY + - TRACK ZoneUpdate: type: object required: diff --git a/editoast/src/models/fixtures.rs b/editoast/src/models/fixtures.rs index 562fcf440ba..2a48b771d2a 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; @@ -29,6 +30,7 @@ use crate::models::Study; use crate::models::Tags; use crate::views::rolling_stock::form::RollingStockForm; use crate::views::train_schedule::TrainScheduleForm; +use crate::views::work_schedules::WorkScheduleItemForm; use crate::ElectricalProfileSet; pub fn project_changeset(name: &str) -> Changeset { @@ -308,3 +310,27 @@ pub async fn create_work_schedule_group(conn: &mut DbConnection) -> WorkSchedule .await .expect("Failed to create empty work schedule group") } + +pub struct WorkSchedulesFixtureSet { + pub work_schedule_group: WorkScheduleGroup, + pub work_schedules: Vec, +} + +pub async fn create_work_schedules_fixture_set( + conn: &mut DbConnection, + work_schedules: Vec, +) -> WorkSchedulesFixtureSet { + let work_schedule_group = create_work_schedule_group(conn).await; + let work_schedules_changesets = work_schedules + .into_iter() + .map(|work_schedule| work_schedule.into_work_schedule_changeset(work_schedule_group.id)) + .collect::>(); + let work_schedules = WorkSchedule::create_batch(conn, work_schedules_changesets) + .await + .expect("Failed to create work test schedules"); + + WorkSchedulesFixtureSet { + work_schedule_group, + work_schedules, + } +} diff --git a/editoast/src/views/work_schedules.rs b/editoast/src/views/work_schedules.rs index e23612447d5..3219a2ad9ca 100644 --- a/editoast/src/views/work_schedules.rs +++ b/editoast/src/views/work_schedules.rs @@ -13,25 +13,34 @@ 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" => project, + }, } editoast_common::schemas! { WorkScheduleCreateForm, WorkScheduleCreateResponse, + WorkScheduleProjectForm, + WorkScheduleProjectResponse, + WorkScheduleProjection, WorkScheduleItemForm, + WorkScheduleType, } #[derive(Debug, Error, EditoastError)] @@ -56,7 +65,7 @@ pub fn map_diesel_error(e: InternalError, name: impl AsRef) -> InternalErro } #[derive(Serialize, Derivative, ToSchema)] -struct WorkScheduleItemForm { +pub struct WorkScheduleItemForm { pub start_date_time: NaiveDateTime, pub end_date_time: NaiveDateTime, pub track_ranges: Vec, @@ -175,15 +184,107 @@ async fn create( })) } +#[derive(Serialize, Deserialize, ToSchema)] +struct WorkScheduleProjectForm { + work_schedule_group_id: i64, + #[schema(value_type = Vec)] + path_track_ranges: Vec, +} + +#[derive(Serialize, Deserialize, ToSchema, PartialEq, Debug)] +struct WorkScheduleProjectResponse { + projections: Vec, +} + +#[derive(Serialize, Deserialize, ToSchema, PartialEq, Debug)] +struct WorkScheduleProjection { + #[serde(rename = "type")] + pub work_schedule_type: WorkScheduleType, + pub start_date_time: NaiveDateTime, + pub end_date_time: NaiveDateTime, + pub path_position_ranges: Vec<(u64, u64)>, +} + +#[utoipa::path( + post, path = "", + tag = "work_schedules", + request_body = WorkScheduleProjectForm, + responses( + ( + status = 201, + body = WorkScheduleProjectResponse, + description = "Returns a list of work schedule whose track ranges intersect the given path" + ), + ) +)] +async fn project( + State(app_state): 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 db_pool = app_state.db_pool_v2.clone(); + 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() + .filter_map(|ws| { + let ws_track_ranges: Vec = ws + .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 + let path_position_ranges = path_projection.get_intersections(&path_track_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(WorkScheduleProjectResponse { 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, WorkSchedulesFixtureSet}, + views::test_app::TestAppBuilder, + }; #[rstest] async fn work_schedule_create() { @@ -271,4 +372,132 @@ 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_form = work_schedule_track_ranges + .into_iter() + .enumerate() + .map(|(index, track_ranges)| WorkScheduleItemForm { + start_date_time: NaiveDate::from_ymd_opt(2024, 1, (index + 1).try_into().unwrap()) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + end_date_time: NaiveDate::from_ymd_opt(2024, 1, (index + 2).try_into().unwrap()) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(), + track_ranges, + obj_id: format!("work_schedule_{}", index), + work_schedule_type: WorkScheduleType::Track, + }) + .collect(); + + let WorkSchedulesFixtureSet { + work_schedule_group, + work_schedules, + } = create_work_schedules_fixture_set(conn, working_schedules_form).await; + + let request = app.post("/work_schedules/project").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 = WorkScheduleProjectResponse { + projections: expected_path_position_ranges + .into_iter() + .enumerate() + .map(|(index, position_ranges)| WorkScheduleProjection { + work_schedule_type: WorkScheduleType::Track, + 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..b6245aa3c6e 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'], }), + postWorkSchedulesProject: build.mutation< + PostWorkSchedulesProjectApiResponse, + PostWorkSchedulesProjectApiArg + >({ + query: (queryArg) => ({ + url: `/work_schedules/project`, + method: 'POST', + body: queryArg.workScheduleProjectForm, + }), + invalidatesTags: ['work_schedules'], + }), }), overrideExisting: false, }); @@ -1479,6 +1490,11 @@ export type PostWorkSchedulesApiResponse = export type PostWorkSchedulesApiArg = { workScheduleCreateForm: WorkScheduleCreateForm; }; +export type PostWorkSchedulesProjectApiResponse = + /** status 201 Returns a list of work schedule whose track ranges intersect the given path */ WorkScheduleProjectResponse; +export type PostWorkSchedulesProjectApiArg = { + workScheduleProjectForm: WorkScheduleProjectForm; +}; export type NewDocumentResponse = { document_key: number; }; @@ -3029,3 +3045,17 @@ export type WorkScheduleCreateForm = { work_schedule_group_name: string; work_schedules: WorkScheduleItemForm[]; }; +export type WorkScheduleType = 'CATENARY' | 'TRACK'; +export type WorkScheduleProjection = { + end_date_time: string; + path_position_ranges: (number & number)[][]; + start_date_time: string; + type: WorkScheduleType; +}; +export type WorkScheduleProjectResponse = { + projections: WorkScheduleProjection[]; +}; +export type WorkScheduleProjectForm = { + path_track_ranges: TrackRange[]; + work_schedule_group_id: number; +};