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

User moderation: Delete a user's communities on block #1023

Merged
merged 5 commits into from
Sep 25, 2023
Merged
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
49 changes: 39 additions & 10 deletions invenio_communities/administration/communities.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
# details.

"""Invenio administration OAI-PMH view module."""
from flask import app, current_app, has_app_context
from functools import partial

from flask import current_app
from invenio_administration.views.base import (
AdminResourceDetailView,
AdminResourceListView,
)
from invenio_search_ui.searchconfig import search_app_config

from invenio_communities.communities.schema import CommunityFeaturedSchema

Expand All @@ -36,28 +39,54 @@ class CommunityListView(AdminResourceListView):
display_edit = False

item_field_list = {
"slug": {
"text": "Slug",
"order": 1,
},
"metadata.title": {"text": "Title", "order": 2},
"slug": {"text": "Slug", "order": 1, "width": 1},
"metadata.title": {"text": "Title", "order": 2, "width": 4},
# This field is for display only, it won't work on forms
"ui.type.title_l10n": {"text": "Type", "order": 3},
"featured": {"text": "Featured", "order": 4},
"created": {"text": "Created", "order": 5},
"ui.type.title_l10n": {"text": "Type", "order": 3, "width": 2},
"featured": {"text": "Featured", "order": 4, "width": 1},
"created": {"text": "Created", "order": 5, "width": 2},
}

actions = {
"featured": {
"text": "Feature",
"payload_schema": CommunityFeaturedSchema,
"order": 1,
}
},
# custom components in the UI
"delete": {
"text": "Delete",
"payload_schema": None,
"order": 2,
},
# custom components in the UI
"restore": {
"text": "Restore",
"payload_schema": None,
"order": 2,
},
}
search_config_name = "COMMUNITIES_SEARCH"
search_facets_config_name = "COMMUNITIES_FACETS"
search_sort_config_name = "COMMUNITIES_SORT_OPTIONS"

def init_search_config(self):
"""Build search view config."""
return partial(
search_app_config,
config_name=self.get_search_app_name(),
available_facets=current_app.config.get(self.search_facets_config_name),
sort_options=current_app.config[self.search_sort_config_name],
endpoint=self.get_api_endpoint(),
headers=self.get_search_request_headers(),
initial_filters=[["status", "P"]],
hidden_params=[
["include_deleted", "1"],
],
page=1,
size=30,
)

@staticmethod
def disabled():
"""Disable the view on demand."""
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { APIRoutes } from "./routes";
import { http } from "react-invenio-forms";

const getFeatured = async (apiEndpoint) => {
return await http.get(apiEndpoint);
};

const deleteCommunity = async (community, payload) => {
const reason = payload["removal_reason"];
payload["removal_reason"] = { id: reason };
// WARNING: Axios does not accept payload without data key
return await http.delete(APIRoutes.delete(community), {
data: { ...payload },
headers: { ...http.headers, if_match: community.revision_id },
});
};

const restore = async (record) => {
return await http.post(APIRoutes.restore(record));
};

