Skip to content

Commit

Permalink
components: make content moderation configurable
Browse files Browse the repository at this point in the history
- Closes #1861.
- Adds a new `RRM_CONTENT_MODERATION_HANDLERS` config variable to allow
  for configuring multiple handlers for the different write actions.
  • Loading branch information
slint committed Nov 4, 2024
1 parent d20c082 commit a1d09c2
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 33 deletions.
13 changes: 13 additions & 0 deletions invenio_rdm_records/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
]
"""Records 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."""
Expand Down
27 changes: 2 additions & 25 deletions invenio_rdm_records/services/communities/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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):
Expand Down Expand Up @@ -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,
Expand All @@ -95,8 +72,8 @@ def create(self, identity, data=None, record=None, **kwargs):
OwnershipComponent,
FeaturedCommunityComponent,
OAISetComponent,
ContentModerationComponent,
CommunityDeletionComponent,
ChildrenComponent,
CommunityParentComponent,
ContentModerationComponent,
]
85 changes: 85 additions & 0 deletions invenio_rdm_records/services/communities/moderation.py
Original file line number Diff line number Diff line change
@@ -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
83 changes: 75 additions & 8 deletions invenio_rdm_records/services/components/verified.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,87 @@
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))
user_id = record.parent.access.owner.owner_id
uow.register(TaskOp(request_moderation, user_id=user_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
7 changes: 7 additions & 0 deletions tests/requests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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

0 comments on commit a1d09c2

Please sign in to comment.