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
28 changes: 21 additions & 7 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,22 @@ 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
self.require_permission(
# If config is true and there is only 1 communities left to remove
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
# Then, check for permissions to remove last community
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", record=record, community_id=community_id
)
if (
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
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()
# check permission here, per community: curator cannot remove another community
elif not can_remove_last_community:
ntarocco marked this conversation as resolved.
Show resolved Hide resolved
raise PermissionDeniedError("remove_community")

# Default community is deleted when the exact same community is removed from the record
record.parent.communities.remove(community_id)
Expand All @@ -233,7 +243,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
12 changes: 12 additions & 0 deletions invenio_rdm_records/services/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,15 @@ 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."


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

description = "Cannot publish without selecting a community."
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved


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

description = "Cannot remove. A record should be part of atleast 1 community."
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 22 additions & 0 deletions invenio_rdm_records/services/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,25 @@ def needs(self, request=None, **kwargs):
return [AccessRequestTokenNeed(request["payload"]["token"])]

return []


class IfOneCommunity(ConditionalGenerator):
"""Conditional generator for records always in communities case."""

def _condition(self, record=None, **kwargs):
"""Check if the record is associated with zero or one community."""
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
if record is None:
return True
rec_communities = record.parent.communities.ids
return len(rec_communities) == 1
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved


class IfAtleastOneCommunity(ConditionalGenerator):
"""Conditional generator for records always in communities case."""

def _condition(self, record=None, **kwargs):
"""Check if the record is associated with zero or one community."""
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
if record is None:
return True
rec_communities = record.parent.communities.ids
return len(rec_communities) > 0
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
28 changes: 26 additions & 2 deletions invenio_rdm_records/services/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
AccessGrant,
CommunityInclusionReviewers,
GuestAccessRequestToken,
IfAtleastOneCommunity,
IfCreate,
IfDeleted,
IfExternalDOIRecord,
IfFileIsLocal,
IfNewRecord,
IfOneCommunity,
IfRecordDeleted,
IfRequestType,
IfRestricted,
Expand Down Expand Up @@ -199,7 +201,17 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
),
]
# Allow publishing a new record or changes to an existing record.
can_publish = can_review
can_publish = [
sakshamarora1 marked this conversation as resolved.
Show resolved Hide resolved
IfConfig(
"RDM_RECORD_ALWAYS_IN_COMMUNITY",
then_=[
IfAtleastOneCommunity(
then_=can_review, else_=[Administration(), SystemProcess()]
)
],
else_=can_review,
)
]
# Allow lifting a record or draft.
can_lift_embargo = can_manage

Expand All @@ -209,11 +221,23 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
# Who can add record to a community
can_add_community = can_manage
# Who can remove a community from a record
can_remove_community = [
can_remove_community_ = [
RecordOwners(),
CommunityCurators(),
SystemProcess(),
]
can_remove_community = [
IfConfig(
"RDM_RECORD_ALWAYS_IN_COMMUNITY",
then_=[
IfOneCommunity(
then_=[Administration(), SystemProcess()],
else_=can_remove_community_,
ntarocco marked this conversation as resolved.
Show resolved Hide resolved
)
],
else_=can_remove_community_,
)
]
# Who can remove records from a community
can_remove_record = [CommunityCurators()]
# Who can add records to a community in bulk
Expand Down
24 changes: 24 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,28 @@ 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

# If config is true and there are no communities selected
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
# Then, check for permissions to upload without community
if (
is_community_required
and is_community_missing
and not self.check_permission(identity, "publish", record=draft)
):
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