Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/634 659 prevent changing work status back prevent post publication deletion #663

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions thoth-api/src/graphql/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions thoth-app/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
51 changes: 43 additions & 8 deletions thoth-app/src/component/work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -181,6 +185,7 @@ impl Component for WorkComponent {
doi_warning,
imprint_id,
work_type,
current_work_status,
data,
fetch_work,
push_work,
Expand All @@ -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
Expand Down Expand Up @@ -552,10 +558,14 @@ impl Component for WorkComponent {
FetchState::NotFetching(_) => html! {<Loader/>},
FetchState::Fetching(_) => html! {<Loader/>},
FetchState::Fetched(_body) => {
let callback = ctx.link().callback(|event: FocusEvent| {
let form_callback = ctx.link().callback(|event: FocusEvent| {
event.prevent_default();
Msg::UpdateWork
});
// if matches conditions for non-superuser (i.e. dialogue is present)
// let form_callback = event.prevent_default();
// nice to have: trigger dialogue by hitting enter

// FormImprintSelect: while the work has any related issues, the imprint cannot
// be changed, because an issue's series and work must both have the same imprint.
let imprints = match self.work.issues.as_ref().unwrap_or(&vec![]).is_empty() {
Expand All @@ -575,14 +585,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! {
<>
<nav class="level">
Expand All @@ -601,7 +617,7 @@ impl Component for WorkComponent {
</div>
</nav>

<form onsubmit={ callback }>
<form onsubmit={ form_callback }>
<div class="field is-horizontal">
<div class="field-body">
<FormWorkTypeSelect
Expand Down Expand Up @@ -657,7 +673,7 @@ impl Component for WorkComponent {
label = "Publication Date"
value={ self.work.publication_date.clone() }
oninput={ ctx.link().callback(|e: InputEvent| Msg::ChangeDate(e.to_value())) }
required = { is_active_withdrawn_or_superseded }
required = { is_published }
/>
<FormDateInput
label = "Withdrawn Date"
Expand Down Expand Up @@ -821,9 +837,28 @@ impl Component for WorkComponent {

<div class="field">
<div class="control">
<button class="button is-success" type="submit">
{ SAVE_BUTTON }
</button>
// if the Work is unpublished (forthcoming, postponed, cancelled)
// and non-superuser sets to published (active, withdrawn, superseded),
// display warning dialogue
// if !ctx.props().current_user.resource_access.is_superuser
// && current_state_unpublished
// && is_published
// // is superuser = false
// {
<ConfirmWorkStatusComponent
onclick={ ctx.link().callback(|_| Msg::UpdateWork) }
object_name={ self.work.full_title.clone() }
current_user={ ctx.props().current_user.clone() }
current_state_unpublished={ current_state_unpublished }
is_published={ is_published }


/>
// } else {
// <button class="button is-success" type="submit">
// { SAVE_BUTTON }
// </button>
// }
</div>
</div>
</form>
Expand Down
139 changes: 139 additions & 0 deletions thoth-app/src/component/work_status_dialogue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use crate::string::CANCEL_BUTTON;
use crate::string::SAVE_BUTTON;
use thoth_api::account::model::AccountDetails;
use yew::html;
use yew::prelude::*;

pub struct ConfirmWorkStatusComponent {
show: bool,
}

#[derive(PartialEq, Properties)]
pub struct Props {
pub onclick: Option<Callback<MouseEvent>>,
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 {
type Message = Msg;
type Properties = Props;

fn create(_ctx: &Context<Self>) -> Self {
ConfirmWorkStatusComponent { show: false }
}

fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::ToggleConfirmWorkStatusDisplay(value) => {
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
}
}
}

fn view(&self, ctx: &Context<Self>) -> Html {
let open_modal = ctx.link().callback(|e: MouseEvent| {
e.prevent_default();
Msg::ToggleConfirmWorkStatusDisplay(true)
});
let close_modal = ctx.link().callback(|e: MouseEvent| {
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! {
<>
<button
class="button is-success"
// onclick={ open_modal }
onclick={ modal_behavior }
disabled={ ctx.props().deactivated }
>
{ SAVE_BUTTON }
</button>
<div class={ self.confirm_work_status_status() }>
<div class="modal-background" onclick={ &close_modal }></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{ "Confirm changing work status" }</p>
<button
class="delete"
aria-label="close"
onclick={ &close_modal }
></button>
</header>
<section class="modal-card-body">
<p>
{ "Are you sure you want to change the work status to Active for " }
<i>{ &ctx.props().object_name }</i>
{ "?" }
</p>
</section>
// 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

<footer class="modal-card-foot">
<button
class="button is-success"
// onclick={ ctx.props().onclick.clone() }
onclick={ ctx.link().callback(|_| Msg::ExecuteCallback) }
>
{ SAVE_BUTTON }
</button>
<button
class="button"
onclick={ &close_modal }
>
{ CANCEL_BUTTON }
</button>
</footer>
</div>
</div>
</>
}
}
}

impl ConfirmWorkStatusComponent {
fn confirm_work_status_status(&self) -> String {
match self.show {
true => "modal is-active".to_string(),
false => "modal".to_string(),
}
}
}
2 changes: 2 additions & 0 deletions thoth-errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ pub enum ThothError {
ThothLocationError,
#[error("Only superusers can update the canonical location when Thoth Location Platform is already set as canonical.")]
ThothUpdateCanonicalError,
#[error("Once a Work has been published, it cannot return to an unpublished Work Status.")]
ThothSetWorkStatusError,
}

impl ThothError {
Expand Down
Loading