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_COMMUNITY_REQUIRED_TO_PUBLISH = False
"""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,
CommunityRequiredError,
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,
)
),
CommunityRequiredError: create_error_handler(
HTTPJSONException(
code=400,
description=_("Cannot publish without selecting a community."),
)
),
}


Expand Down
30 changes: 21 additions & 9 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,20 @@ 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(
identity, "remove_community", record=record, community_id=community_id
)
try:
self.require_permission(
identity, "remove_community", record=record, community_id=community_id
)
# By default, admin/superuser has permission to do everything, so PermissionDeniedError won't be raised for admin in any case
except PermissionDeniedError as exc:
# If permission is denied, determine which error to raise, based on config
community_required = current_app.config["RDM_COMMUNITY_REQUIRED_TO_PUBLISH"]
is_last_community = len(record.parent.communities.ids) <= 1
if community_required and is_last_community:
raise CannotRemoveCommunityError()
else:
# If the config wasn't enabled, then raise the PermissionDeniedError
raise exc

# Default community is deleted when the exact same community is removed from the record
record.parent.communities.remove(community_id)
Expand All @@ -233,7 +241,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 CommunityRequiredError(Exception):
"""Error thrown when a record is being created/updated with less than 1 community."""

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


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

description = _("A record should be part of at least 1 community.")
16 changes: 16 additions & 0 deletions invenio_rdm_records/services/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,19 @@ 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 one community."""
return bool(record and len(record.parent.communities.ids) == 1)


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

def _condition(self, record=None, **kwargs):
"""Check if the record is associated with at least one community."""
return bool(record and record.parent.communities.ids)
31 changes: 28 additions & 3 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,18 @@ 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_COMMUNITY_REQUIRED_TO_PUBLISH",
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,13 +222,25 @@ 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_COMMUNITY_REQUIRED_TO_PUBLISH",
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()]
can_remove_record = [CommunityCurators(), Administration(), SystemProcess()]
# Who can add records to a community in bulk
can_bulk_add = [SystemProcess()]

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 (
CommunityRequiredError,
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()
"""
try:
draft = self.draft_cls.pid.resolve(id_, registered_only=False)
self.require_permission(identity, "publish", record=draft)
# By default, admin/superuser has permission to do everything, so PermissionDeniedError won't be raised for admin in any case
except PermissionDeniedError as exc:
# If user doesn't have permission to publish, determine which error to raise, based on config
community_required = current_app.config["RDM_COMMUNITY_REQUIRED_TO_PUBLISH"]
is_community_missing = len(draft.parent.communities.ids) < 1
if community_required and is_community_missing:
raise CommunityRequiredError()
else:
# If the config wasn't enabled, then raise the PermissionDeniedError
raise exc

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

#
# Search functions
#
Expand Down
19 changes: 11 additions & 8 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 @@ -2013,20 +2014,22 @@ def create_record(
"""Creates new record that belongs to the same community."""
# create draft
draft = current_rdm_records_service.create(uploader.identity, record_dict)
# publish and get record
result_item = current_rdm_records_service.publish(
uploader.identity, draft.id
)
record = result_item._record
record = draft._record
if community:
# 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
result_item = current_rdm_records_service.publish(
uploader.identity, draft.id
)
record = result_item._record
current_rdm_records_service.indexer.index(
record, arguments={"refresh": True}
)

return record

Expand Down
Loading