From 5be04f06d1b059f40b5b6b1130be2d1c0db7077e Mon Sep 17 00:00:00 2001 From: Ole Wieners Date: Tue, 13 Aug 2024 15:14:22 +0200 Subject: [PATCH] Change `mount_series` endpoint to accept and move existing series This is necessary for an upcoming admin UI feature which will allow admins to change the path of series pages with no other blocks. --- backend/src/api/model/realm/mod.rs | 22 +++++++----- backend/src/api/model/realm/mutations.rs | 7 ++++ backend/src/api/model/series.rs | 2 +- backend/src/api/mutation.rs | 45 ++++++++++++++++++++++-- frontend/src/schema.graphql | 11 ++++-- 5 files changed, 72 insertions(+), 15 deletions(-) diff --git a/backend/src/api/model/realm/mod.rs b/backend/src/api/model/realm/mod.rs index 1d3a3307d..561b85d9f 100644 --- a/backend/src/api/model/realm/mod.rs +++ b/backend/src/api/model/realm/mod.rs @@ -230,6 +230,18 @@ impl Realm { self.is_user_realm() && self.parent_key.is_none() } + pub(crate) async fn number_of_descendants(&self, context: &Context) -> ApiResult { + let count = context.db + .query_one( + "select count(*) from realms where full_path like $1 || '/%'", + &[&self.full_path], + ) + .await? + .get::<_, i64>(0); + + Ok(count.try_into().expect("number of descendants overflows i32")) + } + /// Returns the username of the user owning this realm tree IF it is a user /// realm. Otherwise returns `None`. pub(crate) fn owning_user(&self) -> Option<&str> { @@ -440,15 +452,7 @@ impl Realm { /// Returns the number of realms that are descendants of this one /// (excluding this one). Returns a number ≥ 0. async fn number_of_descendants(&self, context: &Context) -> ApiResult { - let count = context.db - .query_one( - "select count(*) from realms where full_path like $1 || '/%'", - &[&self.full_path], - ) - .await? - .get::<_, i64>(0); - - Ok(count.try_into().expect("number of descendants overflows i32")) + self.number_of_descendants(context).await } /// Returns whether the current user has the rights to add sub-pages, edit realm content, diff --git a/backend/src/api/model/realm/mutations.rs b/backend/src/api/model/realm/mutations.rs index 39508106e..463b3f4da 100644 --- a/backend/src/api/model/realm/mutations.rs +++ b/backend/src/api/model/realm/mutations.rs @@ -379,6 +379,13 @@ impl UpdatedRealmName { block: Some(block), } } + + pub(crate) fn plain(name: String) -> Self { + Self { + plain: Some(name), + block: None, + } + } } #[derive(juniper::GraphQLInputObject)] diff --git a/backend/src/api/model/series.rs b/backend/src/api/model/series.rs index 645a4290d..35b5199e0 100644 --- a/backend/src/api/model/series.rs +++ b/backend/src/api/model/series.rs @@ -176,7 +176,7 @@ impl Node for Series { #[derive(GraphQLInputObject)] pub(crate) struct NewSeries { - opencast_id: String, + pub(crate) opencast_id: String, title: String, // TODO In the future this `struct` can be extended with additional // (potentially optional) fields. For now we only need these. diff --git a/backend/src/api/mutation.rs b/backend/src/api/mutation.rs index 33bf2abac..ed22aec6d 100644 --- a/backend/src/api/mutation.rs +++ b/backend/src/api/mutation.rs @@ -232,12 +232,20 @@ impl Mutation { BlockValue::remove(id, context).await } - /// Atomically mount a series into an (empty) realm. + /// Atomically mount or move a series into an (empty) realm. /// Creates all the necessary realms on the path to the target /// and adds a block with the given series at the leaf. + /// + /// If the series is already mounted in `current_realm_path`, we need to check + /// if that realm has any other blocks. + /// - If it does, moving the series is not allowed. + /// - Otherwise, the series is moved, but there is some cleanup necessary: + /// - If the realm has any sub-realms: only the series block is removed. + /// - Otherwise the whole realm is removed. async fn mount_series( series: NewSeries, parent_realm_path: String, + current_realm_path: Option, #[graphql(default = vec![])] new_realms: Vec, context: &Context, @@ -268,8 +276,6 @@ impl Mutation { } } - let series = Series::create(series, context).await?; - let target_realm = { let mut target_realm = parent_realm; for RealmSpecifier { name, path_segment } in new_realms { @@ -285,6 +291,39 @@ impl Mutation { target_realm }; + let series = if let Some(current_realm_path) = current_realm_path { + let old_realm = Realm::load_by_path(current_realm_path, context) + .await? + .ok_or_else(|| invalid_input!("`currentRealmPath` does not refer to a valid realm"))?; + let blocks = BlockValue::load_for_realm(old_realm.key, context).await?; + if blocks.len() > 1 { + // This is already checked and prohibited on the front end side (admin UI), + // but I suppose checking here doesn't hurt . + return Err(invalid_input!("series can only be moved if it's current realm has no other blocks")); + } + + if old_realm.number_of_descendants(context).await? < 1 { + // The realm has no children, so it can be removed. + Realm::remove(old_realm.id(), context).await?; + } else { + // At this point we can be certain that there is only one block, which is the series block. + // Ideally we would restore the previous title, but that's not stored anywhere. So the realm + // gets the name of its path segment. + Realm::rename( + old_realm.id(), + UpdatedRealmName::plain(old_realm.path_segment), + context, + ).await?; + BlockValue::remove(blocks[0].id(), context).await?; + } + + Series::load_by_opencast_id(series.opencast_id, context) + .await? + .ok_or_else(|| invalid_input!("`seriesId` does not refer to a valid series"))? + } else { + Series::create(series, context).await? + }; + BlockValue::add_series( Id::realm(target_realm.key), 0, diff --git a/frontend/src/schema.graphql b/frontend/src/schema.graphql index 0ea3690e5..062be74d6 100644 --- a/frontend/src/schema.graphql +++ b/frontend/src/schema.graphql @@ -559,11 +559,18 @@ type Mutation { "Remove a block from a realm." removeBlock(id: ID!): RemovedBlock! """ - Atomically mount a series into an (empty) realm. + Atomically mount or move a series into an (empty) realm. Creates all the necessary realms on the path to the target and adds a block with the given series at the leaf. + + If the series is already mounted in `current_realm_path`, we need to check + if that realm has any other blocks. + - If it does, moving the series is not allowed. + - Otherwise, the series is moved, but there is some cleanup necessary: + - If the realm has any sub-realms: only the series block is removed. + - Otherwise the whole realm is removed. """ - mountSeries(series: NewSeries!, parentRealmPath: String!, newRealms: [RealmSpecifier!]!): Realm! + mountSeries(series: NewSeries!, parentRealmPath: String!, currentRealmPath: String, newRealms: [RealmSpecifier!]!): Realm! } "Exactly one of `plain` or `block` has to be non-null."