From f31debfbdcf115eaad4c605e05a9b83e97d93a45 Mon Sep 17 00:00:00 2001 From: Wolfgang Silbermayr <w.silbermayr@opentalk.eu> Date: Fri, 7 Mar 2025 21:47:26 +0100 Subject: [PATCH] feat(training_participation_report): add configuration to api Closes #972 --- Cargo.lock | 27 +-- Cargo.toml | 6 +- api/controller/frontend_api.yaml | 103 +++++++++ .../src/api/v1/events/instances.rs | 46 +++- .../src/api/v1/events/invites.rs | 24 +- .../src/api/v1/events/mod.rs | 157 ++++++++++++- .../events/shared_folder.rs | 24 +- .../src/events/notifications.rs | 12 +- crates/opentalk-db-storage/src/events/mod.rs | 211 +++++++++++++++--- ...ng_participation_report_parameter_sets.sql | 7 + crates/opentalk-db-storage/src/schema.rs | 14 ++ .../src/lib.rs | 16 +- .../src/storage/mod.rs | 6 +- .../src/storage/redis.rs | 8 +- .../src/storage/room_state.rs | 3 +- .../training_participation_report_storage.rs | 8 +- .../src/storage/volatile/memory.rs | 8 +- .../src/storage/volatile/storage.rs | 8 +- deny.toml | 1 - docs/developer/database.md | 8 + 20 files changed, 594 insertions(+), 103 deletions(-) create mode 100644 crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql diff --git a/Cargo.lock b/Cargo.lock index b2e3fa04f..868a1db44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1227,7 +1227,7 @@ dependencies = [ "bitflags 2.9.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "lazy_static", "lazycell", "log", @@ -4371,15 +4371,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -6088,9 +6079,9 @@ dependencies = [ [[package]] name = "opentalk-types-api-v1" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8565d68e6994ab6d0d6b5bdbc37d94c4edff34900215309c88d0fe3e248a5924" +checksum = "a7ffb9ca739d7b748de0e188c7a44f5fed08ecdd2d7063ac383e2c92eb2ba527" dependencies = [ "actix-web", "base64 0.22.1", @@ -6111,9 +6102,9 @@ dependencies = [ [[package]] name = "opentalk-types-common" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc7b6f5082ce4410a8a4b483a33ac523863a974344be8ffeeb339e37624a9fd" +checksum = "13523221c1cf75be9cf89b5149542013472d58eafdadb085f6bdd93def143921" dependencies = [ "actix-http", "actix-web-httpauth", @@ -6365,9 +6356,9 @@ dependencies = [ [[package]] name = "opentalk-types-signaling-training-participation-report" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8658e3a6b4303b0d3f637d152029f3ee206ff1bdfdd411bd680094c15f9b004" +checksum = "94dc459b07a8f2f60c2bd7c8c5713b38b053a9527e3995dee7c7ccd904040c4e" dependencies = [ "opentalk-types-common", "opentalk-types-signaling", @@ -7176,7 +7167,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -7216,7 +7207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.11.0", "proc-macro2", "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 06522e964..f008a7880 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,8 +93,8 @@ opentalk-mail-worker-protocol = "0.32" opentalk-nextcloud-client = { version = "0.2.0", default-features = false, features = [ "rustls-tls-native-roots", ] } -opentalk-types-api-v1 = "0.33" -opentalk-types-common = "0.32" +opentalk-types-api-v1 = "0.34" +opentalk-types-common = "0.32.1" opentalk-types-signaling = "0.32" opentalk-types-signaling-breakout = "0.32" opentalk-types-signaling-chat = "0.32" @@ -110,7 +110,7 @@ opentalk-types-signaling-recording-service = "0.32" opentalk-types-signaling-shared-folder = "0.32" opentalk-types-signaling-subroom-audio = "0.32" opentalk-types-signaling-timer = "0.32" -opentalk-types-signaling-training-participation-report = "0.2" +opentalk-types-signaling-training-participation-report = "0.3" opentalk-types-signaling-whiteboard = "0.32" opentalk-version = "0.1.1" opentelemetry = { version = "0.27", default-features = false, features = [ diff --git a/api/controller/frontend_api.yaml b/api/controller/frontend_api.yaml index ea75ced0b..ac7db52cf 100644 --- a/api/controller/frontend_api.yaml +++ b/api/controller/frontend_api.yaml @@ -3095,6 +3095,13 @@ components: title: $ref: "#/components/schemas/EventTitle" description: Title of the event + training_participation_report: + $ref: "#/components/schemas/TrainingParticipationReportParameterSet" + description: |- + The training participation report parameter set for the event. + + When present, the training participation report will be started + automatically in the meeting. type: $ref: "#/components/schemas/EventType" description: "Must always be `instance`" @@ -3158,6 +3165,13 @@ components: timezone: Europe/Berlin status: ok title: Team Event + training_participation_report: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 type: recurring updated_at: "2024-07-20T14:16:19Z" updated_by: @@ -3315,6 +3329,13 @@ components: streaming_endpoint: "https://ingress.streaming.example.com/" streaming_key: aabbccddeeff title: Team Event + training_participation_report: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 type: single updated_at: "2024-07-20T14:16:19Z" updated_by: @@ -3453,6 +3474,13 @@ components: Title of the event For display purposes + training_participation_report: + $ref: "#/components/schemas/TrainingParticipationReportParameterSet" + description: |- + The training participation report parameter set for the event. + + When present, the training participation report will be started + automatically in the meeting. type: $ref: "#/components/schemas/EventType" description: |- @@ -3520,6 +3548,13 @@ components: streaming_endpoint: "https://ingress.streaming.example.com/" streaming_key: aabbccddeeff title: Team Event + training_participation_report: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 type: single updated_at: "2024-07-20T14:16:19Z" updated_by: @@ -3666,6 +3701,13 @@ components: timezone: Europe/Berlin status: ok title: Team Event + training_participation_report: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 type: recurring updated_at: "2024-07-20T14:16:19Z" updated_by: @@ -4037,6 +4079,13 @@ components: title: $ref: "#/components/schemas/EventTitle" description: Patch the title of th event + training_participation_report: + $ref: "#/components/schemas/TrainingParticipationReportParameterSet" + description: |- + The training participation report parameter set for the event. + + When present, the training participation report will be started + automatically in the meeting. waiting_room: type: boolean description: Patch the presence of a waiting room @@ -4243,6 +4292,13 @@ components: title: $ref: "#/components/schemas/EventTitle" description: Title of the event + training_participation_report: + $ref: "#/components/schemas/TrainingParticipationReportParameterSet" + description: |- + The training participation report parameter set for the event. + + When present, the training participation report will be started + automatically in the meeting. waiting_room: type: boolean description: Should the created event have a waiting room? @@ -4269,6 +4325,13 @@ components: streaming_endpoint: "https://ingress.streaming.example.com/" streaming_key: aabbccddeeff title: Teammeeting + training_participation_report: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 waiting_room: false PostInviteRequestBody: type: object @@ -4983,6 +5046,26 @@ components: TicketToken: type: string description: A ticket token + TimeRange: + type: object + description: A time range within which checkpoints can be randomly created + required: + - after + - within + properties: + after: + type: integer + format: int64 + description: The earliest number of seconds after which the checkpoint can be created. + minimum: 0 + within: + type: integer + format: int64 + description: "The number of seconds within which the checkpoint can be created after the `after` value." + minimum: 0 + example: + after: 1200 + within: 600 TimeZone: type: string description: A time zone @@ -4995,6 +5078,26 @@ components: A UTC DateTime wrapper that implements ToRedisArgs and FromRedisValue. The values are stores as unix timestamps in redis. + TrainingParticipationReportParameterSet: + type: object + description: The parameters for a training participant report checkpoint procedure. + required: + - initial_checkpoint_delay + - checkpoint_interval + properties: + checkpoint_interval: + $ref: "#/components/schemas/TimeRange" + description: The time range definition for the subsequent checkpoints. + initial_checkpoint_delay: + $ref: "#/components/schemas/TimeRange" + description: The time range definition for the initial checkpoint delay. + example: + checkpoint_interval: + after: 300 + within: 400 + initial_checkpoint_delay: + after: 100 + within: 200 TurnServer: type: object description: TURN access credentials for users. diff --git a/crates/opentalk-controller-core/src/api/v1/events/instances.rs b/crates/opentalk-controller-core/src/api/v1/events/instances.rs index 99d355075..43413092e 100644 --- a/crates/opentalk-controller-core/src/api/v1/events/instances.rs +++ b/crates/opentalk-controller-core/src/api/v1/events/instances.rs @@ -42,6 +42,7 @@ use opentalk_types_api_v1::{ use opentalk_types_common::{ events::{invites::EventInviteStatus, EventId}, shared_folders::SharedFolder, + training_participation_report::TrainingParticipationReportParameterSet, }; use rrule::RRuleSet; @@ -151,8 +152,16 @@ async fn get_event_instances_inner( let mut conn = db.get_conn().await?; - let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + invite, + room, + sip_config, + is_favorite, + shared_folder, + tariff, + training_participation_report_parameter_set, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let (invitees, invitees_truncated) = super::get_invitees_for_event(settings, &mut conn, event.id, invitees_max).await?; @@ -198,6 +207,9 @@ async fn get_event_instances_inner( .fetch(settings, &mut conn) .await?; + let training_participation_report = training_participation_report_parameter_set + .map(TrainingParticipationReportParameterSet::from); + drop(conn); let room = EventRoomInfo::from_room(settings, room, sip_config, &tariff); @@ -225,6 +237,7 @@ async fn get_event_instances_inner( invitees_truncated, can_edit, shared_folder.clone(), + training_participation_report.clone(), )?; instances.push(instance); @@ -341,8 +354,16 @@ async fn get_event_instance_inner( ) -> DefaultApiResult<GetEventInstanceResponseBody, CaptureApiError> { let mut conn = db.get_conn().await?; - let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + invite, + room, + sip_config, + is_favorite, + shared_folder, + tariff, + training_participation_report_parameter_set, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; verify_recurrence_date(&event, instance_id.into())?; let (invitees, invitees_truncated) = @@ -376,6 +397,7 @@ async fn get_event_instance_inner( invitees_truncated, can_edit, shared_folder, + training_participation_report_parameter_set.map(Into::into), )?; let event_instance = EventInstance { @@ -490,8 +512,16 @@ async fn patch_event_instance_inner( let mut conn = db.get_conn().await?; - let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + invite, + room, + sip_config, + is_favorite, + shared_folder, + tariff, + training_participation_report_parameter_set, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; if !event.is_recurring.unwrap_or_default() { return Err(ApiError::not_found().into()); @@ -648,6 +678,7 @@ async fn patch_event_instance_inner( invitees_truncated, can_edit, shared_folder, + training_participation_report_parameter_set.map(Into::into), )?; let event_instance = EventInstance { @@ -677,6 +708,7 @@ fn create_event_instance( invitees_truncated: bool, can_edit: bool, shared_folder: Option<SharedFolder>, + training_participation_report: Option<TrainingParticipationReportParameterSet>, ) -> opentalk_database::Result<EventInstance> { let mut status = EventStatus::Ok; @@ -741,6 +773,7 @@ fn create_event_instance( is_favorite, can_edit, shared_folder, + training_participation_report, }) } @@ -849,6 +882,7 @@ mod tests { is_favorite: false, can_edit: false, shared_folder: None, + training_participation_report: None, }; assert_eq_json!( diff --git a/crates/opentalk-controller-core/src/api/v1/events/invites.rs b/crates/opentalk-controller-core/src/api/v1/events/invites.rs index fc36c86a6..b750776fd 100644 --- a/crates/opentalk-controller-core/src/api/v1/events/invites.rs +++ b/crates/opentalk-controller-core/src/api/v1/events/invites.rs @@ -912,8 +912,16 @@ async fn delete_invite_to_event_inner( let mut conn = db.get_conn().await?; // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)? - let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + shared_folder, + _tariff, + _training_participation_report_parameter_set, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?; let created_by = if event.created_by == current_user.id { @@ -1069,8 +1077,16 @@ async fn delete_email_invite_to_event_inner( let mut conn = db.get_conn().await?; - let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + shared_folder, + _tariff, + _training_participation_report_parameter_set, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?; let created_by = if event.created_by == current_user.id { diff --git a/crates/opentalk-controller-core/src/api/v1/events/mod.rs b/crates/opentalk-controller-core/src/api/v1/events/mod.rs index 400a41b09..1254fc429 100644 --- a/crates/opentalk-controller-core/src/api/v1/events/mod.rs +++ b/crates/opentalk-controller-core/src/api/v1/events/mod.rs @@ -35,7 +35,8 @@ use opentalk_database::{Db, DbConnection}; use opentalk_db_storage::{ events::{ email_invites::EventEmailInvite, shared_folders::EventSharedFolder, Event, EventException, - EventExceptionKind, EventInvite, NewEvent, UpdateEvent, + EventExceptionKind, EventInvite, EventTrainingParticipationReportParameterSet, NewEvent, + UpdateEvent, UpdateEventTrainingParticipationReportParameterSet, }, invites::Invite, rooms::{NewRoom, Room, UpdateRoom}, @@ -68,6 +69,7 @@ use opentalk_types_common::{ shared_folders::SharedFolder, streaming::{RoomStreamingTarget, StreamingTarget}, time::{DateTimeTz, RecurrencePattern, TimeZone, Timestamp}, + training_participation_report::TrainingParticipationReportParameterSet, }; use rrule::{Frequency, RRuleSet}; use serde::Deserialize; @@ -337,6 +339,7 @@ async fn new_event_inner( streaming_targets, has_shared_folder: _, show_meeting_details, + training_participation_report } if recurrence_pattern.is_empty() => { create_time_independent_event( settings, @@ -351,6 +354,7 @@ async fn new_event_inner( streaming_targets, show_meeting_details, query, + training_participation_report ) .await? } @@ -369,6 +373,7 @@ async fn new_event_inner( streaming_targets, has_shared_folder: _, show_meeting_details, + training_participation_report } => { create_time_dependent_event( settings, @@ -387,6 +392,7 @@ async fn new_event_inner( streaming_targets, show_meeting_details, query, + training_participation_report ) .await? } @@ -461,6 +467,34 @@ async fn store_event_streaming_targets( Ok(room_streaming_targets) } +async fn store_training_participation_report( + conn: &mut DbConnection, + event_id: EventId, + TrainingParticipationReportParameterSet { + initial_checkpoint_delay, + checkpoint_interval, + }: TrainingParticipationReportParameterSet, +) -> Result<Option<TrainingParticipationReportParameterSet>, CaptureApiError> { + let initial_checkpoint_delay_after = + i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX); + let initial_checkpoint_delay_within = + i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX); + let checkpoint_interval_after = i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX); + let checkpoint_interval_within = i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX); + + let inserted = EventTrainingParticipationReportParameterSet { + event_id, + initial_checkpoint_delay_after, + initial_checkpoint_delay_within, + checkpoint_interval_after, + checkpoint_interval_within, + } + .try_insert(conn) + .await?; + + Ok(inserted.map(Into::into)) +} + struct MailResource { pub current_user: User, pub event: Event, @@ -483,6 +517,7 @@ async fn create_time_independent_event( streaming_targets: Vec<StreamingTarget>, show_meeting_details: bool, query: EventOptionsQuery, + training_participation_report: Option<TrainingParticipationReportParameterSet>, ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> { let room = NewRoom { created_by: current_user.id, @@ -521,6 +556,12 @@ async fn create_time_independent_event( let streaming_targets = store_event_streaming_targets(conn, event.id, streaming_targets).await?; + let training_participation_report = if let Some(parameters) = training_participation_report { + store_training_participation_report(conn, event.id, parameters).await? + } else { + None + }; + let tariff = Tariff::get_by_user_id(conn, ¤t_user.id).await?; let suppress_email_notification = is_adhoc || query.suppress_email_notification; @@ -557,6 +598,7 @@ async fn create_time_independent_event( shared_folder: None, streaming_targets, show_meeting_details, + training_participation_report, }, mail_resource, )) @@ -581,6 +623,7 @@ async fn create_time_dependent_event( streaming_targets: Vec<StreamingTarget>, show_meeting_details: bool, query: EventOptionsQuery, + training_participation_report: Option<TrainingParticipationReportParameterSet>, ) -> Result<(EventResource, Option<MailResource>), CaptureApiError> { let recurrence_pattern = recurrence_pattern.to_multiline_string(); @@ -667,6 +710,7 @@ async fn create_time_dependent_event( shared_folder: None, streaming_targets, show_meeting_details, + training_participation_report, }, mail_resource, )) @@ -774,7 +818,7 @@ async fn get_events_inner( ) .await?; - for (event, _, _, _, exceptions, _, _, _) in &events { + for (event, _, _, _, exceptions, _, _, _, _) in &events { users.add(event); users.add(exceptions); } @@ -812,7 +856,17 @@ async fn get_events_inner( let mut ret_cursor_data = None; for ( - (event, invite, room, sip_config, exceptions, is_favorite, shared_folder, tariff), + ( + event, + invite, + room, + sip_config, + exceptions, + is_favorite, + shared_folder, + tariff, + training_participation_report, + ), (mut invites_with_user, mut email_invites), ) in events.into_iter().zip(invites_grouped_by_event) { @@ -887,6 +941,7 @@ async fn get_events_inner( shared_folder, streaming_targets: Vec::new(), show_meeting_details: event.show_meeting_details, + training_participation_report, })); for exception in exceptions { @@ -997,8 +1052,16 @@ async fn get_event_inner( ) -> DefaultApiResult<EventResource, CaptureApiError> { let mut conn = db.get_conn().await?; - let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + invite, + room, + sip_config, + is_favorite, + shared_folder, + tariff, + training_participation_report, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let room_streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?; let (invitees, invitees_truncated) = get_invitees_for_event(settings, &mut conn, event_id, query.invitees_max).await?; @@ -1050,6 +1113,7 @@ async fn get_event_inner( shared_folder, streaming_targets: room_streaming_targets, show_meeting_details: event.show_meeting_details, + training_participation_report: training_participation_report.map(Into::into), }; let event_resource = EventResource { @@ -1157,8 +1221,16 @@ async fn patch_event_inner( let mut conn = db.get_conn().await?; - let (event, invite, room, sip_config, is_favorite, shared_folder, tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + invite, + room, + sip_config, + is_favorite, + shared_folder, + tariff, + training_participation_report, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let room = if patch.password.is_some() || patch.waiting_room.is_some() { // Update the event's room if at least one of the fields is set @@ -1197,6 +1269,19 @@ async fn patch_event_inner( None => {} } + let training_participation_report = match &patch.training_participation_report { + Some(Some(parameter_set)) => Some( + UpdateEventTrainingParticipationReportParameterSet::from(parameter_set.clone()) + .apply(&mut conn, event.id) + .await?, + ), + Some(None) => { + EventTrainingParticipationReportParameterSet::delete_by_id(&mut conn, event.id).await?; + None + } + None => training_participation_report, + }; + // Special case: if the patch only modifies the password do not update the event let event = if patch.only_modifies_room() { event @@ -1296,6 +1381,7 @@ async fn patch_event_inner( shared_folder: shared_folder.clone(), streaming_targets: streaming_targets.clone(), show_meeting_details: event.show_meeting_details, + training_participation_report: training_participation_report.map(Into::into), }; if send_email_notification { @@ -1602,8 +1688,16 @@ async fn delete_event_inner( let mut conn = db.get_conn().await?; // TODO(w.rabl) Further DB access optimization (replacing call to get_with_invite_and_room)? - let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + shared_folder, + _tariff, + _training_participation_report, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; let streaming_targets = get_room_streaming_targets(&mut conn, room.id).await?; @@ -1981,7 +2075,10 @@ mod tests { use std::time::SystemTime; use opentalk_test_util::assert_eq_json; - use opentalk_types_common::{events::invites::InviteRole, rooms::RoomId, users::UserId}; + use opentalk_types_common::{ + events::invites::InviteRole, rooms::RoomId, training_participation_report::TimeRange, + users::UserId, + }; use super::*; @@ -2058,6 +2155,16 @@ mod tests { shared_folder: None, streaming_targets: Vec::new(), show_meeting_details: true, + training_participation_report: Some(TrainingParticipationReportParameterSet { + initial_checkpoint_delay: TimeRange { + after: 100, + within: 200, + }, + checkpoint_interval: TimeRange { + after: 300, + within: 400, + }, + }), }; assert_eq_json!( @@ -2124,6 +2231,16 @@ mod tests { "can_edit": true, "is_adhoc": false, "show_meeting_details": true, + "training_participation_report": { + "initial_checkpoint_delay": { + "after": 100, + "within": 200, + }, + "checkpoint_interval": { + "after": 300, + "within": 400, + }, + }, } ); } @@ -2185,6 +2302,16 @@ mod tests { shared_folder: None, streaming_targets: Vec::new(), show_meeting_details: false, + training_participation_report: Some(TrainingParticipationReportParameterSet { + initial_checkpoint_delay: TimeRange { + after: 100, + within: 200, + }, + checkpoint_interval: TimeRange { + after: 300, + within: 400, + }, + }), }; assert_eq_json!( @@ -2247,6 +2374,16 @@ mod tests { "can_edit": false, "is_adhoc": false, "show_meeting_details": false, + "training_participation_report": { + "initial_checkpoint_delay": { + "after": 100, + "within": 200, + }, + "checkpoint_interval": { + "after": 300, + "within": 400, + }, + }, } ); } diff --git a/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs index 0bc5c9e17..ce9cd25be 100644 --- a/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs +++ b/crates/opentalk-controller-service/src/controller_backend/events/shared_folder.rs @@ -73,8 +73,16 @@ impl ControllerBackend { let (shared_folder, created) = put_shared_folder(&settings, event_id, &mut conn).await?; - let (event, _invite, room, sip_config, _is_favorite, _shared_folder, _tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + _shared_folder, + _tariff, + _training_participation_report, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; if send_email_notification { let shared_folder_for_user = shared_folder_for_user( @@ -117,8 +125,16 @@ impl ControllerBackend { let send_email_notification = !query.suppress_email_notification; - let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) = - Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + shared_folder, + _tariff, + _training_participation_report, + ) = Event::get_with_related_items(&mut conn, current_user.id, event_id).await?; if let Some(shared_folder) = shared_folder { let shared_folders = std::slice::from_ref(&shared_folder); diff --git a/crates/opentalk-controller-service/src/events/notifications.rs b/crates/opentalk-controller-service/src/events/notifications.rs index 3d7aefa9d..fc1ba56b6 100644 --- a/crates/opentalk-controller-service/src/events/notifications.rs +++ b/crates/opentalk-controller-service/src/events/notifications.rs @@ -61,8 +61,16 @@ pub async fn notify_event_invitees_by_room_about_update( let event = Event::get_for_room(conn, room_id).await?; if let Some(event) = event { - let (event, _invite, room, sip_config, _is_favorite, shared_folder, _tariff) = - Event::get_with_related_items(conn, current_user.id, event.id).await?; + let ( + event, + _invite, + room, + sip_config, + _is_favorite, + shared_folder, + _tariff, + _training_participation_report, + ) = Event::get_with_related_items(conn, current_user.id, event.id).await?; let shared_folder_for_user = shared_folder_for_user(shared_folder, event.created_by, current_user.id); diff --git a/crates/opentalk-db-storage/src/events/mod.rs b/crates/opentalk-db-storage/src/events/mod.rs index f5d7d7d87..b0d97c724 100644 --- a/crates/opentalk-db-storage/src/events/mod.rs +++ b/crates/opentalk-db-storage/src/events/mod.rs @@ -27,6 +27,7 @@ use opentalk_types_common::{ sql_enum, tenants::TenantId, time::TimeZone, + training_participation_report::{TimeRange, TrainingParticipationReportParameterSet}, users::UserId, }; use redis_args::{FromRedisValue, ToRedisArgs}; @@ -36,8 +37,9 @@ use self::shared_folders::EventSharedFolder; use crate::{ rooms::Room, schema::{ - event_exceptions, event_favorites, event_invites, event_shared_folders, events, rooms, - sip_configs, tariffs, users, + event_exceptions, event_favorites, event_invites, event_shared_folders, + event_training_participation_report_parameter_sets, events, rooms, sip_configs, tariffs, + users, }, sip_configs::SipConfig, tariffs::Tariff, @@ -352,36 +354,43 @@ impl Event { bool, Option<EventSharedFolder>, Tariff, + Option<EventTrainingParticipationReportParameterSet>, )> { - let query = events::table - .left_join( - event_invites::table.on(event_invites::event_id - .eq(events::id) - .and(event_invites::invitee.eq(user_id))), - ) - .left_join( - event_favorites::table.on(event_favorites::event_id - .eq(events::id) - .and(event_favorites::user_id.eq(user_id))), - ) - .left_join( - event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)), - ) - .inner_join(rooms::table.on(events::room.eq(rooms::id))) - .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room))) - .inner_join(users::table.on(users::id.eq(rooms::created_by))) - .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id))) - .select(( - events::all_columns, - event_invites::all_columns.nullable(), - rooms::all_columns, - sip_configs::all_columns.nullable(), - event_favorites::user_id.nullable().is_not_null(), - event_shared_folders::all_columns.nullable(), - tariffs::all_columns, - )) - .filter(events::id.eq(event_id)); - + let query = + events::table + .left_join( + event_invites::table.on(event_invites::event_id + .eq(events::id) + .and(event_invites::invitee.eq(user_id))), + ) + .left_join( + event_favorites::table.on(event_favorites::event_id + .eq(events::id) + .and(event_favorites::user_id.eq(user_id))), + ) + .left_join( + event_shared_folders::table.on(event_shared_folders::event_id.eq(events::id)), + ) + .inner_join(rooms::table.on(events::room.eq(rooms::id))) + .left_join(sip_configs::table.on(rooms::id.eq(sip_configs::room))) + .inner_join(users::table.on(users::id.eq(rooms::created_by))) + .inner_join(tariffs::table.on(tariffs::id.eq(users::tariff_id))) + .inner_join( + event_training_participation_report_parameter_sets::table + .on(event_training_participation_report_parameter_sets::event_id + .eq(events::id)), + ) + .select(( + events::all_columns, + event_invites::all_columns.nullable(), + rooms::all_columns, + sip_configs::all_columns.nullable(), + event_favorites::user_id.nullable().is_not_null(), + event_shared_folders::all_columns.nullable(), + tariffs::all_columns, + event_training_participation_report_parameter_sets::all_columns.nullable(), + )) + .filter(events::id.eq(event_id)); Ok(query.first(conn).await?) } @@ -431,6 +440,7 @@ impl Event { bool, Option<EventSharedFolder>, Tariff, + Option<TrainingParticipationReportParameterSet>, )>, > { // Filter applied to all events which validates that the event is either created by @@ -585,6 +595,8 @@ impl Event { } else { vec![] }; + // TODO: remove this + let training_participation_report_parameter_set = None; events_with_invite_room_and_exceptions.push(( event, @@ -595,6 +607,7 @@ impl Event { is_favorite, shared_folders, tariff, + training_participation_report_parameter_set, )); } @@ -1132,3 +1145,139 @@ impl NewEventFavorite { } } } + +#[derive(Debug, Insertable, Queryable, Identifiable, Associations)] +#[diesel(table_name = event_training_participation_report_parameter_sets)] +#[diesel(primary_key(event_id))] +#[diesel(belongs_to(Event, foreign_key = event_id))] +pub struct EventTrainingParticipationReportParameterSet { + pub event_id: EventId, + pub initial_checkpoint_delay_after: i64, + pub initial_checkpoint_delay_within: i64, + pub checkpoint_interval_after: i64, + pub checkpoint_interval_within: i64, +} + +impl EventTrainingParticipationReportParameterSet { + #[tracing::instrument(err, skip_all)] + pub async fn get_for_event(conn: &mut DbConnection, event_id: EventId) -> Result<Option<Self>> { + let parameter_set = event_training_participation_report_parameter_sets::table + .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id)) + .get_result(conn) + .await + .optional()?; + + Ok(parameter_set) + } + + /// Tries to insert the EventTrainingParticipationParameterSet into the database + /// + /// When yielding a unique key violation, None is returned. + #[tracing::instrument(err, skip_all)] + pub async fn try_insert(self, conn: &mut DbConnection) -> Result<Option<Self>> { + let query = self.insert_into(event_training_participation_report_parameter_sets::table); + + let result = query.get_result(conn).await; + + match result { + Ok(event_invite) => Ok(Some(event_invite)), + Err(diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UniqueViolation, + .., + )) => Ok(None), + Err(e) => Err(e.into()), + } + } + + #[tracing::instrument(err, skip_all)] + pub async fn delete_by_id(conn: &mut DbConnection, event_id: EventId) -> Result<()> { + let query = diesel::delete( + event_training_participation_report_parameter_sets::table + .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id)), + ); + + query.execute(conn).await?; + + Ok(()) + } +} + +impl From<EventTrainingParticipationReportParameterSet> + for TrainingParticipationReportParameterSet +{ + fn from( + EventTrainingParticipationReportParameterSet { + event_id: _, + initial_checkpoint_delay_after, + initial_checkpoint_delay_within, + checkpoint_interval_after, + checkpoint_interval_within, + }: EventTrainingParticipationReportParameterSet, + ) -> Self { + Self { + initial_checkpoint_delay: TimeRange { + after: u64::try_from(initial_checkpoint_delay_after).unwrap_or_default(), + within: u64::try_from(initial_checkpoint_delay_within).unwrap_or_default(), + }, + checkpoint_interval: TimeRange { + after: u64::try_from(checkpoint_interval_after).unwrap_or_default(), + within: u64::try_from(checkpoint_interval_within).unwrap_or_default(), + }, + } + } +} + +#[derive(AsChangeset)] +#[diesel(table_name = event_training_participation_report_parameter_sets)] +pub struct UpdateEventTrainingParticipationReportParameterSet { + pub initial_checkpoint_delay_after: Option<i64>, + pub initial_checkpoint_delay_within: Option<i64>, + pub checkpoint_interval_after: Option<i64>, + pub checkpoint_interval_within: Option<i64>, +} + +impl UpdateEventTrainingParticipationReportParameterSet { + /// Apply the update to the invite where `user_id` is the invitee + #[tracing::instrument(err, skip_all)] + pub async fn apply( + self, + conn: &mut DbConnection, + event_id: EventId, + ) -> Result<EventTrainingParticipationReportParameterSet> { + let query = diesel::update(event_training_participation_report_parameter_sets::table) + .filter(event_training_participation_report_parameter_sets::event_id.eq(event_id)) + .set(self) + // change it here + .returning(event_training_participation_report_parameter_sets::all_columns); + + let event_training_participation_report_parameter_sets = query.get_result(conn).await?; + + Ok(event_training_participation_report_parameter_sets) + } +} + +impl From<TrainingParticipationReportParameterSet> + for UpdateEventTrainingParticipationReportParameterSet +{ + fn from( + TrainingParticipationReportParameterSet { + initial_checkpoint_delay, + checkpoint_interval, + }: TrainingParticipationReportParameterSet, + ) -> Self { + Self { + initial_checkpoint_delay_after: Some( + i64::try_from(initial_checkpoint_delay.after).unwrap_or(i64::MAX), + ), + initial_checkpoint_delay_within: Some( + i64::try_from(initial_checkpoint_delay.within).unwrap_or(i64::MAX), + ), + checkpoint_interval_after: Some( + i64::try_from(checkpoint_interval.after).unwrap_or(i64::MAX), + ), + checkpoint_interval_within: Some( + i64::try_from(checkpoint_interval.within).unwrap_or(i64::MAX), + ), + } + } +} diff --git a/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql new file mode 100644 index 000000000..c9b3ed6f9 --- /dev/null +++ b/crates/opentalk-db-storage/src/migrations/V47__create_event_training_participation_report_parameter_sets.sql @@ -0,0 +1,7 @@ +CREATE TABLE event_training_participation_report_parameter_sets ( + event_id UUID PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE NOT NULL, + initial_checkpoint_delay_after BIGINT NOT NULL, + initial_checkpoint_delay_within BIGINT NOT NULL, + checkpoint_interval_after BIGINT NOT NULL, + checkpoint_interval_within BIGINT NOT NULL +); diff --git a/crates/opentalk-db-storage/src/schema.rs b/crates/opentalk-db-storage/src/schema.rs index f41661531..3e091cc2a 100644 --- a/crates/opentalk-db-storage/src/schema.rs +++ b/crates/opentalk-db-storage/src/schema.rs @@ -113,6 +113,18 @@ diesel::table! { } } +diesel::table! { + use crate::sql_types::*; + + event_training_participation_report_parameter_sets (event_id) { + event_id -> Uuid, + initial_checkpoint_delay_after -> Int8, + initial_checkpoint_delay_within -> Int8, + checkpoint_interval_after -> Int8, + checkpoint_interval_within -> Int8, + } +} + diesel::table! { use crate::sql_types::*; @@ -384,6 +396,7 @@ diesel::joinable!(event_favorites -> events (event_id)); diesel::joinable!(event_favorites -> users (user_id)); diesel::joinable!(event_invites -> events (event_id)); diesel::joinable!(event_shared_folders -> events (event_id)); +diesel::joinable!(event_training_participation_report_parameter_sets -> events (event_id)); diesel::joinable!(events -> rooms (room)); diesel::joinable!(events -> tenants (tenant_id)); diesel::joinable!(external_tariffs -> tariffs (tariff_id)); @@ -413,6 +426,7 @@ diesel::allow_tables_to_appear_in_same_query!( event_favorites, event_invites, event_shared_folders, + event_training_participation_report_parameter_sets, events, external_tariffs, groups, diff --git a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs index 2361ae8d3..d9f58596c 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/lib.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/lib.rs @@ -52,6 +52,7 @@ use opentalk_types_common::{ modules::ModuleId, rooms::RoomId, time::{TimeZone, Timestamp}, + training_participation_report::TimeRange, users::{DisplayName, UserId}, }; use opentalk_types_signaling::ParticipantId; @@ -63,7 +64,7 @@ use opentalk_types_signaling_training_participation_report::{ PresenceLoggingStartedReason, TrainingParticipationReportEvent, }, state::{ParticipationLoggingState, TrainingParticipationReportState}, - TimeRange, MODULE_ID, + MODULE_ID, }; use rand::Rng as _; use snafu::{Report, ResultExt as _}; @@ -209,6 +210,8 @@ impl TrainingParticipationReport { .get_recorded_presence_state(self.room, self.participant) .await?; + let parameter_set = None; + if control_data.is_room_owner { let (other_room_owners, trainees) = self .load_already_present_room_owners_and_trainees( @@ -225,8 +228,17 @@ impl TrainingParticipationReport { // Don't ask the room owner for confirmation state = ParticipationLoggingState::Enabled; } + + *frontend_data = Some(TrainingParticipationReportState { + state, + parameter_set, + }); + } else { + *frontend_data = Some(TrainingParticipationReportState { + state, + parameter_set: None, + }); } - *frontend_data = Some(TrainingParticipationReportState { state }); Ok(()) } diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs index 3f0a19e1b..655a0e75c 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/mod.rs @@ -19,11 +19,9 @@ mod test_common { use std::collections::{BTreeMap, BTreeSet}; use opentalk_signaling_core::SignalingModuleError; - use opentalk_types_common::rooms::RoomId; + use opentalk_types_common::{rooms::RoomId, training_participation_report::TimeRange}; use opentalk_types_signaling::ParticipantId; - use opentalk_types_signaling_training_participation_report::{ - state::ParticipationLoggingState, TimeRange, - }; + use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState; use pretty_assertions::assert_eq; use super::TrainingParticipationReportStorage; diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs index 2c7a0e153..9bf44eddd 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/redis.rs @@ -6,11 +6,11 @@ use std::collections::{BTreeMap, BTreeSet}; use async_trait::async_trait; use opentalk_signaling_core::{NotFoundSnafu, RedisConnection, RedisSnafu, SignalingModuleError}; -use opentalk_types_common::{rooms::RoomId, time::Timestamp}; -use opentalk_types_signaling::ParticipantId; -use opentalk_types_signaling_training_participation_report::{ - state::ParticipationLoggingState, TimeRange, +use opentalk_types_common::{ + rooms::RoomId, time::Timestamp, training_participation_report::TimeRange, }; +use opentalk_types_signaling::ParticipantId; +use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState; use redis::{AsyncCommands, ExistenceCheck, SetOptions}; use redis_args::{FromRedisValue, ToRedisArgs}; use serde::{Deserialize, Serialize}; diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs index 16e734e91..ce960b89a 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/room_state.rs @@ -4,9 +4,8 @@ use std::collections::BTreeSet; -use opentalk_types_common::time::Timestamp; +use opentalk_types_common::{time::Timestamp, training_participation_report::TimeRange}; use opentalk_types_signaling::ParticipantId; -use opentalk_types_signaling_training_participation_report::TimeRange; use super::{Checkpoint, TrainingReportState}; diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs index 83648365e..1d0f5d87c 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/training_participation_report_storage.rs @@ -6,11 +6,11 @@ use std::collections::BTreeSet; use async_trait::async_trait; use opentalk_signaling_core::SignalingModuleError; -use opentalk_types_common::{rooms::RoomId, time::Timestamp}; -use opentalk_types_signaling::ParticipantId; -use opentalk_types_signaling_training_participation_report::{ - state::ParticipationLoggingState, TimeRange, +use opentalk_types_common::{ + rooms::RoomId, time::Timestamp, training_participation_report::TimeRange, }; +use opentalk_types_signaling::ParticipantId; +use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState; use super::{RoomState, TrainingReportState}; diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs index 998f2ff60..2b2b4d85c 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/memory.rs @@ -5,11 +5,11 @@ use std::collections::{BTreeMap, BTreeSet}; use opentalk_signaling_core::{NotFoundSnafu, SignalingModuleError}; -use opentalk_types_common::{rooms::RoomId, time::Timestamp}; -use opentalk_types_signaling::ParticipantId; -use opentalk_types_signaling_training_participation_report::{ - state::ParticipationLoggingState, TimeRange, +use opentalk_types_common::{ + rooms::RoomId, time::Timestamp, training_participation_report::TimeRange, }; +use opentalk_types_signaling::ParticipantId; +use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState; use snafu::{ensure_whatever, OptionExt as _}; use crate::storage::{Checkpoint, RoomState, TrainingReportState}; diff --git a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs index 40355229a..95ebf1e25 100644 --- a/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs +++ b/crates/opentalk-signaling-module-training-participation-report/src/storage/volatile/storage.rs @@ -9,11 +9,11 @@ use std::{ use async_trait::async_trait; use opentalk_signaling_core::{SignalingModuleError, VolatileStaticMemoryStorage}; -use opentalk_types_common::{rooms::RoomId, time::Timestamp}; -use opentalk_types_signaling::ParticipantId; -use opentalk_types_signaling_training_participation_report::{ - state::ParticipationLoggingState, TimeRange, +use opentalk_types_common::{ + rooms::RoomId, time::Timestamp, training_participation_report::TimeRange, }; +use opentalk_types_signaling::ParticipantId; +use opentalk_types_signaling_training_participation_report::state::ParticipationLoggingState; use parking_lot::RwLock; use super::memory::TrainingParticipationReportState; diff --git a/deny.toml b/deny.toml index adf627c2f..428373243 100644 --- a/deny.toml +++ b/deny.toml @@ -322,7 +322,6 @@ skip = [ { name = "indexmap", version = "1" }, { name = "itertools", version = "0.10" }, { name = "itertools", version = "0.11" }, - { name = "itertools", version = "0.12" }, { name = "jiff", version = "0.1" }, { name = "linux-raw-sys", version = "0.4" }, { name = "p256", version = "0.11" }, diff --git a/docs/developer/database.md b/docs/developer/database.md index 8783ef729..b885582a5 100644 --- a/docs/developer/database.md +++ b/docs/developer/database.md @@ -75,6 +75,13 @@ event_shared_folders { text write_share_id text write_url } +event_training_participation_report_parameter_sets { + uuid event_id PK,FK + bigint checkpoint_interval_after + bigint checkpoint_interval_within + bigint initial_checkpoint_delay_after + bigint initial_checkpoint_delay_within +} events { uuid id PK uuid created_by FK @@ -241,6 +248,7 @@ event_invites }o--|| events: "" event_invites }o--|| users: "" event_invites }o--|| users: "" event_shared_folders |o--|| events: "" +event_training_participation_report_parameter_sets |o--|| events: "" events }o--|| rooms: "" events }o--|| users: "" events }o--|| users: "" -- GitLab