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/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..2dbcf9de4aa 100644 --- a/editoast/src/views/work_schedules.rs +++ b/editoast/src/views/work_schedules.rs @@ -13,19 +13,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" => project, + }, } editoast_common::schemas! { @@ -56,7 +61,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 +180,98 @@ async fn create( })) } +#[derive(Serialize, Deserialize, ToSchema)] +struct WorkScheduleProjectForm { + work_schedule_group_id: i64, + 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 = "/project", + tag = "work_schedules", + request_body = ProjectWorkScheduleForm, + responses( + (status = 201, body = String, description = ""), + ) +)] +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() + .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); + 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 +359,114 @@ pub mod test { "editoast:work_schedule:NameAlreadyUsed" ); } + + #[rstest] + #[case::one_work_schedule( + vec![ + vec![ + TrackRange::new("a", 0.0, 100.0), + TrackRange::new("b", 0.0, 50.0), + ] + ], + vec![ + vec![(0, 150000)], + ] + )] + #[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’s have bunch of track ranges that represent a path + 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); + } }