Skip to content

Commit

Permalink
records: added verified field to record.
Browse files Browse the repository at this point in the history
  • Loading branch information
alejandromumo authored and ntarocco committed Aug 15, 2023
1 parent 15a164c commit ff1434d
Show file tree
Hide file tree
Showing 17 changed files with 248 additions and 3 deletions.
3 changes: 3 additions & 0 deletions invenio_rdm_records/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
)
from .systemfields import (
HasDraftCheckField,
IsVerifiedField,
ParentRecordAccessField,
RecordAccessField,
RecordDeletionStatusField,
Expand Down Expand Up @@ -87,6 +88,8 @@ class RDMParent(ParentRecordBase):

pids = DictField("pids")

is_verified = IsVerifiedField("is_verified")


#
# Common properties between records and drafts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"review": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"permission_flags": {
"type": "object",
"dynamic": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"review": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"communities": {
"properties": {
"ids": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"communities": {
"properties": {
"ids": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@
}
}
},
"is_verified": {
"type": "boolean"
},
"communities": {
"properties": {
"ids": {
Expand Down
2 changes: 2 additions & 0 deletions invenio_rdm_records/records/systemfields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
from .deletion_status import RecordDeletionStatusField
from .draft_status import DraftStatus
from .has_draftcheck import HasDraftCheckField
from .is_verified import IsVerifiedField
from .statistics import RecordStatisticsField
from .tombstone import TombstoneField

__all__ = (
"DraftStatus",
"HasDraftCheckField",
"IsVerifiedField",
"ParentRecordAccessField",
"RecordAccessField",
"RecordStatisticsField",
Expand Down
27 changes: 27 additions & 0 deletions invenio_rdm_records/records/systemfields/is_verified.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 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.
"""Record 'verified' system field."""

from invenio_records_resources.records.systemfields.calculated import CalculatedField


class IsVerifiedField(CalculatedField):
"""Systemfield for calculating whether or not the request is expired."""

def __init__(self, key=None):
"""Constructor."""
super().__init__(key=key, use_cache=False)

def calculate(self, record):
"""Calculate the ``is_expired`` property of the request."""
owner = record.access.owner
if not owner:
return False
is_verified = (
owner.resolve().verified_at is not None
) # TODO property is_verified in user
return is_verified
4 changes: 2 additions & 2 deletions invenio_rdm_records/requests/user_moderation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
# it under the terms of the MIT License; see LICENSE file for more details.
"""User moderation actions specific to RDM-Records."""

from .actions import on_block, on_restore
from .actions import on_approve, on_block, on_restore

__all__ = ("on_block", "on_restore")
__all__ = ("on_approve", "on_block", "on_restore")
14 changes: 14 additions & 0 deletions invenio_rdm_records/requests/user_moderation/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# it under the terms of the MIT License; see LICENSE file for more details.
"""RDM user moderation action."""

from invenio_access.permissions import system_identity

from invenio_rdm_records.proxies import current_rdm_records_service


def on_block(user_id, uow=None, **kwargs):
"""Removes records that belong to a user."""
Expand All @@ -15,3 +19,13 @@ def on_block(user_id, uow=None, **kwargs):
def on_restore(user_id, uow=None, **kwargs):
"""Restores records that belong to a user."""
pass


def on_approve(user_id, **kwargs):
"""Execute on user approve.
Re-index user records and dump verified field into records.
"""
current_rdm_records_service.reindex_user_records(
identity=system_identity, id_=user_id
)
1 change: 1 addition & 0 deletions invenio_rdm_records/services/schemas/parent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class RDMParentSchema(ParentSchema, FieldPermissionsMixin):
values=fields.Nested(PIDSchema),
dump_only=True,
)
is_verified = fields.Boolean(dump_only=True)

# TODO: move to a reusable place (taken from records-resources)
@pre_load
Expand Down
16 changes: 16 additions & 0 deletions invenio_rdm_records/services/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,19 @@ def oai_result_item(self, identity, oai_record_source):
record,
links_tpl=self.links_item_tpl,
)

@unit_of_work()
def reindex_user_records(self, identity, id_, uow=None):
"""Reindexes all the records from a given user."""
self.require_permission(identity, "manage")

user_records_q = f"parent.access.owned_by.user:{id_}"
params = {"allversions": True, "q": user_records_q}

records = self.scan(identity, params=params)

for record in records:
record, parent = self._get_record_and_parent_by_id(record["id"])
self._index_related_records(record, parent, uow=uow)

return True
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ invenio_oauth2server.scopes =
invenio_users_resources.moderation.actions =
block = invenio_rdm_records.requests.user_moderation.actions:on_block
restore = invenio_rdm_records.requests.user_moderation.actions:on_restore
approve = invenio_rdm_records.requests.user_moderation.actions:on_approve

[build_sphinx]
source-dir = docs/
Expand Down
62 changes: 61 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import pytest
from dateutil import tz
from flask import g
from flask_principal import Identity, Need, UserNeed
from flask_principal import Identity, Need, RoleNeed, UserNeed
from flask_security import login_user
from flask_security.utils import hash_password
from invenio_access.models import ActionRoles
Expand All @@ -67,6 +67,9 @@
CommentRequestEventCreateNotificationBuilder,
)
from invenio_users_resources.proxies import current_users_service
from invenio_requests.proxies import current_user_moderation_service as mod_service
from invenio_users_resources.permissions import user_management_action
from invenio_users_resources.records.api import UserAggregate
from invenio_users_resources.services.schemas import (
NotificationPreferences,
UserPreferencesSchema,
Expand Down Expand Up @@ -929,6 +932,63 @@ def parent(app, db):
return RDMParent.create({})


@pytest.fixture(scope="module")
def moderator_role(app, database):
"""Moderator role."""
REQUESTS_MODERATION_ROLE = app.config["REQUESTS_MODERATION_ROLE"]
mod_role = Role(name=REQUESTS_MODERATION_ROLE)
database.session.add(mod_role)

action_role = ActionRoles.create(action=user_management_action, role=mod_role)
database.session.add(action_role)
database.session.commit()
return mod_role


@pytest.fixture(scope="module")
def moderator_user(UserFixture, app, database, moderator_role):
"""Admin user for requests."""
u = UserFixture(
email="mod@example.org",
password=hash_password("password"),
active=True,
)
u.create(app, database)
u.user.roles.append(moderator_role)

database.session.commit()
UserAggregate.index.refresh()
return u


@pytest.fixture(scope="module")
def mod_identity(app, moderator_user):
"""Admin user for requests."""
idt = Identity(moderator_user.id)
REQUESTS_MODERATION_ROLE = app.config["REQUESTS_MODERATION_ROLE"]

# Add Role user_moderator
idt.provides.add(RoleNeed(REQUESTS_MODERATION_ROLE))
# Search requires user to be authenticated
idt.provides.add(Need(method="system_role", value="authenticated_user"))
return idt


@pytest.fixture
def mod_request_create(running_app, mod_identity):
"""Yields a fixture that encloses a function to create a moderation request."""

def _request(user_id):
"""Creates the request."""
request_item = mod_service.request_moderation(mod_identity, user_id=user_id)
assert request_item

return request_item

# Pass this closure to the test
yield _request


@pytest.fixture()
def users(app, db):
"""Create example user."""
Expand Down
63 changes: 63 additions & 0 deletions tests/requests/test_user_moderation_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# # -*- coding: utf-8 -*-
# #
# # Copyright (C) 2023 CERN.
# #
# # Invenio-RDM is free software; you can redistribute it and/or modify
# # it under the terms of the MIT License; see LICENSE file for more details.
"""Test user moderation actions."""

from invenio_access.permissions import system_identity
from invenio_requests.proxies import current_requests_service

from invenio_rdm_records.proxies import current_rdm_records_service as records_service


def fetch_user_records(user_id, allversions=False):
"""Fetches records from a given user."""
user_records_q = f"parent.access.owned_by.user:{user_id}"
return records_service.search(
system_identity, params={"q": user_records_q, "allversions": allversions}
)


def test_user_moderation_approve(
running_app,
mod_request_create,
mod_identity,
uploader,
test_user,
es_clear,
minimal_record,
):
"""Tests moderation action after user approval.
The user records, and all its versions, should have a "is_verified" field set to "True".
"""
# Create a record
draft = records_service.create(uploader.identity, minimal_record)
record = records_service.publish(id_=draft.id, identity=uploader.identity)

# Create a new version of the record
new_version = records_service.new_version(uploader.identity, id_=record.id)
records_service.update_draft(uploader.identity, new_version.id, minimal_record)
new_record = records_service.publish(identity=uploader.identity, id_=new_version.id)

pre_approval_records = fetch_user_records(uploader.id, allversions=True)
assert pre_approval_records.total == 2
hits = pre_approval_records.to_dict()["hits"]["hits"]
is_verified = all([hit["parent"]["is_verified"] for hit in hits])

assert is_verified == False

moderation_request = mod_request_create(uploader.id)

# Approve user, records are verified from now on
current_requests_service.execute_action(
mod_identity, id_=moderation_request.id, action="accept"
)

post_approval_records = fetch_user_records(uploader.id, allversions=True)
assert post_approval_records.total == 2
hits = post_approval_records.to_dict()["hits"]["hits"]
is_verified = all([hit["parent"]["is_verified"] for hit in hits])
assert is_verified == True
40 changes: 40 additions & 0 deletions tests/services/test_rdm_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""Service level tests for Invenio RDM Records."""

import pytest
from invenio_records_resources.services.errors import PermissionDeniedError

from invenio_rdm_records.proxies import current_rdm_records
from invenio_rdm_records.services.errors import EmbargoNotLiftedError
Expand Down Expand Up @@ -127,3 +128,42 @@ def test_embargo_lift_with_error(running_app, search_clear, minimal_record):
# Record should not be lifted since it didn't expire (until 3220)
with pytest.raises(EmbargoNotLiftedError):
service.lift_embargo(_id=record["id"], identity=superuser_identity)


def test_reindex_user_records(running_app, uploader, minimal_record, search_clear):
"""Tests reindexing of records belonging to a user.
Tests include:
- Reindexing does not fail and search results are the same
- Permissions - regular users can't trigger a reindex.
"""
service = current_rdm_records.records_service
superuser_identity = running_app.superuser_identity
uploader_identity = uploader.identity

# Create a record
draft = service.create(uploader_identity, minimal_record)
assert draft
record = service.publish(id_=draft.id, identity=uploader_identity)
assert record

user_records_q = f"parent.access.owned_by.user:{uploader.id}"
res = service.search(uploader_identity, params={"q": user_records_q})
assert res.total == 1

# Reindex records
reindex_success = service.reindex_user_records(
identity=superuser_identity, id_=uploader.id
)
assert reindex_success

user_records_q = f"parent.access.owned_by.user:{uploader.id}"
res = service.search(uploader_identity, params={"q": user_records_q})
assert res.total == 1

# Permissions
with pytest.raises(PermissionDeniedError):
# Regular users can't trigger a reindex
reindex_success = service.reindex_user_records(
identity=uploader_identity, id_=uploader.id
)

0 comments on commit ff1434d

Please sign in to comment.