From c45e2c46d7acc4120eb0dcc4812e0620ce04ae2e Mon Sep 17 00:00:00 2001
From: Brendan O'Connell
Date: Wed, 11 Dec 2024 13:04:47 +0100
Subject: [PATCH 1/3] Updated changelog
---
CHANGELOG.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79af24cc7..4691eab2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
-
+### Changed
+ - [634](https://github.com/thoth-pub/thoth/issues/634) - Prevent changing a `Work`'s `WorkStatus` to `Forthcoming`, `Cancelled` or `Postponed` once its `WorkStatus` has been set to `Active`.
+ - [659](https://github.com/thoth-pub/thoth/issues/659) - Prevent deletion of a `Work` by non-superusers after `WorkStatus` has been set to `Active`.
## [[0.13.4]](https://github.com/thoth-pub/thoth/releases/tag/v0.13.4) - 2024-12-11
### Added
From fde822b710a4fbc306b38d8e90a969560ff55012 Mon Sep 17 00:00:00 2001
From: Brendan O'Connell
Date: Mon, 16 Dec 2024 11:14:51 +0100
Subject: [PATCH 2/3] Add a warning dialogue before a non-superuser can change
an unpublished Work to published
---
thoth-api/src/graphql/model.rs | 17 +++
thoth-app/src/component/mod.rs | 1 +
thoth-app/src/component/work.rs | 37 ++++++-
.../src/component/work_status_dialogue.rs | 103 ++++++++++++++++++
thoth-errors/src/lib.rs | 2 +
5 files changed, 154 insertions(+), 6 deletions(-)
create mode 100644 thoth-app/src/component/work_status_dialogue.rs
diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs
index 339ff4e1c..3dd6a53ea 100644
--- a/thoth-api/src/graphql/model.rs
+++ b/thoth-api/src/graphql/model.rs
@@ -1744,6 +1744,23 @@ impl MutationRoot {
}
data.validate()?;
+
+ let is_data_unpublished =
+ data.work_status == WorkStatus::Forthcoming ||
+ data.work_status == WorkStatus::Cancelled||
+ data.work_status == WorkStatus::PostponedIndefinitely;
+
+ let is_work_published =
+ work.work_status == WorkStatus::Active ||
+ work.work_status == WorkStatus::Withdrawn ||
+ work.work_status == WorkStatus::Superseded;
+
+ // return an error if a non-superuser attempts to change the
+ // Work Status of a published Work to unpublished
+ if is_work_published && is_data_unpublished && !context.account_access.is_superuser {
+ return Err(ThothError::ThothSetWorkStatusError.into());
+ }
+
let account_id = context.token.jwt.as_ref().unwrap().account_id(&context.db);
// update the work and, if it succeeds, synchronise its children statuses and pub. date
match work.update(&context.db, &data, &account_id) {
diff --git a/thoth-app/src/component/mod.rs b/thoth-app/src/component/mod.rs
index a3c523d77..034ab5bf9 100644
--- a/thoth-app/src/component/mod.rs
+++ b/thoth-app/src/component/mod.rs
@@ -495,4 +495,5 @@ pub mod serieses;
pub mod subjects_form;
pub mod utils;
pub mod work;
+pub mod work_status_dialogue;
pub mod works;
diff --git a/thoth-app/src/component/work.rs b/thoth-app/src/component/work.rs
index 185185bf0..d8d26cd84 100644
--- a/thoth-app/src/component/work.rs
+++ b/thoth-app/src/component/work.rs
@@ -50,6 +50,7 @@ use crate::component::utils::FormUrlInput;
use crate::component::utils::FormWorkStatusSelect;
use crate::component::utils::FormWorkTypeSelect;
use crate::component::utils::Loader;
+use crate::component::work_status_dialogue::ConfirmWorkStatusComponent;
use crate::models::work::delete_work_mutation::DeleteWorkRequest;
use crate::models::work::delete_work_mutation::DeleteWorkRequestBody;
use crate::models::work::delete_work_mutation::PushActionDeleteWork;
@@ -83,6 +84,8 @@ pub struct WorkComponent {
imprint_id: Uuid,
// Track work_type stored in database, as distinct from work_type selected in dropdown
work_type: WorkType,
+ // Track work_status stored in database, as distinct from work_status selected in dropdown
+ current_work_status: WorkStatus,
data: WorkFormData,
fetch_work: FetchWork,
push_work: PushUpdateWork,
@@ -169,6 +172,7 @@ impl Component for WorkComponent {
let doi_warning = Default::default();
let imprint_id = work.imprint.imprint_id;
let work_type = work.work_type;
+ let current_work_status = work.work_status;
let data: WorkFormData = Default::default();
let resource_access = ctx.props().current_user.resource_access.clone();
let work_id = ctx.props().work_id;
@@ -181,6 +185,7 @@ impl Component for WorkComponent {
doi_warning,
imprint_id,
work_type,
+ current_work_status,
data,
fetch_work,
push_work,
@@ -207,6 +212,7 @@ impl Component for WorkComponent {
self.doi = self.work.doi.clone().unwrap_or_default().to_string();
self.imprint_id = self.work.imprint.imprint_id;
self.work_type = self.work.work_type;
+ self.current_work_status = self.work.work_status;
body.data.imprints.clone_into(&mut self.data.imprints);
body.data
.work_types
@@ -575,14 +581,20 @@ impl Component for WorkComponent {
true => vec![WorkType::BookChapter],
false => vec![],
};
- // Grey out chapter-specific or "book"-specific fields
+
+ // Variables required to grey out chapter-specific or "book"-specific fields
// based on currently selected work type.
let is_chapter = self.work.work_type == WorkType::BookChapter;
let is_not_withdrawn_or_superseded = self.work.work_status != WorkStatus::Withdrawn
&& self.work.work_status != WorkStatus::Superseded;
- let is_active_withdrawn_or_superseded = self.work.work_status == WorkStatus::Active
+ let is_published = self.work.work_status == WorkStatus::Active
|| self.work.work_status == WorkStatus::Withdrawn
|| self.work.work_status == WorkStatus::Superseded;
+
+ let current_state_unpublished = self.current_work_status == WorkStatus::Forthcoming ||
+ self.current_work_status == WorkStatus::PostponedIndefinitely ||
+ self.current_work_status == WorkStatus::Cancelled;
+
html! {
<>
-
diff --git a/thoth-app/src/component/work_status_dialogue.rs b/thoth-app/src/component/work_status_dialogue.rs
index 484a7efcf..ea0a480a8 100644
--- a/thoth-app/src/component/work_status_dialogue.rs
+++ b/thoth-app/src/component/work_status_dialogue.rs
@@ -1,5 +1,6 @@
use crate::string::CANCEL_BUTTON;
use crate::string::SAVE_BUTTON;
+use thoth_api::account::model::AccountDetails;
use yew::html;
use yew::prelude::*;
@@ -11,12 +12,17 @@ pub struct ConfirmWorkStatusComponent {
pub struct Props {
pub onclick: Option>,
pub object_name: String,
+ pub current_user: AccountDetails,
+ pub current_state_unpublished: bool,
+ pub is_published: bool,
+ // pub form_callback: Callback<()>,
#[prop_or_default]
pub deactivated: bool,
}
pub enum Msg {
ToggleConfirmWorkStatusDisplay(bool),
+ ExecuteCallback
}
impl Component for ConfirmWorkStatusComponent {
@@ -33,6 +39,16 @@ impl Component for ConfirmWorkStatusComponent {
self.show = value;
true
}
+ Msg::ExecuteCallback => {
+ self.show = false;
+ // trigger the callback
+ // _ctx.props().form_callback(|_| form_callback).emit(());
+ // form_callback.emit(());
+ // when set as true, the modal closes and it saves correctly
+ // true
+ // when set as false, the modal also closes and saves correctly.
+ false
+ }
}
}
@@ -45,11 +61,20 @@ impl Component for ConfirmWorkStatusComponent {
e.prevent_default();
Msg::ToggleConfirmWorkStatusDisplay(false)
});
+ let modal_behavior = if !ctx.props().current_user.resource_access.is_superuser
+ && ctx.props().current_state_unpublished
+ && ctx.props().is_published {
+ &open_modal
+ } else {
+ &close_modal
+ };
+
html! {
<>
+ // Ok, so it looks like the delete confirmation doesn't take care of
+ // closing the modal because the delete message redirects to a different route,
+ // so there's no need to close the modal
+
+ // You'll need to explicitly close the work status modal yourself
+
+ // to do so, you can create a message in the dialogue that is
+ // called onclick and (a) closes the dialogue, (b) emmits the onclick callback
+ // it receives from work.rs
+