From 2ea61cded09ee58ffbb2eccaeb054b668a7ceff9 Mon Sep 17 00:00:00 2001 From: Alex Ioannidis Date: Wed, 30 Oct 2024 16:26:26 +0100 Subject: [PATCH] components: make content moderation configurable - Closes #1861. - Adds a new `RRM_CONTENT_MODERATION_HANDLERS` config variable to allow for configuring multiple handlers for the different write actions. --- invenio_rdm_records/config.py | 13 +++ .../services/communities/components.py | 27 +----- .../services/communities/moderation.py | 85 +++++++++++++++++++ .../services/components/verified.py | 84 ++++++++++++++++-- tests/requests/conftest.py | 7 ++ 5 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 invenio_rdm_records/services/communities/moderation.py diff --git a/invenio_rdm_records/config.py b/invenio_rdm_records/config.py index 53d634f22..afad58246 100644 --- a/invenio_rdm_records/config.py +++ b/invenio_rdm_records/config.py @@ -15,6 +15,9 @@ import idutils from invenio_i18n import lazy_gettext as _ +import invenio_rdm_records.services.communities.moderation as communities_moderation +from invenio_rdm_records.services.components.verified import UserModerationHandler + from . import tokens from .resources.serializers import DataCite43JSONSerializer from .services import facets @@ -555,6 +558,16 @@ def make_doi(prefix, record): def lock_edit_published_files(service, identity, record=None, draft=None): """ +RDM_CONTENT_MODERATION_HANDLERS = [ + UserModerationHandler(), +] +"""Content moderation handlers.""" + +RDM_COMMUNITY_CONTENT_MODERATION_HANDLERS = [ + communities_moderation.UserModerationHandler(), +] +"""Community content moderation handlers.""" + # Feature flag to enable/disable user moderation RDM_USER_MODERATION_ENABLED = False """Flag to enable creation of user moderation requests on specific user actions.""" diff --git a/invenio_rdm_records/services/communities/components.py b/invenio_rdm_records/services/communities/components.py index 0c9922258..8c2f4c5a9 100644 --- a/invenio_rdm_records/services/communities/components.py +++ b/invenio_rdm_records/services/communities/components.py @@ -7,8 +7,6 @@ """Record communities service components.""" -from flask import current_app -from invenio_access.permissions import system_identity from invenio_communities.communities.records.systemfields.access import VisibilityEnum from invenio_communities.communities.services.components import ChildrenComponent from invenio_communities.communities.services.components import ( @@ -24,18 +22,16 @@ OwnershipComponent, PIDComponent, ) -from invenio_drafts_resources.services.records.components import ServiceComponent from invenio_i18n import lazy_gettext as _ from invenio_records_resources.services.records.components import ( MetadataComponent, RelationsComponent, ) -from invenio_records_resources.services.uow import TaskOp -from invenio_requests.tasks import request_moderation from invenio_search.engine import dsl from ...proxies import current_community_records_service from ..errors import InvalidCommunityVisibility +from .moderation import ContentModerationComponent class CommunityAccessComponent(BaseAccessComponent): @@ -66,25 +62,6 @@ def update(self, identity, data=None, record=None, **kwargs): self._check_visibility(identity, record) -class ContentModerationComponent(ServiceComponent): - """Service component for content moderation.""" - - def create(self, identity, data=None, record=None, **kwargs): - """Create a moderation request if the user is not verified.""" - if current_app.config["RDM_USER_MODERATION_ENABLED"]: - # If the publisher is the system process, we don't want to create a moderation request. - # Even if the record being published is owned by a user that is not system - if identity == system_identity: - return - - # resolve current user and check if they are verified - is_verified = identity.user.verified_at is not None - - if not is_verified: - # Spawn a task to request moderation. - self.uow.register(TaskOp(request_moderation, user_id=identity.id)) - - CommunityServiceComponents = [ MetadataComponent, CommunityThemeComponent, @@ -95,8 +72,8 @@ def create(self, identity, data=None, record=None, **kwargs): OwnershipComponent, FeaturedCommunityComponent, OAISetComponent, - ContentModerationComponent, CommunityDeletionComponent, ChildrenComponent, CommunityParentComponent, + ContentModerationComponent, ] diff --git a/invenio_rdm_records/services/communities/moderation.py b/invenio_rdm_records/services/communities/moderation.py new file mode 100644 index 000000000..52e7e4342 --- /dev/null +++ b/invenio_rdm_records/services/communities/moderation.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-RDM-Records is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Content moderation for communities.""" + +from flask import current_app +from invenio_access.permissions import system_identity +from invenio_records_resources.services.records.components import ServiceComponent +from invenio_records_resources.services.uow import TaskOp +from invenio_requests.tasks import request_moderation + + +class BaseHandler: + """Base class for content moderation handlers.""" + + def create(self, identity, record=None, data=None, uow=None, **kwargs): + """Create handler.""" + pass + + def update(self, identity, record=None, data=None, uow=None, **kwargs): + """Update handler.""" + pass + + def delete(self, identity, data=None, record=None, uow=None, **kwargs): + """Delete handler.""" + pass + + +class UserModerationHandler(BaseHandler): + """Creates user moderation request if the user publishing is not verified.""" + + @property + def enabled(self): + """Check if user moderation is enabled.""" + return current_app.config["RDM_USER_MODERATION_ENABLED"] + + def run(self, identity, record=None, uow=None): + """Calculate the moderation score for a given record or draft.""" + if self.enabled: + # If the publisher is the system process, we don't want to create a moderation request. + # Even if the record being published is owned by a user that is not system + if identity == system_identity: + return + + # resolve current user and check if they are verified + is_verified = identity.user.verified_at is not None + if not is_verified: + # Spawn a task to request moderation. + self.uow.register(TaskOp(request_moderation, user_id=identity.id)) + + def create(self, identity, record=None, data=None, uow=None, **kwargs): + """Handle create.""" + self.run(identity, record=record, uow=uow) + + def update(self, identity, record=None, data=None, uow=None, **kwargs): + """Handle update.""" + self.run(identity, record=record, uow=uow) + + +class ContentModerationComponent(ServiceComponent): + """Service component for content moderation.""" + + def handler_for(action): + """Get the handlers for an action.""" + + def _handler_method(self, *args, **kwargs): + handlers = current_app.config.get( + "RDM_COMMUNITY_CONTENT_MODERATION_HANDLERS", [] + ) + for handler in handlers: + action_method = getattr(handler, action, None) + if action_method: + action_method(*args, **kwargs, uow=self.uow) + + return _handler_method + + create = handler_for("create") + update = handler_for("update") + delete = handler_for("delete") + + del handler_for diff --git a/invenio_rdm_records/services/components/verified.py b/invenio_rdm_records/services/components/verified.py index 65e123bae..1625520a2 100644 --- a/invenio_rdm_records/services/components/verified.py +++ b/invenio_rdm_records/services/components/verified.py @@ -13,20 +13,88 @@ from invenio_requests.tasks import request_moderation -class ContentModerationComponent(ServiceComponent): - """Service component for content moderation.""" +class BaseHandler: + """Base class for content moderation handlers.""" + + def update_draft( + self, identity, data=None, record=None, errors=None, uow=None, **kwargs + ): + """Update draft handler.""" + pass + + def delete_draft( + self, identity, draft=None, record=None, force=False, uow=None, **kwargs + ): + """Delete draft handler.""" + pass + + def edit(self, identity, draft=None, record=None, uow=None, **kwargs): + """Edit a record handler.""" + pass + + def new_version(self, identity, draft=None, record=None, uow=None, **kwargs): + """New version handler.""" + pass + + def publish(self, identity, draft=None, record=None, uow=None, **kwargs): + """Publish handler.""" + pass + + def post_publish( + self, identity, record=None, is_published=False, uow=None, **kwargs + ): + """Post publish handler.""" + pass + + +class UserModerationHandler(BaseHandler): + """Creates user moderation request if the user publishing is not verified.""" + + @property + def enabled(self): + """Check if user moderation is enabled.""" + return current_app.config["RDM_USER_MODERATION_ENABLED"] - def publish(self, identity, draft=None, record=None): - """Create a moderation request if the user is not verified.""" - if current_app.config["RDM_USER_MODERATION_ENABLED"]: + def run(self, identity, record=None, uow=None): + """Calculate the moderation score for a given record or draft.""" + if self.enabled: # If the publisher is the system process, we don't want to create a moderation request. # Even if the record being published is owned by a user that is not system if identity == system_identity: return is_verified = record.parent.is_verified - if not is_verified: # Spawn a task to request moderation. - owner_id = record.parent.access.owner.owner_id - self.uow.register(TaskOp(request_moderation, user_id=owner_id)) + uow.register( + TaskOp(request_moderation, record.parent.access.owner.owner_id) + ) + + def publish(self, identity, draft=None, record=None, uow=None, **kwargs): + """Handle publish.""" + self.run(identity, record=record, uow=uow) + + +class ContentModerationComponent(ServiceComponent): + """Service component for content moderation.""" + + def handler_for(action): + """Get the handlers for an action.""" + + def _handler_method(self, *args, **kwargs): + handlers = current_app.config.get("RDM_CONTENT_MODERATION_HANDLERS", []) + for handler in handlers: + action_method = getattr(handler, action, None) + if action_method: + action_method(*args, **kwargs, uow=self.uow) + + return _handler_method + + update_draft = handler_for("update_draft") + delete_draft = handler_for("delete_draft") + edit = handler_for("edit") + publish = handler_for("publish") + post_publish = handler_for("post_publish") + new_version = handler_for("new_version") + + del handler_for diff --git a/tests/requests/conftest.py b/tests/requests/conftest.py index 6a1248a63..b6638f452 100644 --- a/tests/requests/conftest.py +++ b/tests/requests/conftest.py @@ -15,6 +15,8 @@ import pytest from invenio_records_permissions.generators import AuthenticatedUser, SystemProcess +import invenio_rdm_records.services.communities.moderation as communities_moderation +from invenio_rdm_records.services.components.verified import UserModerationHandler from invenio_rdm_records.services.permissions import RDMRecordPermissionPolicy @@ -35,4 +37,9 @@ def app_config(app_config): app_config["RDM_PERMISSION_POLICY"] = CustomRDMRecordPermissionPolicy # Enable user moderation app_config["RDM_USER_MODERATION_ENABLED"] = True + # Enable content moderation handlers + app_config["RDM_CONTENT_MODERATION_HANDLERS"] = [UserModerationHandler()] + app_config["RDM_COMMUNITY_CONTENT_MODERATION_HANDLERS"] = [ + communities_moderation.UserModerationHandler(), + ] return app_config