From f65d82c24cfe5aa91cc3fc45ced03f33bddb0963 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 8 Nov 2024 11:13:37 +0100 Subject: [PATCH] editoast: use DateTime with timezone in work schedule API The timezone was stored in the database, but was stripped at the HTTP API boundary. This is a breaking change: the timezone is required when submitting a work schedule, and is always returned when projecting a work schedule. Signed-off-by: Simon Ser --- editoast/src/models/fixtures.rs | 2 +- editoast/src/models/work_schedules.rs | 9 ++++--- editoast/src/views/timetable/stdcm.rs | 26 +++++++++---------- editoast/src/views/work_schedules.rs | 36 ++++++++++++++------------- tests/tests/test_stdcm.py | 10 ++++---- 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/editoast/src/models/fixtures.rs b/editoast/src/models/fixtures.rs index fb389816e92..b1cc8cc081e 100644 --- a/editoast/src/models/fixtures.rs +++ b/editoast/src/models/fixtures.rs @@ -354,7 +354,7 @@ pub async fn create_small_infra(conn: &mut DbConnection) -> Infra { pub async fn create_work_schedule_group(conn: &mut DbConnection) -> WorkScheduleGroup { WorkScheduleGroup::changeset() .name("Empty work schedule group".to_string()) - .creation_date(Utc::now().naive_utc()) + .creation_date(Utc::now()) .create(conn) .await .expect("Failed to create empty work schedule group") diff --git a/editoast/src/models/work_schedules.rs b/editoast/src/models/work_schedules.rs index 894912dd037..87beb1064e6 100644 --- a/editoast/src/models/work_schedules.rs +++ b/editoast/src/models/work_schedules.rs @@ -1,4 +1,5 @@ -use chrono::NaiveDateTime; +use chrono::DateTime; +use chrono::Utc; use editoast_derive::Model; use editoast_schemas::infra::TrackRange; use strum::FromRepr; @@ -12,7 +13,7 @@ use utoipa::ToSchema; #[model(gen(ops = crd, batch_ops = c, list))] pub struct WorkScheduleGroup { pub id: i64, - pub creation_date: NaiveDateTime, + pub creation_date: DateTime, pub name: String, } @@ -29,8 +30,8 @@ pub enum WorkScheduleType { #[model(gen(batch_ops = c, list))] pub struct WorkSchedule { pub id: i64, - pub start_date_time: NaiveDateTime, - pub end_date_time: NaiveDateTime, + pub start_date_time: DateTime, + pub end_date_time: DateTime, #[model(json)] pub track_ranges: Vec, pub obj_id: String, diff --git a/editoast/src/views/timetable/stdcm.rs b/editoast/src/views/timetable/stdcm.rs index f92a64d75b8..ff1bea6b7d7 100644 --- a/editoast/src/views/timetable/stdcm.rs +++ b/editoast/src/views/timetable/stdcm.rs @@ -3,8 +3,7 @@ use axum::extract::Path; use axum::extract::Query; use axum::extract::State; use axum::Extension; -use chrono::Utc; -use chrono::{DateTime, Duration, NaiveDateTime, TimeZone}; +use chrono::{DateTime, Duration, Utc}; use editoast_authz::BuiltinRole; use editoast_derive::EditoastError; use editoast_models::DbConnection; @@ -786,8 +785,8 @@ async fn build_temporary_speed_limits( Ok(applicable_speed_limits) } -fn elapsed_since_time_ms(time: &NaiveDateTime, zero: &DateTime) -> u64 { - max(0, (Utc.from_utc_datetime(time) - zero).num_milliseconds()) as u64 +fn elapsed_since_time_ms(time: &DateTime, zero: &DateTime) -> u64 { + max(0, (*time - zero).num_milliseconds()) as u64 } /// Create steps from track_map and waypoints @@ -1200,17 +1199,17 @@ mod tests { #[rstest] // A day before the 'start_time' -> FILTERED OUT - #[case("2024-03-13 06:00:00", "2024-03-13 12:00:00", true)] + #[case("2024-03-13 06:00:00Z", "2024-03-13 12:00:00Z", true)] // Finishing just after the 'start_time' -> KEPT - #[case("2024-03-14 06:00:00", "2024-03-14 08:01:00", false)] + #[case("2024-03-14 06:00:00Z", "2024-03-14 08:01:00Z", false)] // Starting after the 'latest_simulation_end' -> FILTERED OUT - #[case("2024-03-14 10:01:00", "2024-03-14 12:00:00", true)] + #[case("2024-03-14 10:01:00Z", "2024-03-14 12:00:00Z", true)] // Starting before the 'latest_simulation_end' -> KEPT - #[case("2024-03-14 09:59:00", "2024-03-14 12:00:00", false)] + #[case("2024-03-14 09:59:00Z", "2024-03-14 12:00:00Z", false)] // Starting before the 'start_time' and finishing after 'latest_simulation_end' -> KEPT - #[case("2024-03-14 06:00:00", "2024-03-14 12:00:00", false)] + #[case("2024-03-14 06:00:00Z", "2024-03-14 12:00:00Z", false)] // Starting after the 'start_time' and finishing before 'latest_simulation_end' -> KEPT - #[case("2024-03-14 08:30:00", "2024-03-14 09:30:00", false)] + #[case("2024-03-14 08:30:00Z", "2024-03-14 09:30:00Z", false)] fn filter_stdcm_work_schedules_with_window( #[case] ws_start_time: &str, #[case] ws_end_time: &str, @@ -1219,9 +1218,10 @@ mod tests { // GIVEN let work_schedules = [WorkSchedule { id: rand::random::(), - start_date_time: NaiveDateTime::parse_from_str(ws_start_time, "%Y-%m-%d %H:%M:%S") - .unwrap(), - end_date_time: NaiveDateTime::parse_from_str(ws_end_time, "%Y-%m-%d %H:%M:%S").unwrap(), + start_date_time: DateTime::parse_from_rfc3339(ws_start_time) + .unwrap() + .to_utc(), + end_date_time: DateTime::parse_from_rfc3339(ws_end_time).unwrap().to_utc(), ..Default::default() }]; let start_time = DateTime::parse_from_rfc3339("2024-03-14T08:00:00Z") diff --git a/editoast/src/views/work_schedules.rs b/editoast/src/views/work_schedules.rs index 1aaf0c23820..0f7f116daf2 100644 --- a/editoast/src/views/work_schedules.rs +++ b/editoast/src/views/work_schedules.rs @@ -1,7 +1,7 @@ use axum::extract::Json; use axum::extract::State; use axum::Extension; -use chrono::NaiveDateTime; +use chrono::DateTime; use chrono::Utc; use derivative::Derivative; use editoast_authz::BuiltinRole; @@ -64,8 +64,8 @@ pub fn map_diesel_error(e: InternalError, name: impl AsRef) -> InternalErro #[derive(Serialize, Derivative, ToSchema)] struct WorkScheduleItemForm { - pub start_date_time: NaiveDateTime, - pub end_date_time: NaiveDateTime, + pub start_date_time: DateTime, + pub end_date_time: DateTime, pub track_ranges: Vec, pub obj_id: String, #[schema(inline)] @@ -80,8 +80,8 @@ impl<'de> Deserialize<'de> for WorkScheduleItemForm { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct Internal { - start_date_time: NaiveDateTime, - end_date_time: NaiveDateTime, + start_date_time: DateTime, + end_date_time: DateTime, track_ranges: Vec, obj_id: String, work_schedule_type: WorkScheduleType, @@ -163,7 +163,7 @@ async fn create( // Create the work_schedule_group let work_schedule_group = WorkScheduleGroup::changeset() .name(work_schedule_group_name.clone()) - .creation_date(Utc::now().naive_utc()) + .creation_date(Utc::now()) .create(conn) .await; let work_schedule_group = @@ -197,9 +197,9 @@ struct WorkScheduleProjection { /// 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, + pub start_date_time: DateTime, /// The date and time when the work schedule ends. - pub end_date_time: NaiveDateTime, + pub end_date_time: DateTime, /// 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 @@ -297,8 +297,8 @@ pub mod tests { let request = app.post("/work_schedules").json(&json!({ "work_schedule_group_name": "work schedule group name", "work_schedules": [{ - "start_date_time": "2024-01-01T08:00:00", - "end_date_time": "2024-01-01T09:00:00", + "start_date_time": "2024-01-01T08:00:00Z", + "end_date_time": "2024-01-01T09:00:00Z", "track_ranges": [], "obj_id": "work_schedule_obj_id", "work_schedule_type": "CATENARY" @@ -328,8 +328,8 @@ pub mod tests { let request = app.post("/work_schedules").json(&json!({ "work_schedule_group_name": "work schedule group name", "work_schedules": [{ - "start_date_time": "2024-01-01T08:00:00", - "end_date_time": "2024-01-01T07:00:00", + "start_date_time": "2024-01-01T08:00:00Z", + "end_date_time": "2024-01-01T07:00:00Z", "track_ranges": [], "obj_id": "work_schedule_obj_id", "work_schedule_type": "CATENARY" @@ -348,7 +348,7 @@ pub mod tests { WorkScheduleGroup::changeset() .name("duplicated work schedule group name".to_string()) - .creation_date(Utc::now().naive_utc()) + .creation_date(Utc::now()) .create(&mut pool.get_ok()) .await .expect("Failed to create work schedule group"); @@ -356,8 +356,8 @@ pub mod tests { let request = app.post("/work_schedules").json(&json!({ "work_schedule_group_name": "duplicated work schedule group name", "work_schedules": [{ - "start_date_time": "2024-01-01T08:00:00", - "end_date_time": "2024-01-01T09:00:00", + "start_date_time": "2024-01-01T08:00:00Z", + "end_date_time": "2024-01-01T09:00:00Z", "track_ranges": [], "obj_id": "work_schedule_obj_id", "work_schedule_type": "CATENARY" @@ -437,12 +437,14 @@ pub mod tests { NaiveDate::from_ymd_opt(2024, 1, (index + 1).try_into().unwrap()) .unwrap() .and_hms_opt(0, 0, 0) - .unwrap(); + .unwrap() + .and_utc(); let end_date_time = NaiveDate::from_ymd_opt(2024, 1, (index + 2).try_into().unwrap()) .unwrap() .and_hms_opt(0, 0, 0) - .unwrap(); + .unwrap() + .and_utc(); WorkSchedule::changeset() .start_date_time(start_date_time) .end_date_time(end_date_time) diff --git a/tests/tests/test_stdcm.py b/tests/tests/test_stdcm.py index 12f5815757e..222ec6c70c4 100644 --- a/tests/tests/test_stdcm.py +++ b/tests/tests/test_stdcm.py @@ -107,7 +107,7 @@ def test_between_trains(small_scenario: Scenario, fast_rolling_stock: int): def test_work_schedules(small_scenario: Scenario, fast_rolling_stock: int): requests.post(EDITOAST_URL + f"infra/{small_scenario.infra}/load") - start_time = datetime.datetime(2024, 1, 1, 14, 0, 0) + start_time = datetime.datetime(2024, 1, 1, 14, 0, 0, tzinfo=datetime.timezone.utc) end_time = start_time + datetime.timedelta(days=4) # TODO: we cannot delete work schedules for now, so let's give a unique name # to avoid collisions @@ -217,10 +217,10 @@ def test_max_running_time(small_scenario: Scenario, fast_rolling_stock: int): [ ] < departure time window """ requests.post(EDITOAST_URL + f"infra/{small_scenario.infra}/load") - origin_start_time = datetime.datetime(2024, 1, 1, 8, 0, 0) - origin_end_time = datetime.datetime(2025, 1, 1, 8, 0, 0) - destination_start_time = datetime.datetime(2023, 1, 1, 8, 0, 0) - destination_end_time = datetime.datetime(2024, 1, 1, 16, 0, 0) + origin_start_time = datetime.datetime(2024, 1, 1, 8, 0, 0, tzinfo=datetime.timezone.utc) + origin_end_time = datetime.datetime(2025, 1, 1, 8, 0, 0, tzinfo=datetime.timezone.utc) + destination_start_time = datetime.datetime(2023, 1, 1, 8, 0, 0, tzinfo=datetime.timezone.utc) + destination_end_time = datetime.datetime(2024, 1, 1, 16, 0, 0, tzinfo=datetime.timezone.utc) # TODO: we cannot delete work schedules for now, so let's give a unique name # to avoid collisions now = datetime.datetime.now()