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

Services: Communities: Add feature to require community based on config #1813

Merged
6 changes: 6 additions & 0 deletions invenio_rdm_records/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ def always_valid(identifier):
RDM_ALLOW_RESTRICTED_RECORDS = True
"""Allow users to set restricted/private records."""

#
# Record communities
#
RDM_RECORD_ALWAYS_IN_COMMUNITY = False
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
"""Enforces at least one community per record."""

#
# Search configuration
#
Expand Down
8 changes: 8 additions & 0 deletions invenio_rdm_records/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from ..services.errors import (
AccessRequestExistsError,
CommunityNotSelectedError,
GrantExistsError,
InvalidAccessRestrictions,
RecordDeletedException,
Expand Down Expand Up @@ -187,6 +188,7 @@ class RDMRecordResourceConfig(RecordResourceConfig, ConfiguratorMixin):
)

error_handlers = {
**ErrorHandlersMixin.error_handlers,
DeserializerError: create_error_handler(
lambda exc: HTTPJSONException(
code=400,
Expand Down Expand Up @@ -255,6 +257,12 @@ class RDMRecordResourceConfig(RecordResourceConfig, ConfiguratorMixin):
description=e.description,
)
),
CommunityNotSelectedError: create_error_handler(
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
HTTPJSONException(
code=400,
description="Cannot publish without selecting a community.",
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
)
),
}


