Skip to content

Commit

Permalink
record: add internal_notes
Browse files Browse the repository at this point in the history
* components: add internal notes cmp
  • Loading branch information
kpsherva committed Nov 5, 2024
1 parent 3d975a6 commit 556d2f7
Show file tree
Hide file tree
Showing 13 changed files with 379 additions and 46 deletions.
20 changes: 20 additions & 0 deletions invenio_rdm_records/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@
from datetime import timedelta

import idutils
from invenio_administration.permissions import administration_permission
from invenio_i18n import lazy_gettext as _
from invenio_records_resources.services.records.queryparser import QueryParser
from invenio_records_resources.services.records.queryparser.transformer import (
RestrictedTerm,
SearchFieldTransformer,
)

from . import tokens
from .resources.serializers import DataCite43JSONSerializer
from .services import facets
from .services.config import lock_edit_published_files
from .services.permissions import RDMRecordPermissionPolicy
from .services.pids import providers
from .services.queryparser import word_internal_notes

# Invenio-RDM-Records
# ===================
Expand Down Expand Up @@ -251,6 +258,7 @@ def always_valid(identifier):
"""


RDM_SEARCH = {
"facets": ["access_status", "file_type", "resource_type"],
"sort": [
Expand All @@ -261,6 +269,18 @@ def always_valid(identifier):
"mostviewed",
"mostdownloaded",
],
"query_parser_cls": QueryParser.factory(
mapping={
"internal_notes.note": RestrictedTerm(administration_permission),
"internal_notes.id": RestrictedTerm(administration_permission),
"internal_notes.added_by": RestrictedTerm(administration_permission),
"internal_notes.timestamp": RestrictedTerm(administration_permission),
"_exists_": RestrictedTerm(
administration_permission, word=word_internal_notes
),
},
tree_transformer_cls=SearchFieldTransformer,
),
}
"""Record search configuration.
Expand Down
34 changes: 18 additions & 16 deletions invenio_rdm_records/records/jsonschemas/records/record-v6.0.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,22 +341,6 @@
}
}
}
},
"_internal_notes": {
"properties": {
"note": {
"type": "string"
},
"timestamp": {
"type": "string",
"description": "ISO8601 formatted timestamp in UTC.",
"format": "date-time"
},
"user": {
"type": "string",
"description": "User id of the person who created the note"
}
}
}
}
},
Expand Down Expand Up @@ -395,6 +379,24 @@
}
}
},
"internal_notes": {
"properties": {
"id": {
"type": "string"
},
"note": {
"type": "string"
},
"timestamp": {
"type": "string",
"description": "ISO8601 formatted timestamp in UTC.",
"format": "date-time"
},
"added_by": {
"$ref": "local://records/definitions-v2.0.0.json#/agent"
}
}
},
"provenance": {
"type": "object",
"description": "Record provenance.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,28 @@
"type": "boolean"
}
}
},
"internal_notes": {
"type": "object",
"properties": {
"id": {
"type": "keyword"
},
"note": {
"type": "text"
},
"timestamp": {
"type": "date"
},
"added_by": {
"type": "object",
"properties": {
"user": {
"type": "keyword"
}
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,13 @@
"accent_analyzer": {
"tokenizer": "standard",
"type": "custom",
"char_filter": ["strip_special_chars"],
"filter": ["lowercase", "asciifolding"]
"char_filter": [
"strip_special_chars"
],
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
Expand Down Expand Up @@ -703,19 +708,6 @@
"type": "object",
"enabled": false
},
"_internal_notes": {
"properties": {
"note": {
"type": "text"
},
"timestamp": {
"type": "date"
},
"user": {
"type": "keyword"
}
}
},
"contact": {
"type": "keyword"
},
Expand Down Expand Up @@ -1595,6 +1587,28 @@
"type": "boolean"
}
}
},
"internal_notes": {
"type": "object",
"properties": {
"id": {
"type": "keyword"
},
"note": {
"type": "text"
},
"timestamp": {
"type": "date"
},
"added_by": {
"type": "object",
"properties": {
"user": {
"type": "keyword"
}
}
}
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions invenio_rdm_records/services/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from .access import AccessComponent
from .custom_fields import CustomFieldsComponent
from .internal_notes import InternalNotesComponent
from .metadata import MetadataComponent
from .pids import ParentPIDsComponent, PIDsComponent
from .record_deletion import RecordDeletionComponent
Expand All @@ -40,6 +41,7 @@
RelationsComponent,
ReviewComponent,
ContentModerationComponent,
InternalNotesComponent,
]


Expand Down
64 changes: 64 additions & 0 deletions invenio_rdm_records/services/components/internal_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- 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.

"""RDM service component for custom fields."""
from copy import copy, deepcopy
from datetime import datetime, timezone

from invenio_pidstore.providers.recordid_v2 import RecordIdProviderV2

from .metadata import MetadataComponent


class InternalNotesComponent(MetadataComponent):
"""Service component for custom fields."""

field = "internal_notes"
new_version_skip_fields = []

def create(self, identity, data=None, record=None, **kwargs):
"""Inject note to the record."""
notes = data.get(self.field, [])
for note in notes:
note.setdefault("added_by", {"user": identity.id})
note.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
note.setdefault("id", RecordIdProviderV2.generate_id())
record.update({"internal_notes": notes})

def update_draft(self, identity, data=None, record=None, **kwargs):
"""Inject parsed metadata to the record."""
notes_updated = data.get(self.field, [])
notes = deepcopy(record.get(self.field, []))
existing_ids = set([note["id"] for note in notes])
new_ids = set([note["id"] for note in notes_updated])
to_delete = existing_ids - new_ids

if sorted(existing_ids) != sorted(new_ids):
for note in notes_updated:
if note["id"] in existing_ids:
notes_updated.remove(note)
continue
note.setdefault("added_by", {"user": identity.id})
note.setdefault("timestamp", datetime.now(timezone.utc).isoformat())
note.setdefault("id", RecordIdProviderV2.generate_id())
for note in notes:
if note["id"] in to_delete:
notes.remove(note)

record.update({"internal_notes": notes + notes_updated})

def publish(self, identity, draft=None, record=None, **kwargs):
"""Update draft metadata."""
record.update({"internal_notes": draft.get(self.field, [])})

def edit(self, identity, draft=None, record=None, **kwargs):
"""Update draft metadata."""
draft.update({"internal_notes": record.get(self.field, [])})

def new_version(self, identity, draft=None, record=None, **kwargs):
"""Update draft metadata."""
draft.update({"internal_notes": copy(record.get(self.field, []))})
13 changes: 13 additions & 0 deletions invenio_rdm_records/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pathlib import Path

from flask import current_app
from invenio_administration.permissions import administration_permission
from invenio_communities.communities.records.api import Community
from invenio_drafts_resources.services.records.components import (
DraftMediaFilesComponent,
Expand Down Expand Up @@ -64,6 +65,13 @@
PaginationParam,
QueryStrParam,
)
from invenio_records_resources.services.records.queryparser import (
QueryParser,
SearchFieldTransformer,
)
from invenio_records_resources.services.records.queryparser.transformer import (
RestrictedTerm,
)
from invenio_requests.services.requests import RequestItem, RequestList
from invenio_requests.services.requests.config import RequestSearchOptions
from requests import Request
Expand Down Expand Up @@ -231,6 +239,11 @@ class RDMSearchOptions(SearchOptions, SearchOptionsMixin):
MetricsParam,
]

query_parser_cls = QueryParser.factory(
mapping={"internal_notes.*": RestrictedTerm(administration_permission)},
tree_transformer_cls=SearchFieldTransformer,
)


class RDMSearchDraftsOptions(SearchDraftsOptions, SearchOptionsMixin):
"""Search options for drafts search."""
Expand Down
3 changes: 3 additions & 0 deletions invenio_rdm_records/services/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class RDMRecordPermissionPolicy(RecordPermissionPolicy):
"object-read": "read_files",
}