export const InvenioAdministrationCommunitiesApi = {
getFeatured: getFeatured,
delete: deleteCommunity,
restore: restore,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { InvenioAdministrationCommunitiesApi } from "./api";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import _get from "lodash/get";

const APIRoutesGenerators = {
delete: (record, idKeyPath = "id") => {
return `/api/communities/${_get(record, idKeyPath)}`;
},
restore: (record, idKeyPath = "id") => {
return `/api/communities/${_get(record, idKeyPath)}/restore`;
},
};

export const APIRoutes = {
...APIRoutesGenerators,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* // This file is part of Invenio-App-Rdm
* // Copyright (C) 2023 CERN.
* //
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
* // under the terms of the MIT License; see LICENSE file for more details.
*/

import { RestoreConfirmation } from "./RestoreConfirmation";
import TombstoneForm from "./TombstoneForm";
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Button, Modal, Icon } from "semantic-ui-react";
import { ActionModal, ActionForm } from "@js/invenio_administration";
import _isEmpty from "lodash/isEmpty";
import { i18next } from "@translations/invenio_app_rdm/i18next";

export class RecordResourceActions extends Component {
constructor(props) {
super(props);
this.state = {
modalOpen: false,
modalHeader: undefined,
modalBody: undefined,
};
}

onModalTriggerClick = (e, { payloadSchema, dataName, dataActionKey }) => {
const { resource } = this.props;

if (dataActionKey === "delete") {
this.setState({
modalOpen: true,
modalHeader: i18next.t("Delete community"),
modalBody: (
<TombstoneForm
actionSuccessCallback={this.handleSuccess}
actionCancelCallback={this.closeModal}
resource={resource}
/>
),
});
} else if (dataActionKey === "restore") {
this.setState({
modalOpen: true,
modalHeader: i18next.t("Restore community"),
modalBody: (
<RestoreConfirmation
actionSuccessCallback={this.handleSuccess}
actionCancelCallback={this.closeModal}
resource={resource}
/>
),
});
} else {
this.setState({
modalOpen: true,
modalHeader: dataName,
modalBody: (
<ActionForm
actionKey={dataActionKey}
actionSchema={payloadSchema}
actionSuccessCallback={this.handleSuccess}
actionCancelCallback={this.closeModal}
resource={resource}
/>
),
});
}
};

closeModal = () => {
this.setState({
modalOpen: false,
modalHeader: undefined,
modalBody: undefined,
});
};

handleSuccess = () => {
const { successCallback } = this.props;
this.setState({
modalOpen: false,
modalHeader: undefined,
modalBody: undefined,
});
successCallback();
};

render() {
const { actions, Element, resource } = this.props;
const { modalOpen, modalHeader, modalBody } = this.state;
return (
<>
{Object.entries(actions).map(([actionKey, actionConfig]) => {

Check warning on line 95 in invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / Tests (3.8, pypi, postgresql13, opensearch2)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 95 in invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / Tests (3.8, pypi, postgresql13, elasticsearch7)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 95 in invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / Tests (3.9, pypi, postgresql13, opensearch2)

Array.prototype.map() expects a value to be returned at the end of arrow function

Check warning on line 95 in invenio_communities/assets/semantic-ui/js/invenio_communities/administration/components/RecordResourceActions.js

View workflow job for this annotation

GitHub Actions / Tests (3.9, pypi, postgresql13, elasticsearch7)

Array.prototype.map() expects a value to be returned at the end of arrow function
if (actionKey === "delete" && !resource.deletion_status.is_deleted) {
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
icon
fluid
labelPosition="left"
>
<Icon name="trash alternate" />
{actionConfig.text}
</Element>
);
} else if (actionKey === "restore" && resource.deletion_status.is_deleted) {
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
icon
fluid
labelPosition="left"
>
<Icon name="undo" />
{actionConfig.text}
</Element>
);
} else if (!resource.deletion_status.is_deleted && actionKey !== "restore") {
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
>
{actionConfig.text}
</Element>
);
}
})}
<ActionModal modalOpen={modalOpen} resource={resource}>
{modalHeader && <Modal.Header>{modalHeader}</Modal.Header>}
{!_isEmpty(modalBody) && modalBody}
</ActionModal>
</>
);
}
}

RecordResourceActions.propTypes = {
resource: PropTypes.object.isRequired,
successCallback: PropTypes.func.isRequired,
actions: PropTypes.shape({
text: PropTypes.string.isRequired,
payload_schema: PropTypes.object.isRequired,
order: PropTypes.number.isRequired,
}),
Element: PropTypes.node,
};

RecordResourceActions.defaultProps = {
Element: Button,
actions: undefined,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* // This file is part of Invenio-App-Rdm
* // Copyright (C) 2023 CERN.
* //
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
* // under the terms of the MIT License; see LICENSE file for more details.
*/

import React, { Component } from "react";
import PropTypes from "prop-types";
import { Dropdown, http, withCancel } from "react-invenio-forms";
import { Dropdown as SUIDropdown } from "semantic-ui-react";
import { i18next } from "@translations/invenio_app_rdm/i18next";

export default class RemovalReasonsSelect extends Component {
constructor(props) {
super(props);
this.state = {
options: undefined,
loading: false,
defaultOpt: undefined,
};
}

componentDidMount() {
this.fetchOptions();
}

componentWillUnmount() {
this.cancellableAction && this.cancellableAction.cancel();
}

fetchOptions = async () => {
const { setFieldValue } = this.props;
this.setState({ loading: true });
const url = "/api/vocabularies/removalreasons";
this.cancellableAction = withCancel(
http.get(url, {
headers: {
Accept: "application/vnd.inveniordm.v1+json",
},
})
);
try {
const response = await this.cancellableAction.promise;
const options = response.data.hits.hits;

const defaultOpt = options
? { text: options[0].title_l10n, value: options[0].id, key: options[0].id }
: {};
this.setState({
options: options,
loading: false,
defaultOpt: defaultOpt,
});
setFieldValue("removal_reason", defaultOpt.value);
} catch (e) {
this.setState({ loading: false });
console.error(e);
}
};

render() {
const { loading, options, defaultOpt } = this.state;

if (loading) {
return <SUIDropdown loading={loading} />;
}

return (
<Dropdown
required
label={i18next.t("Unavailability statement")}
options={options}
fieldPath="removal_reason"
defaultValue={defaultOpt}
clearable={false}
/>
);
}
}

RemovalReasonsSelect.propTypes = {
setFieldValue: PropTypes.func.isRequired,
};
Loading
Loading