Expand Down
30 changes: 24 additions & 6 deletions invenio_rdm_records/services/communities/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
from flask import current_app
from invenio_access.permissions import system_identity
from invenio_communities.proxies import current_communities
from invenio_drafts_resources.services.records.uow import (
ParentRecordCommitOp,
RecordCommitOp,
)
from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
from invenio_i18n import lazy_gettext as _
from invenio_notifications.services.uow import NotificationOp
from invenio_pidstore.errors import PIDDoesNotExistError
Expand All @@ -38,6 +35,7 @@
from ...proxies import current_rdm_records, current_rdm_records_service
from ...requests import CommunityInclusion
from ..errors import (
CannotRemoveCommunityError,
CommunityAlreadyExists,
InvalidAccessRestrictions,
OpenRequestAlreadyExists,
Expand Down Expand Up @@ -205,10 +203,26 @@ def _remove(self, identity, community_id, record):
if community_id not in record.parent.communities.ids:
raise RecordCommunityMissing(record.id, community_id)

# check permission here, per community: curator cannot remove another community
# Check for permission to remove a community from a record
self.require_permission(
identity, "remove_community", record=record, community_id=community_id
)
is_community_required = current_app.config["RDM_RECORD_ALWAYS_IN_COMMUNITY"]
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
is_last_community = len(record.parent.communities.ids) == 1
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
# If community is required for a record and if it is the last community to remove
# Then, only users with special permissions can remove
can_remove_last_community = self.check_permission(
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
identity,
"remove_community_elevated",
record=record,
community_id=community_id,
)
if (
is_community_required
and is_last_community
and not can_remove_last_community
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
):
raise CannotRemoveCommunityError()

# Default community is deleted when the exact same community is removed from the record
record.parent.communities.remove(community_id)
Expand All @@ -233,7 +247,11 @@ def remove(self, identity, id_, data, uow):
try:
self._remove(identity, community_id, record)
processed.append({"community": community_id})
except (RecordCommunityMissing, PermissionDeniedError) as ex:
except (
RecordCommunityMissing,
PermissionDeniedError,
CannotRemoveCommunityError,
) as ex:
errors.append(
{
"community": community_id,
Expand Down
16 changes: 15 additions & 1 deletion invenio_rdm_records/services/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,18 @@ def description(self):
class RecordSubmissionClosedCommunityError(PermissionDenied):
"""Record submission policy forbids non-members from submitting records to community."""

description = "Submission to this community is only allowed to community members."
description = _(
"Submission to this community is only allowed to community members."
)


class CommunityNotSelectedError(Exception):
"""Error thrown when a record is being created/updated with less than 1 community."""

description = _("Cannot publish without selecting a community.")


class CannotRemoveCommunityError(Exception):
"""Error thrown when the last community is being removed from the record."""

description = _("Cannot remove. A record should be part of at least 1 community.")
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 7 additions & 1 deletion invenio_rdm_records/services/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
RecordOwners(),
RecordCommunitiesAction("curate"),
AccessGrant("manage"),
Administration(),
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
SystemProcess(),
]
can_curate = can_manage + [AccessGrant("edit"), SecretLinks("edit")]
Expand Down Expand Up @@ -200,6 +201,8 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
]
# Allow publishing a new record or changes to an existing record.
can_publish = can_review
# Permission to allow special users to publish a record in special cases
can_publish_elevated = [Administration(), SystemProcess()]
# Allow lifting a record or draft.
can_lift_embargo = can_manage

Expand All @@ -212,10 +215,13 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
can_remove_community = [
RecordOwners(),
CommunityCurators(),
Administration(),
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
SystemProcess(),
]
# Permission to allow special users to remove community in special cases
can_remove_community_elevated = [Administration(), SystemProcess()]
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
# Who can remove records from a community
can_remove_record = [CommunityCurators()]
can_remove_record = [CommunityCurators(), Administration()]
# Who can add records to a community in bulk
can_bulk_add = [SystemProcess()]

Expand Down
27 changes: 27 additions & 0 deletions invenio_rdm_records/services/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from invenio_drafts_resources.services.records import RecordService
from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
from invenio_records_resources.services import LinksTemplate, ServiceSchemaWrapper
from invenio_records_resources.services.errors import PermissionDeniedError
from invenio_records_resources.services.uow import (
RecordCommitOp,
RecordIndexDeleteOp,
Expand All @@ -37,6 +38,7 @@

from ..records.systemfields.deletion_status import RecordDeletionStatusEnum
from .errors import (
CommunityNotSelectedError,
DeletionStatusException,
EmbargoNotLiftedError,
RecordDeletedException,
Expand Down Expand Up @@ -404,6 +406,31 @@ def purge_record(self, identity, id_, uow=None):

raise NotImplementedError()

@unit_of_work()
def publish(self, identity, id_, uow=None, expand=False):
"""Publish a draft.

Check for permissions to publish a draft and then call invenio_drafts_resourcs.services.records.services.publish()
"""
# Get the draft
draft = self.draft_cls.pid.resolve(id_, registered_only=False)
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved

is_community_required = current_app.config["RDM_RECORD_ALWAYS_IN_COMMUNITY"]
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
is_community_missing = len(draft.parent.communities.ids) == 0
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
# If community is required for a record and there are no communities selected
# Then, check for permissions to publish without community
can_publish_without_community = self.check_permission(
identity, "publish_elevated", record=draft
)
if (
is_community_required
and is_community_missing
and not can_publish_without_community
):
raise CommunityNotSelectedError()

return super().publish(identity, id_, uow=uow, expand=expand)

#
# Search functions
#
Expand Down
36 changes: 36 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
from invenio_records_resources.proxies import current_service_registry
from invenio_records_resources.references.entity_resolvers import ServiceResultResolver
from invenio_records_resources.services.custom_fields import TextCF
from invenio_records_resources.services.uow import UnitOfWork
from invenio_requests.notifications.builders import (
CommentRequestEventCreateNotificationBuilder,
)
Expand Down Expand Up @@ -2080,6 +2081,41 @@ def create_record(
return RecordFactory()


@pytest.fixture()
def record_required_community(db, uploader, minimal_record, community):
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
"""Creates a record that belongs to a community before publishing."""

class Record:
"""Test record class."""

def create_record(
self,
record_dict=minimal_record,
uploader=uploader,
community=community,
):
"""Creates new record that belongs to the same community."""
# create draft
draft = current_rdm_records_service.create(uploader.identity, record_dict)
record = draft._record
# add the record to the community
community_record = community._record
record.parent.communities.add(community_record, default=False)
record.parent.commit()
db.session.commit()
current_rdm_records_service.indexer.index(
record, arguments={"refresh": True}
)

# publish and get record
community_record = current_rdm_records_service.publish(
uploader.identity, draft.id
)
return community_record

return Record()


@pytest.fixture(scope="session")
def headers():
"""Default headers for making requests."""
Expand Down
Loading