# permission meant for global curators of the instance
# (for now applies to internal notes field only
can_manage_internal = [Administration()]
#
# High-level permissions (used by low-level)
#
Expand Down
16 changes: 16 additions & 0 deletions invenio_rdm_records/services/queryparser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- 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.

"""Query Parser module for InvenioRdmRecords."""
from luqum.tree import Word


def word_internal_notes(node):
"""Quote DOIs."""
if not node.value.startswith("internal_notes"):
return node
return Word(" ")
11 changes: 1 addition & 10 deletions invenio_rdm_records/services/schemas/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# it under the terms of the MIT License; see LICENSE file for more details.

"""RDM record schemas."""
from datetime import timezone
from functools import partial
from urllib import parse

Expand All @@ -35,7 +34,7 @@
EDTFDateTimeString,
IdentifierSet,
SanitizedHTML,
SanitizedUnicode, TZDateTime,
SanitizedUnicode,
)
from marshmallow_utils.schemas import GeometryObjectSchema, IdentifierSchema
from werkzeug.local import LocalProxy
Expand Down Expand Up @@ -353,13 +352,6 @@ class FeatureSchema(Schema):
features = fields.List(fields.Nested(LocationSchema))


class InternalNoteSchema(Schema):
"""Schema for internal notes."""
timestamp = TZDateTime(timezone=timezone.utc, format="iso", dump_only=True)
user = fields.Nested(Agent, dump_only=True)
note = SanitizedUnicode()


class MetadataSchema(Schema):
"""Schema for the record metadata."""

Expand Down Expand Up @@ -398,4 +390,3 @@ class MetadataSchema(Schema):
locations = fields.Nested(FeatureSchema)
funding = fields.List(fields.Nested(FundingSchema))
references = fields.List(fields.Nested(ReferenceSchema))
_internal_notes = fields.List(fields.Nested(InternalNoteSchema))
Loading

0 comments on commit 556d2f7

Please sign in to comment.