Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
data-pending-communities-search-config='{{ search_app_rdm_record_requests_config(app_id="InvenioAppRdm.RecordRequests", endpoint=record_ui["links"]["requests"]) | tojson }}'
data-permissions='{{ permissions | tojson }}'
data-record='{{ record_ui | tojson }}'
data-record-requests='{{ record_requests | default({}) | tojson }}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the default needed, I assume the record_requests in this case would be empty when passed, so maybe we don't need it

Copy link
Contributor Author

@carlinmack carlinmack Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it is passed as undefined to the HTML in the case of the review page for example. This is a safeguard as otherwise the page doesn't render (undefined is not valid json)

class='sidebar-container'
>

Expand Down
44 changes: 44 additions & 0 deletions invenio_app_rdm/records_ui/views/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from flask import abort, current_app, g, redirect, render_template, request, url_for
from flask_login import current_user
from flask_principal import AnonymousIdentity
from invenio_base.utils import obj_or_import_string
from invenio_communities.communities.resources.serializer import (
UICommunityJSONSerializer,
Expand All @@ -28,7 +29,10 @@
from invenio_rdm_records.records.systemfields.access.access_settings import (
AccessSettings,
)
from invenio_rdm_records.requests import CommunityInclusion, CommunitySubmission
from invenio_rdm_records.resources.serializers import UIJSONSerializer
from invenio_requests.proxies import current_requests_service
from invenio_search.api import dsl
from invenio_stats.proxies import current_stats
from invenio_users_resources.proxies import current_user_resources
from marshmallow import ValidationError
Expand Down Expand Up @@ -99,6 +103,43 @@ def get_record_community(record):
return None, None


def get_record_requests(record, identity):
"""Return all requests that concern this record.

Output: {<Community-UUID>: <Request-UUID>}
"""
can_review = current_rdm_records.records_service.check_permission(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be "can_preview"? This would also include the links with guest token who would have access.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain why can preview? From what I understand can preview refers to people who can preview a draft, whereas review is people who can review the record (i.e. see the request)

Copy link
Contributor

@sakshamarora1 sakshamarora1 Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guests can also see the requests! Screenshot from permissions settings during share link creation

Screenshot 2025-12-04 at 09 41 04

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update our docs then ahah https://help.zenodo.org/docs/share/about/

identity, "review", record=record._record
)
if not can_review:
return {}

if type(identity) is AnonymousIdentity:
return {} # secret link users do not have permissions to search requests

record_requests = current_requests_service.search(
identity,
extra_filter=dsl.Q(
"bool",
must=[
dsl.Q("term", **{"topic.record": record["id"]}),
dsl.Q(
"terms",
**{
"type": [
CommunityInclusion.type_id,
CommunitySubmission.type_id,
]
},
),
],
),
params={"sort": "oldest"},
)

return {r["receiver"]["community"]: r["id"] for r in record_requests}


class PreviewFile:
"""Preview file implementation for InvenioRDM.

Expand Down Expand Up @@ -238,6 +279,8 @@ def record_detail(
)
theme = resolved_community_ui.get("theme", {}) if resolved_community else None

record_requests = get_record_requests(record, g.identity)

return render_community_theme_template(
current_app.config.get("APP_RDM_RECORD_LANDING_PAGE_TEMPLATE"),
theme=theme,
Expand Down Expand Up @@ -267,6 +310,7 @@ def record_detail(
is_draft=is_draft,
community=resolved_community,
community_ui=resolved_community_ui,
record_requests=record_requests,
external_resources=get_external_resources(record),
user_avatar=avatar,
record_deletion=record_deletion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@
{%- block request_header %}
{% if is_user_dashboard %}
{% set back_button_url = url_for("invenio_app_rdm_users.requests") %}
{% else %}
{% elif community %}
{% set back_button_url = url_for("invenio_communities.communities_requests", pid_value=community.id) %}
{% else %}
{#
if you access from a secret link there is no dashboard or community
/access/requests/<id>?token=<token>
back button is not rendered if there is no URL
#}
{% set back_button_url = "" %}
{% endif %}
{% from "invenio_requests/macros/request_header.html" import inclusion_request_header %}
{{ inclusion_request_header(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class CommunitiesManagement extends Component {
recordUserCommunitySearchConfig,
searchConfig,
record,
recordRequests,
} = this.props;
const { communities, loading, error, manageCommunitiesModalOpen } = this.state;
return (
Expand Down Expand Up @@ -126,6 +127,7 @@ export class CommunitiesManagement extends Component {
loading={loading}
maxDisplayedCommunities={MAX_COMMUNITIES}
branded={record.parent?.communities?.default}
recordRequests={recordRequests}
/>
<RecordCommunitiesListModal
id="record-communities-list-modal"
Expand All @@ -136,6 +138,7 @@ export class CommunitiesManagement extends Component {
recordCommunityEndpoint={recordCommunityEndpoint}
permissions={permissions}
recordParent={record.parent}
recordRequests={recordRequests}
/>

{!loading && communities?.length > MAX_COMMUNITIES && (
Expand Down Expand Up @@ -168,4 +171,9 @@ CommunitiesManagement.propTypes = {
userCommunitiesMemberships: PropTypes.object.isRequired,
searchConfig: PropTypes.object.isRequired,
record: PropTypes.object.isRequired,
recordRequests: PropTypes.object,
};

CommunitiesManagement.defaultProps = {
recordRequests: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ManageDefaultBrandingAction extends Component {
<Popup
trigger={
<Button
size="tiny"
size="mini"
labelPosition="left"
icon="paint brush"
floated="right"
Expand All @@ -81,7 +81,7 @@ export class ManageDefaultBrandingAction extends Component {
<Popup
trigger={
<Button
size="tiny"
size="mini"
labelPosition="left"
icon="paint brush"
floated="right"
Expand Down Expand Up @@ -111,7 +111,7 @@ export class ManageDefaultBrandingAction extends Component {
negative
floated="right"
className="community-branding-error"
size="tiny"
size="mini"
>
{error}
</Message>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {

export class RecordCommunitiesList extends Component {
render() {
const { communities, loading, error, maxDisplayedCommunities } = this.props;
const { communities, loading, error, maxDisplayedCommunities, recordRequests } =
this.props;
let Element = null;

if (loading) {
Expand Down Expand Up @@ -56,49 +57,71 @@ export class RecordCommunitiesList extends Component {
} else if (communities?.length > 0) {
const communityItems = communities
?.slice(0, maxDisplayedCommunities)
.map((community) => (
<Grid key={community.id}>
<Grid.Row verticalAlign="middle">
<Grid.Column width={2}>
<Image wrapped size="mini" src={community.links.logo} alt="" />
</Grid.Column>
<Grid.Column width={14}>
<Item.Content>
<Item.Header className="ui">
<Header as="a" href={community.links.self_html} size="small">
{community.metadata.title}
{/* Show the icon for communities allowing children, and for subcommunities */}
{(community.children?.allow ||
community.parent !== undefined) && (
<p className="ml-5 display-inline-block">
<Popup
content="Verified community"
trigger={
<Icon
size="small"
color="green"
name="check circle outline"
/>
}
position="top center"
/>
</p>
.map((community) => {
const viewRequest = community.id in recordRequests;
return (
<Grid key={community.id}>
<Grid.Row verticalAlign="middle">
<Grid.Column width={3}>
<Image wrapped size="mini" src={community.links.logo} alt="" />
</Grid.Column>
<Grid.Column width={13} className="pl-0">
<Item.Content>
<Item.Header className="ui">
<Header as="a" href={community.links.self_html} size="small">
{community.metadata.title}
{/* Show the icon for communities allowing children, and for subcommunities */}
{(community.children?.allow ||
community.parent !== undefined) && (
<p className="ml-5 display-inline-block">
<Popup
content="Verified community"
trigger={
<Icon
size="small"
color="green"
name="check circle outline"
/>
}
position="top center"
/>
</p>
)}
</Header>
{community.parent && (
<HeaderSubheader>
{i18next.t("Part of")}{" "}
<a href={`/communities/${community.parent.slug}`}>
{i18next.t(community.parent.metadata.title)}
</a>
</HeaderSubheader>
)}
</Header>
{community.parent && (
<HeaderSubheader>
{i18next.t("Part of")}{" "}
<a href={`/communities/${community.parent.slug}`}>
{i18next.t(community.parent.metadata.title)}
</a>
</HeaderSubheader>
)}
</Item.Header>
</Item.Content>
</Grid.Column>
</Grid.Row>
</Grid>
));
{viewRequest && (
<div>
<small>
<b>
<a
// building request link as the self_html of the request is
// /requests/<uuid> which doesn't resolve as missing
// /communities/ or /me/. We prefer /communities/ here
href={`${community.links.self_html}requests/${
recordRequests[community.id]
Comment on lines +107 to +108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would also need to propagate guest token here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can guests see requests? I thought guest tokens are for previewing a draft/viewing restricted files/editing a draft

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see the screenshot above ^^

}`}
>
<Icon name="discussions" className="mr-5" />
{i18next.t("View comments")}
</a>
</b>
</small>
</div>
)}
</Item.Header>
</Item.Content>
</Grid.Column>
</Grid.Row>
</Grid>
);
});

Element = (
<>
Expand All @@ -116,10 +139,12 @@ RecordCommunitiesList.propTypes = {
communities: PropTypes.array,
loading: PropTypes.bool,
error: PropTypes.string,
recordRequests: PropTypes.object,
};

RecordCommunitiesList.defaultProps = {
communities: undefined,
loading: false,
error: "",
recordRequests: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class RecordCommunitiesListModal extends Component {
handleOnClose,
trigger,
permissions,
recordRequests,
} = this.props;
const { recordParent } = this.state;

Expand Down Expand Up @@ -59,6 +60,7 @@ export class RecordCommunitiesListModal extends Component {
permissions={permissions}
recordParent={recordParent}
updateRecordCallback={this.handleRecordUpdate}
recordRequests={recordRequests}
/>

<Modal.Actions>
Expand All @@ -78,9 +80,11 @@ RecordCommunitiesListModal.propTypes = {
handleOnOpen: PropTypes.func.isRequired,
permissions: PropTypes.object.isRequired,
recordParent: PropTypes.object.isRequired,
recordRequests: PropTypes.object,
};

RecordCommunitiesListModal.defaultProps = {
modalOpen: false,
trigger: undefined,
recordRequests: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ export class RecordCommunitiesSearch extends Component {
};

render() {
const { recordCommunityEndpoint, permissions, recordParent, updateRecordCallback } =
this.props;
const {
recordCommunityEndpoint,
permissions,
recordParent,
updateRecordCallback,
recordRequests,
} = this.props;
const overriddenComponents = {
[`${appName}.ResultsList.item`]: parametrize(RecordCommunitiesSearchItem, {
recordCommunityEndpoint: recordCommunityEndpoint,
successCallback: this.handleSuccessCallback,
updateRecordCallback: updateRecordCallback,
permissions: permissions,
recordParent: recordParent,
recordRequests: recordRequests,
}),
};

Expand Down Expand Up @@ -95,4 +101,9 @@ RecordCommunitiesSearch.propTypes = {
updateRecordCallback: PropTypes.func.isRequired,
permissions: PropTypes.object.isRequired,
recordParent: PropTypes.object.isRequired,
recordRequests: PropTypes.object,
};

RecordCommunitiesSearch.defaultProps = {
recordRequests: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class RecordCommunitiesSearchItem extends Component {
recordCommunityEndpoint,
recordParent,
permissions: { can_manage: canManage },
recordRequests,
} = this.props;

const isCommunityDefault = recordParent?.communities?.default === result?.id;
Expand All @@ -36,6 +37,7 @@ export class RecordCommunitiesSearchItem extends Component {
actions={actions}
result={result}
isCommunityDefault={isCommunityDefault}
recordRequests={recordRequests}
/>
);
}
Expand All @@ -48,4 +50,9 @@ RecordCommunitiesSearchItem.propTypes = {
updateRecordCallback: PropTypes.func.isRequired,
permissions: PropTypes.object.isRequired,
recordParent: PropTypes.object.isRequired,
recordRequests: PropTypes.object,
};

RecordCommunitiesSearchItem.defaultProps = {
recordRequests: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class RemoveFromCommunityAction extends Component {
return (
<>
<Button
size="tiny"
size="mini"
negative
labelPosition="left"
icon="trash"
Expand Down
Loading
Loading