invenio-app-rdm 13.0.0b3.dev15__py2.py3-none-any.whl → 13.0.0b3.dev17__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invenio_app_rdm/__init__.py +1 -1
- invenio_app_rdm/administration/audit_logs/audit_logs.py +22 -18
- invenio_app_rdm/communities_ui/sitemap.py +63 -0
- invenio_app_rdm/communities_ui/views/communities.py +9 -8
- invenio_app_rdm/communities_ui/views/ui.py +1 -1
- invenio_app_rdm/config.py +14 -0
- invenio_app_rdm/records_ui/sitemap.py +44 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/AuditLogActions.js +136 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/ViewJson.js +35 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/ViewRecentChanges.js +119 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/index.js +2 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/search/SearchResultItemLayout.js +34 -18
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/components/RevisionsDiffViewer.js +1 -1
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/api.js +5 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/records/api/routes.js +7 -1
- invenio_app_rdm/theme/assets/semantic-ui/translations/invenio_app_rdm/package.json +2 -1
- invenio_app_rdm/theme/{static → templates/semantic-ui/invenio_app_rdm}/robots.txt +4 -0
- invenio_app_rdm/theme/views.py +6 -2
- invenio_app_rdm/theme/webpack.py +1 -0
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/METADATA +13 -2
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/RECORD +29 -24
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/WHEEL +1 -1
- tests/conftest.py +126 -8
- tests/ui/conftest.py +6 -8
- tests/ui/test_robotstxt.py +35 -0
- tests/ui/test_sitemaps.py +85 -0
- invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/ViewAction.js +0 -0
- tests/ui/test_static.py +0 -25
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/entry_points.txt +0 -0
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/licenses/LICENSE +0 -0
- {invenio_app_rdm-13.0.0b3.dev15.dist-info → invenio_app_rdm-13.0.0b3.dev17.dist-info}/top_level.txt +0 -0
invenio_app_rdm/__init__.py
CHANGED
|
@@ -34,36 +34,40 @@ class AuditLogListView(AdminResourceListView):
|
|
|
34
34
|
display_edit = False
|
|
35
35
|
|
|
36
36
|
item_field_list = {
|
|
37
|
-
"id": {
|
|
38
|
-
"text": _("Log ID"),
|
|
39
|
-
"order": 1,
|
|
40
|
-
"width": 3,
|
|
41
|
-
"width": 3,
|
|
42
|
-
},
|
|
43
37
|
"resource.type": {
|
|
44
38
|
"text": _("Resource"),
|
|
45
|
-
"order":
|
|
39
|
+
"order": 1,
|
|
46
40
|
"width": 2,
|
|
47
41
|
},
|
|
48
|
-
"resource.id": {
|
|
42
|
+
"resource.id": {
|
|
49
43
|
"text": _("Resource ID"),
|
|
50
|
-
"order":
|
|
51
|
-
"width": 2,
|
|
44
|
+
"order": 2,
|
|
52
45
|
},
|
|
53
46
|
"action": {
|
|
54
47
|
"text": _("Action"),
|
|
55
|
-
"order":
|
|
56
|
-
"width": 2,
|
|
48
|
+
"order": 3,
|
|
57
49
|
},
|
|
58
|
-
"user.id": {
|
|
50
|
+
"user.id": {
|
|
59
51
|
"text": _("User"),
|
|
60
|
-
"order":
|
|
61
|
-
"width": 3,
|
|
52
|
+
"order": 4,
|
|
62
53
|
},
|
|
63
54
|
"created": {
|
|
64
|
-
"text": _("
|
|
65
|
-
"order":
|
|
66
|
-
|
|
55
|
+
"text": _("Created"),
|
|
56
|
+
"order": 5,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
actions = {
|
|
61
|
+
"view_log": {
|
|
62
|
+
"text": _("View Log"),
|
|
63
|
+
"payload_schema": None,
|
|
64
|
+
"order": 1,
|
|
65
|
+
},
|
|
66
|
+
"view_changes": {
|
|
67
|
+
"text": _("View Changes"),
|
|
68
|
+
"payload_schema": None,
|
|
69
|
+
"order": 2,
|
|
70
|
+
"show_for": ["record.publish"],
|
|
67
71
|
},
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2025 CERN.
|
|
4
|
+
# Copyright (C) 2025 Northwestern University.
|
|
5
|
+
#
|
|
6
|
+
# Invenio-App-RDM is free software; you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
|
|
9
|
+
"""Community sitemap content."""
|
|
10
|
+
|
|
11
|
+
from invenio_base import invenio_url_for
|
|
12
|
+
from invenio_communities.proxies import current_communities
|
|
13
|
+
from invenio_search.api import RecordsSearchV2
|
|
14
|
+
from invenio_sitemap import SitemapSection, format_to_w3c
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SitemapSectionOfCommunities(SitemapSection):
|
|
18
|
+
"""Defines the Sitemap entries for Communities."""
|
|
19
|
+
|
|
20
|
+
def iter_entities(self):
|
|
21
|
+
"""Iterate over objects."""
|
|
22
|
+
communities_scan = (
|
|
23
|
+
RecordsSearchV2(index=current_communities.service.record_cls.index._name)
|
|
24
|
+
.filter("term", **{"access.visibility": "public"})
|
|
25
|
+
.filter("term", deletion_status="P")
|
|
26
|
+
.sort("-updated")
|
|
27
|
+
# Using preserve_order is fine.
|
|
28
|
+
# Using Point in Time is a recommended alternative but not
|
|
29
|
+
# particularly better than this one for our needs. See
|
|
30
|
+
# https://opensearch.org/docs/latest/search-plugins/searching-data/point-in-time/
|
|
31
|
+
.params(preserve_order=True)
|
|
32
|
+
# In keeping with original: only page is looked for
|
|
33
|
+
# (curation policy is not most relevant)
|
|
34
|
+
.source(["slug", "updated", "metadata.page"])
|
|
35
|
+
.scan()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for community in communities_scan:
|
|
39
|
+
yield {
|
|
40
|
+
"slug": community.slug,
|
|
41
|
+
"updated": community.updated,
|
|
42
|
+
"loc": invenio_url_for(
|
|
43
|
+
"invenio_app_rdm_communities.communities_detail",
|
|
44
|
+
pid_value=community.slug,
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if "page" in community.get("metadata", {}):
|
|
49
|
+
yield {
|
|
50
|
+
"slug": community.slug,
|
|
51
|
+
"updated": community.updated,
|
|
52
|
+
"loc": invenio_url_for(
|
|
53
|
+
"invenio_communities.communities_about",
|
|
54
|
+
pid_value=community.slug,
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def to_dict(self, entity):
|
|
59
|
+
"""To dict used in sitemap."""
|
|
60
|
+
return {
|
|
61
|
+
"loc": entity["loc"],
|
|
62
|
+
"lastmod": format_to_w3c(entity["updated"]),
|
|
63
|
+
}
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"""Request views module."""
|
|
10
10
|
|
|
11
11
|
from flask import abort, g, redirect, request, url_for
|
|
12
|
+
from invenio_collections.errors import (
|
|
13
|
+
CollectionNotFound,
|
|
14
|
+
CollectionTreeNotFound,
|
|
15
|
+
LogoNotFoundError,
|
|
16
|
+
)
|
|
17
|
+
from invenio_collections.proxies import current_collections
|
|
12
18
|
from invenio_communities.views.communities import (
|
|
13
19
|
HEADER_PERMISSIONS,
|
|
14
20
|
_get_roles_can_invite,
|
|
@@ -18,11 +24,6 @@ from invenio_communities.views.communities import (
|
|
|
18
24
|
from invenio_communities.views.decorators import pass_community
|
|
19
25
|
from invenio_pages.proxies import current_pages_service
|
|
20
26
|
from invenio_pages.records.errors import PageNotFoundError
|
|
21
|
-
from invenio_rdm_records.collections import (
|
|
22
|
-
CollectionNotFound,
|
|
23
|
-
CollectionTreeNotFound,
|
|
24
|
-
LogoNotFoundError,
|
|
25
|
-
)
|
|
26
27
|
from invenio_rdm_records.proxies import (
|
|
27
28
|
current_community_records_service,
|
|
28
29
|
current_rdm_records,
|
|
@@ -54,7 +55,7 @@ def communities_detail(pid_value, community, community_ui):
|
|
|
54
55
|
def communities_home(pid_value, community, community_ui):
|
|
55
56
|
"""Community home page."""
|
|
56
57
|
query_params = request.args
|
|
57
|
-
collections_service =
|
|
58
|
+
collections_service = current_collections.service
|
|
58
59
|
permissions = community.has_permissions_to(HEADER_PERMISSIONS)
|
|
59
60
|
if not permissions["can_read"]:
|
|
60
61
|
raise PermissionDeniedError()
|
|
@@ -124,7 +125,7 @@ def communities_browse(pid_value, community, community_ui):
|
|
|
124
125
|
"""Community browse page."""
|
|
125
126
|
permissions = community.has_permissions_to(HEADER_PERMISSIONS)
|
|
126
127
|
|
|
127
|
-
collections_service =
|
|
128
|
+
collections_service = current_collections.service
|
|
128
129
|
|
|
129
130
|
trees_ui = collections_service.list_trees(
|
|
130
131
|
g.identity, community_id=community.id, depth=2
|
|
@@ -167,7 +168,7 @@ def community_collection(
|
|
|
167
168
|
community, community_ui, pid_value, tree_slug=None, collection_slug=None
|
|
168
169
|
):
|
|
169
170
|
"""Render a community collection page."""
|
|
170
|
-
collections_service =
|
|
171
|
+
collections_service = current_collections.service
|
|
171
172
|
try:
|
|
172
173
|
collection = collections_service.read(
|
|
173
174
|
identity=g.identity,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"""Communities UI blueprints module."""
|
|
11
11
|
|
|
12
12
|
from flask import Blueprint, current_app, request
|
|
13
|
+
from invenio_collections.searchapp import search_app_context as c_search_app_context
|
|
13
14
|
from invenio_communities.errors import CommunityDeletedError
|
|
14
15
|
from invenio_communities.views.ui import (
|
|
15
16
|
not_found_error,
|
|
@@ -17,7 +18,6 @@ from invenio_communities.views.ui import (
|
|
|
17
18
|
record_tombstone_error,
|
|
18
19
|
)
|
|
19
20
|
from invenio_pidstore.errors import PIDDeletedError, PIDDoesNotExistError
|
|
20
|
-
from invenio_rdm_records.collections import search_app_context as c_search_app_context
|
|
21
21
|
from invenio_records_resources.services.errors import (
|
|
22
22
|
PermissionDeniedError,
|
|
23
23
|
RecordPermissionDeniedError,
|
invenio_app_rdm/config.py
CHANGED
|
@@ -167,6 +167,8 @@ from invenio_vocabularies.contrib.subjects.datastreams import (
|
|
|
167
167
|
)
|
|
168
168
|
from werkzeug.local import LocalProxy
|
|
169
169
|
|
|
170
|
+
from .communities_ui.sitemap import SitemapSectionOfCommunities
|
|
171
|
+
from .records_ui.sitemap import SitemapSectionOfRDMRecords
|
|
170
172
|
from .theme.views import notification_settings
|
|
171
173
|
from .users.schemas import NotificationsUserSchema, UserPreferencesNotificationsSchema
|
|
172
174
|
|
|
@@ -485,6 +487,10 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
485
487
|
"task": "invenio_jobs.logging.tasks.delete_logs",
|
|
486
488
|
"schedule": crontab(minute=5, hour=0),
|
|
487
489
|
},
|
|
490
|
+
"update_sitemap": {
|
|
491
|
+
"task": "invenio_sitemap.tasks.update_sitemap_cache",
|
|
492
|
+
"schedule": crontab(minute=0, hour=2),
|
|
493
|
+
},
|
|
488
494
|
}
|
|
489
495
|
"""Scheduled tasks configuration (aka cronjobs)."""
|
|
490
496
|
|
|
@@ -1479,3 +1485,11 @@ APP_RDM_SUBCOMMUNITIES_LABEL = "Subcommunities"
|
|
|
1479
1485
|
|
|
1480
1486
|
RDM_DETAIL_SIDE_BAR_MANAGE_ATTRIBUTES_EXTENSION_TEMPLATE = None
|
|
1481
1487
|
"""Side bar manage attributes extension template."""
|
|
1488
|
+
|
|
1489
|
+
# Invenio-Sitemap
|
|
1490
|
+
# ===============
|
|
1491
|
+
# See https://github.com/inveniosoftware/invenio-sitemap/blob/master/invenio_sitemap/config.py # noqa
|
|
1492
|
+
SITEMAP_SECTIONS = [
|
|
1493
|
+
SitemapSectionOfRDMRecords(),
|
|
1494
|
+
SitemapSectionOfCommunities(),
|
|
1495
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2025 CERN.
|
|
4
|
+
# Copyright (C) 2025 Northwestern University.
|
|
5
|
+
#
|
|
6
|
+
# Invenio-App-RDM is free software; you can redistribute it and/or modify it
|
|
7
|
+
# under the terms of the MIT License; see LICENSE file for more details.
|
|
8
|
+
|
|
9
|
+
"""RDMRecords sitemap content."""
|
|
10
|
+
|
|
11
|
+
from invenio_base import invenio_url_for
|
|
12
|
+
from invenio_rdm_records.proxies import current_rdm_records_service
|
|
13
|
+
from invenio_search.api import RecordsSearchV2
|
|
14
|
+
from invenio_sitemap import SitemapSection, format_to_w3c
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SitemapSectionOfRDMRecords(SitemapSection):
|
|
18
|
+
"""Defines the Sitemap entries for Records."""
|
|
19
|
+
|
|
20
|
+
def iter_entities(self):
|
|
21
|
+
"""Iterate over objects."""
|
|
22
|
+
records_scan = (
|
|
23
|
+
RecordsSearchV2(index=current_rdm_records_service.record_cls.index._name)
|
|
24
|
+
.filter("term", **{"access.record": "public"})
|
|
25
|
+
.filter("term", deletion_status="P")
|
|
26
|
+
.sort("-updated")
|
|
27
|
+
# Using preserve_order is fine.
|
|
28
|
+
# Using Point in Time is a recommended alternative but not
|
|
29
|
+
# particularly better than this one for our needs. See
|
|
30
|
+
# https://opensearch.org/docs/latest/search-plugins/searching-data/point-in-time/
|
|
31
|
+
.params(preserve_order=True)
|
|
32
|
+
.source(["id", "updated"])
|
|
33
|
+
.scan()
|
|
34
|
+
)
|
|
35
|
+
return records_scan
|
|
36
|
+
|
|
37
|
+
def to_dict(self, entity):
|
|
38
|
+
"""To dict used in sitemap."""
|
|
39
|
+
return {
|
|
40
|
+
"loc": invenio_url_for(
|
|
41
|
+
"invenio_app_rdm_records.record_detail", pid_value=entity["id"]
|
|
42
|
+
),
|
|
43
|
+
"lastmod": format_to_w3c(entity["updated"]),
|
|
44
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* // This file is part of Invenio-App-Rdm
|
|
3
|
+
* // Copyright (C) 2025 CERN.
|
|
4
|
+
* //
|
|
5
|
+
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
|
|
6
|
+
* // under the terms of the MIT License; see LICENSE file for more details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { Component } from "react";
|
|
10
|
+
import PropTypes from "prop-types";
|
|
11
|
+
import { Button, Modal, Icon } from "semantic-ui-react";
|
|
12
|
+
import { ActionModal } from "@js/invenio_administration";
|
|
13
|
+
import _isEmpty from "lodash/isEmpty";
|
|
14
|
+
import { i18next } from "@translations/invenio_app_rdm/i18next";
|
|
15
|
+
import { ViewRecentChanges } from "./ViewRecentChanges";
|
|
16
|
+
import { ViewJson } from "./ViewJson";
|
|
17
|
+
|
|
18
|
+
export class AuditLogActions extends Component {
|
|
19
|
+
constructor(props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = {
|
|
22
|
+
modalOpen: false,
|
|
23
|
+
modalHeader: undefined,
|
|
24
|
+
modalBody: undefined,
|
|
25
|
+
modalProps: undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onModalTriggerClick = (e, { payloadSchema, dataName, dataActionKey }) => {
|
|
30
|
+
const { resource } = this.props;
|
|
31
|
+
|
|
32
|
+
if (dataActionKey === "view_changes") {
|
|
33
|
+
this.setState({
|
|
34
|
+
modalOpen: true,
|
|
35
|
+
modalHeader: i18next.t("Recent changes"),
|
|
36
|
+
modalProps: {
|
|
37
|
+
size: "large",
|
|
38
|
+
},
|
|
39
|
+
modalBody: (
|
|
40
|
+
<ViewRecentChanges
|
|
41
|
+
actionCancelCallback={this.closeModal}
|
|
42
|
+
resource={resource}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
} else if (dataActionKey === "view_log") {
|
|
47
|
+
this.setState({
|
|
48
|
+
modalOpen: true,
|
|
49
|
+
modalHeader: i18next.t("Audit Log Details"),
|
|
50
|
+
modalProps: {
|
|
51
|
+
size: "large",
|
|
52
|
+
},
|
|
53
|
+
modalBody: <ViewJson jsonData={resource} onCloseHandler={this.closeModal} />,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
closeModal = () => {
|
|
59
|
+
this.setState({
|
|
60
|
+
modalOpen: false,
|
|
61
|
+
modalHeader: undefined,
|
|
62
|
+
modalBody: undefined,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
const { actions, Element, resource } = this.props;
|
|
68
|
+
const { action } = resource;
|
|
69
|
+
const { modalOpen, modalHeader, modalBody, modalProps } = this.state;
|
|
70
|
+
let icon;
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
{Object.entries(actions).map(([actionKey, actionConfig]) => {
|
|
74
|
+
if (actionKey === "view_log") {
|
|
75
|
+
icon = "eye";
|
|
76
|
+
return (
|
|
77
|
+
<Element
|
|
78
|
+
key={actionKey}
|
|
79
|
+
onClick={this.onModalTriggerClick}
|
|
80
|
+
payloadSchema={actionConfig.payload_schema}
|
|
81
|
+
dataName={actionConfig.text}
|
|
82
|
+
dataActionKey={actionKey}
|
|
83
|
+
icon={icon}
|
|
84
|
+
fluid
|
|
85
|
+
basic
|
|
86
|
+
labelPosition="left"
|
|
87
|
+
>
|
|
88
|
+
{icon && <Icon name={icon} />}
|
|
89
|
+
{actionConfig.text}...
|
|
90
|
+
</Element>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (actionKey === "view_changes" && actionConfig.show_for.includes(action)) {
|
|
94
|
+
icon = "file code outline";
|
|
95
|
+
return (
|
|
96
|
+
<Element
|
|
97
|
+
key={actionKey}
|
|
98
|
+
onClick={this.onModalTriggerClick}
|
|
99
|
+
payloadSchema={actionConfig.payload_schema}
|
|
100
|
+
dataName={actionConfig.text}
|
|
101
|
+
dataActionKey={actionKey}
|
|
102
|
+
icon={icon}
|
|
103
|
+
fluid
|
|
104
|
+
basic
|
|
105
|
+
labelPosition="left"
|
|
106
|
+
>
|
|
107
|
+
{icon && <Icon name={icon} />}
|
|
108
|
+
{actionConfig.text}...
|
|
109
|
+
</Element>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})}
|
|
114
|
+
<ActionModal modalOpen={modalOpen} resource={resource} modalProps={modalProps}>
|
|
115
|
+
{modalHeader && <Modal.Header>{modalHeader}</Modal.Header>}
|
|
116
|
+
{!_isEmpty(modalBody) && modalBody}
|
|
117
|
+
</ActionModal>
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
AuditLogActions.propTypes = {
|
|
124
|
+
resource: PropTypes.object.isRequired,
|
|
125
|
+
actions: PropTypes.shape({
|
|
126
|
+
text: PropTypes.string.isRequired,
|
|
127
|
+
payload_schema: PropTypes.object.isRequired,
|
|
128
|
+
order: PropTypes.number.isRequired,
|
|
129
|
+
}),
|
|
130
|
+
Element: PropTypes.node,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
AuditLogActions.defaultProps = {
|
|
134
|
+
Element: Button,
|
|
135
|
+
actions: undefined,
|
|
136
|
+
};
|
invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/ViewJson.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* // This file is part of Invenio-App-Rdm
|
|
3
|
+
* // Copyright (C) 2025 CERN.
|
|
4
|
+
* //
|
|
5
|
+
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
|
|
6
|
+
* // under the terms of the MIT License; see LICENSE file for more details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { Component } from "react";
|
|
10
|
+
import { Button, Modal } from "semantic-ui-react";
|
|
11
|
+
import PropTypes from "prop-types";
|
|
12
|
+
import ReactJson from "react-json-view";
|
|
13
|
+
|
|
14
|
+
export class ViewJson extends Component {
|
|
15
|
+
render() {
|
|
16
|
+
const { jsonData, onCloseHandler } = this.props;
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<Modal.Content>
|
|
20
|
+
<Modal.Description>
|
|
21
|
+
<ReactJson src={jsonData} name={null} />
|
|
22
|
+
</Modal.Description>
|
|
23
|
+
</Modal.Content>
|
|
24
|
+
<Modal.Actions>
|
|
25
|
+
<Button onClick={onCloseHandler}>Close</Button>
|
|
26
|
+
</Modal.Actions>
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ViewJson.propTypes = {
|
|
33
|
+
jsonData: PropTypes.object.isRequired,
|
|
34
|
+
onCloseHandler: PropTypes.object.isRequired,
|
|
35
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* // This file is part of Invenio-App-Rdm
|
|
3
|
+
* // Copyright (C) 2025 CERN.
|
|
4
|
+
* //
|
|
5
|
+
* // Invenio-App-Rdm is free software; you can redistribute it and/or modify it
|
|
6
|
+
* // under the terms of the MIT License; see LICENSE file for more details.
|
|
7
|
+
*/
|
|
8
|
+
import React, { Component } from "react";
|
|
9
|
+
import PropTypes from "prop-types";
|
|
10
|
+
import { RecordModerationApi } from "../records/api";
|
|
11
|
+
import { withCancel, ErrorMessage } from "react-invenio-forms";
|
|
12
|
+
import { Modal, Button, Grid } from "semantic-ui-react";
|
|
13
|
+
import { i18next } from "@translations/invenio_app_rdm/i18next";
|
|
14
|
+
import { RevisionsDiffViewer } from "../components/RevisionsDiffViewer";
|
|
15
|
+
|
|
16
|
+
export class ViewRecentChanges extends Component {
|
|
17
|
+
constructor(props) {
|
|
18
|
+
super(props);
|
|
19
|
+
|
|
20
|
+
this.state = {
|
|
21
|
+
loading: true,
|
|
22
|
+
error: undefined,
|
|
23
|
+
diff: undefined,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async fetchPreviousRevision() {
|
|
28
|
+
const { resource } = this.props;
|
|
29
|
+
const {
|
|
30
|
+
resource: record,
|
|
31
|
+
metadata: { revision_id: targetRevision } = { revision_id: null },
|
|
32
|
+
} = resource;
|
|
33
|
+
this.setState({ loading: true });
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
if (!targetRevision) {
|
|
37
|
+
this.setState({
|
|
38
|
+
error: i18next.t("No revision ID found."),
|
|
39
|
+
loading: false,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.cancellableAction = withCancel(
|
|
44
|
+
RecordModerationApi.getLastRevision(record, targetRevision, true)
|
|
45
|
+
);
|
|
46
|
+
const response = await this.cancellableAction.promise;
|
|
47
|
+
const revisions = await response.data;
|
|
48
|
+
|
|
49
|
+
this.setState({
|
|
50
|
+
diff: {
|
|
51
|
+
targetRevision: revisions[0],
|
|
52
|
+
srcRevision: revisions.length > 1 ? revisions[1] : revisions[0],
|
|
53
|
+
},
|
|
54
|
+
loading: false,
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error === "UNMOUNTED") return;
|
|
58
|
+
this.setState({ error: error, loading: false });
|
|
59
|
+
console.error(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
componentDidMount() {
|
|
64
|
+
this.fetchPreviousRevision();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
componentWillUnmount() {
|
|
68
|
+
this.cancellableAction && this.cancellableAction.cancel();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
handleModalClose = () => {
|
|
72
|
+
const { actionCancelCallback } = this.props;
|
|
73
|
+
actionCancelCallback();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
render() {
|
|
77
|
+
const { error, loading, diff } = this.state;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Modal.Content>
|
|
82
|
+
{error && (
|
|
83
|
+
<Modal.Content>
|
|
84
|
+
<ErrorMessage
|
|
85
|
+
header={i18next.t("Unable to fetch revisions.")}
|
|
86
|
+
content={error}
|
|
87
|
+
icon="exclamation"
|
|
88
|
+
className="text-align-left"
|
|
89
|
+
negative
|
|
90
|
+
/>
|
|
91
|
+
</Modal.Content>
|
|
92
|
+
)}
|
|
93
|
+
</Modal.Content>
|
|
94
|
+
<Modal.Content scrolling>
|
|
95
|
+
<RevisionsDiffViewer diff={diff} />
|
|
96
|
+
</Modal.Content>
|
|
97
|
+
<Modal.Actions>
|
|
98
|
+
<Grid>
|
|
99
|
+
<Grid.Column floated="left" width={8} textAlign="left">
|
|
100
|
+
<Button
|
|
101
|
+
onClick={this.handleModalClose}
|
|
102
|
+
disabled={loading}
|
|
103
|
+
loading={loading}
|
|
104
|
+
aria-label={i18next.t("Cancel revision comparison")}
|
|
105
|
+
>
|
|
106
|
+
Close
|
|
107
|
+
</Button>
|
|
108
|
+
</Grid.Column>
|
|
109
|
+
</Grid>
|
|
110
|
+
</Modal.Actions>
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ViewRecentChanges.propTypes = {
|
|
117
|
+
resource: PropTypes.object.isRequired,
|
|
118
|
+
actionCancelCallback: PropTypes.func.isRequired,
|
|
119
|
+
};
|
invenio_app_rdm/theme/assets/semantic-ui/js/invenio_app_rdm/administration/auditLogs/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createSearchAppInit } from "@js/invenio_search_ui";
|
|
|
9
9
|
import { NotificationController } from "@js/invenio_administration";
|
|
10
10
|
import { SearchResultItemLayout } from "./search";
|
|
11
11
|
import { SearchFacets } from "@js/invenio_administration";
|
|
12
|
+
import { AuditLogActions } from "./AuditLogActions";
|
|
12
13
|
|
|
13
14
|
const domContainer = document.getElementById("invenio-search-config");
|
|
14
15
|
|
|
@@ -18,6 +19,7 @@ const overridenComponents = {
|
|
|
18
19
|
...defaultComponents,
|
|
19
20
|
"InvenioAdministration.SearchResultItem.layout": SearchResultItemLayout,
|
|
20
21
|
"SearchApp.facets": SearchFacets,
|
|
22
|
+
"InvenioAdministration.ResourceActions": AuditLogActions,
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
createSearchAppInit(
|
|
@@ -8,26 +8,24 @@
|
|
|
8
8
|
|
|
9
9
|
import PropTypes from "prop-types";
|
|
10
10
|
import React, { Component } from "react";
|
|
11
|
-
import { Item, Table } from "semantic-ui-react";
|
|
12
|
-
import { Image } from "react-invenio-forms";
|
|
11
|
+
import { Button, Item, Table } from "semantic-ui-react";
|
|
12
|
+
import { Image, toRelativeTime } from "react-invenio-forms";
|
|
13
13
|
import { withState } from "react-searchkit";
|
|
14
14
|
import { i18next } from "@translations/invenio_app_rdm/i18next";
|
|
15
|
+
import { Actions } from "@js/invenio_administration";
|
|
16
|
+
import { AdminUIRoutes } from "@js/invenio_administration/src/routes";
|
|
15
17
|
|
|
16
18
|
class SearchResultItemComponent extends Component {
|
|
17
|
-
componentDidMount() {
|
|
18
|
-
console.error("result", this.props.result);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
19
|
refreshAfterAction = () => {
|
|
22
20
|
const { updateQueryState, currentQueryState } = this.props;
|
|
23
21
|
updateQueryState(currentQueryState);
|
|
24
22
|
};
|
|
25
23
|
|
|
26
24
|
render() {
|
|
27
|
-
const { result } =
|
|
25
|
+
const { title, resourceName, result, actions, idKeyPath, listUIEndpoint } =
|
|
26
|
+
this.props;
|
|
28
27
|
|
|
29
28
|
const {
|
|
30
|
-
id,
|
|
31
29
|
created,
|
|
32
30
|
action,
|
|
33
31
|
resource: { id: resourceId, type: resourceType },
|
|
@@ -36,14 +34,9 @@ class SearchResultItemComponent extends Component {
|
|
|
36
34
|
|
|
37
35
|
return (
|
|
38
36
|
<Table.Row>
|
|
39
|
-
<Table.Cell data-label={i18next.t("Log ID")}>
|
|
40
|
-
<a target="_blank" rel="noreferrer noopener" href={result.links.self}>
|
|
41
|
-
{id}
|
|
42
|
-
</a>
|
|
43
|
-
</Table.Cell>
|
|
44
37
|
<Table.Cell data-label={i18next.t("Resource")}>{resourceType}</Table.Cell>
|
|
45
38
|
<Table.Cell data-label={i18next.t("Resource ID")}>
|
|
46
|
-
<a href={`/
|
|
39
|
+
<a target="_blank" rel="noreferrer noopener" href={`/uploads/${resourceId}`}>
|
|
47
40
|
{resourceId}
|
|
48
41
|
</a>
|
|
49
42
|
</Table.Cell>
|
|
@@ -51,12 +44,30 @@ class SearchResultItemComponent extends Component {
|
|
|
51
44
|
<Table.Cell data-label={i18next.t("User")}>
|
|
52
45
|
<Item className="flex" key={userId}>
|
|
53
46
|
<Image src={`/api/users/${userId}/avatar.svg`} avatar loadFallbackFirst />
|
|
54
|
-
<a href={`/administration/users?q=id:${userId}`}>
|
|
55
|
-
{userEmail} ({userId})
|
|
56
|
-
</a>
|
|
47
|
+
<a href={`/administration/users?q=id:${userId}`}>{userEmail}</a>
|
|
57
48
|
</Item>
|
|
58
49
|
</Table.Cell>
|
|
59
|
-
<Table.Cell data-label={i18next.t("
|
|
50
|
+
<Table.Cell data-label={i18next.t("Created")}>
|
|
51
|
+
{toRelativeTime(created)}
|
|
52
|
+
</Table.Cell>
|
|
53
|
+
|
|
54
|
+
{/* Actions */}
|
|
55
|
+
<Table.Cell collapsing>
|
|
56
|
+
<Button.Group size="tiny" basic widths={5} compact className="margined">
|
|
57
|
+
<Actions
|
|
58
|
+
title={title}
|
|
59
|
+
resourceName={resourceName}
|
|
60
|
+
editUrl={AdminUIRoutes.editView(listUIEndpoint, result, idKeyPath)}
|
|
61
|
+
displayEdit={false}
|
|
62
|
+
displayDelete={false}
|
|
63
|
+
actions={actions}
|
|
64
|
+
resource={result}
|
|
65
|
+
idKeyPath={idKeyPath}
|
|
66
|
+
successCallback={this.refreshAfterAction}
|
|
67
|
+
listUIEndpoint={listUIEndpoint}
|
|
68
|
+
/>
|
|
69
|
+
</Button.Group>
|
|
70
|
+
</Table.Cell>
|
|
60
71
|
</Table.Row>
|
|
61
72
|
);
|
|
62
73
|
}
|
|
@@ -64,8 +75,13 @@ class SearchResultItemComponent extends Component {
|
|
|
64
75
|
|
|
65
76
|
SearchResultItemComponent.propTypes = {
|
|
66
77
|
result: PropTypes.object.isRequired,
|
|
78
|
+
idKeyPath: PropTypes.string.isRequired,
|
|
67
79
|
updateQueryState: PropTypes.func.isRequired,
|
|
68
80
|
currentQueryState: PropTypes.object.isRequired,
|
|
81
|
+
listUIEndpoint: PropTypes.string.isRequired,
|
|
82
|
+
title: PropTypes.string.isRequired,
|
|
83
|
+
resourceName: PropTypes.string.isRequired,
|
|
84
|
+
actions: PropTypes.object.isRequired,
|
|
69
85
|
};
|
|
70
86
|
|
|
71
87
|
export const SearchResultItemLayout = withState(SearchResultItemComponent);
|