oarepo-runtime 1.10.3__py3-none-any.whl → 2.0.0.dev4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oarepo_runtime/__init__.py +24 -0
- oarepo_runtime/api.py +210 -0
- oarepo_runtime/cli/__init__.py +10 -21
- oarepo_runtime/cli/search.py +34 -0
- oarepo_runtime/config.py +98 -13
- oarepo_runtime/ext.py +64 -82
- oarepo_runtime/proxies.py +21 -5
- oarepo_runtime/records/__init__.py +11 -50
- oarepo_runtime/records/drafts.py +24 -18
- oarepo_runtime/records/mapping.py +84 -0
- oarepo_runtime/records/pid_providers.py +43 -7
- oarepo_runtime/records/systemfields/__init__.py +15 -33
- oarepo_runtime/records/systemfields/mapping.py +41 -24
- oarepo_runtime/records/systemfields/publication_status.py +61 -0
- oarepo_runtime/services/__init__.py +12 -0
- oarepo_runtime/services/config/__init__.py +15 -21
- oarepo_runtime/services/config/link_conditions.py +69 -75
- oarepo_runtime/services/config/permissions.py +62 -0
- oarepo_runtime/services/facets/__init__.py +12 -33
- oarepo_runtime/services/facets/params.py +45 -110
- oarepo_runtime/services/records/__init__.py +14 -1
- oarepo_runtime/services/records/links.py +21 -11
- oarepo_runtime/services/records/mapping.py +42 -0
- oarepo_runtime/services/results.py +98 -109
- oarepo_runtime/services/schema/__init__.py +12 -44
- oarepo_runtime/services/schema/i18n.py +47 -22
- oarepo_runtime/services/schema/i18n_ui.py +61 -24
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/METADATA +10 -21
- oarepo_runtime-2.0.0.dev4.dist-info/RECORD +32 -0
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/WHEEL +1 -2
- oarepo_runtime-2.0.0.dev4.dist-info/entry_points.txt +5 -0
- oarepo_runtime/cli/assets.py +0 -145
- oarepo_runtime/cli/base.py +0 -25
- oarepo_runtime/cli/cf.py +0 -15
- oarepo_runtime/cli/check.py +0 -167
- oarepo_runtime/cli/configuration.py +0 -51
- oarepo_runtime/cli/fixtures.py +0 -167
- oarepo_runtime/cli/index.py +0 -272
- oarepo_runtime/cli/permissions/__init__.py +0 -6
- oarepo_runtime/cli/permissions/base.py +0 -26
- oarepo_runtime/cli/permissions/evaluate.py +0 -63
- oarepo_runtime/cli/permissions/list.py +0 -239
- oarepo_runtime/cli/permissions/search.py +0 -121
- oarepo_runtime/cli/validate.py +0 -150
- oarepo_runtime/datastreams/__init__.py +0 -38
- oarepo_runtime/datastreams/asynchronous.py +0 -247
- oarepo_runtime/datastreams/catalogue.py +0 -150
- oarepo_runtime/datastreams/datastreams.py +0 -152
- oarepo_runtime/datastreams/errors.py +0 -54
- oarepo_runtime/datastreams/ext.py +0 -41
- oarepo_runtime/datastreams/fixtures.py +0 -265
- oarepo_runtime/datastreams/json.py +0 -4
- oarepo_runtime/datastreams/readers/__init__.py +0 -39
- oarepo_runtime/datastreams/readers/attachments.py +0 -51
- oarepo_runtime/datastreams/readers/excel.py +0 -123
- oarepo_runtime/datastreams/readers/json.py +0 -27
- oarepo_runtime/datastreams/readers/service.py +0 -54
- oarepo_runtime/datastreams/readers/yaml.py +0 -14
- oarepo_runtime/datastreams/semi_asynchronous.py +0 -91
- oarepo_runtime/datastreams/synchronous.py +0 -70
- oarepo_runtime/datastreams/transformers.py +0 -18
- oarepo_runtime/datastreams/types.py +0 -323
- oarepo_runtime/datastreams/utils.py +0 -131
- oarepo_runtime/datastreams/writers/__init__.py +0 -21
- oarepo_runtime/datastreams/writers/attachments_file.py +0 -92
- oarepo_runtime/datastreams/writers/attachments_service.py +0 -118
- oarepo_runtime/datastreams/writers/publish.py +0 -70
- oarepo_runtime/datastreams/writers/service.py +0 -175
- oarepo_runtime/datastreams/writers/utils.py +0 -30
- oarepo_runtime/datastreams/writers/validation_errors.py +0 -20
- oarepo_runtime/datastreams/writers/yaml.py +0 -56
- oarepo_runtime/ext_config.py +0 -67
- oarepo_runtime/i18n/__init__.py +0 -3
- oarepo_runtime/info/__init__.py +0 -0
- oarepo_runtime/info/check.py +0 -95
- oarepo_runtime/info/permissions/__init__.py +0 -0
- oarepo_runtime/info/permissions/debug.py +0 -191
- oarepo_runtime/info/views.py +0 -586
- oarepo_runtime/profile.py +0 -60
- oarepo_runtime/records/dumpers/__init__.py +0 -8
- oarepo_runtime/records/dumpers/edtf_interval.py +0 -38
- oarepo_runtime/records/dumpers/multilingual_dumper.py +0 -34
- oarepo_runtime/records/entity_resolvers/__init__.py +0 -13
- oarepo_runtime/records/entity_resolvers/proxies.py +0 -57
- oarepo_runtime/records/mappings/__init__.py +0 -0
- oarepo_runtime/records/mappings/rdm_parent_mapping.json +0 -483
- oarepo_runtime/records/owners/__init__.py +0 -3
- oarepo_runtime/records/owners/registry.py +0 -22
- oarepo_runtime/records/relations/__init__.py +0 -22
- oarepo_runtime/records/relations/base.py +0 -296
- oarepo_runtime/records/relations/internal.py +0 -46
- oarepo_runtime/records/relations/lookup.py +0 -28
- oarepo_runtime/records/relations/pid_relation.py +0 -102
- oarepo_runtime/records/systemfields/featured_file.py +0 -45
- oarepo_runtime/records/systemfields/has_draftcheck.py +0 -47
- oarepo_runtime/records/systemfields/icu.py +0 -371
- oarepo_runtime/records/systemfields/owner.py +0 -115
- oarepo_runtime/records/systemfields/record_status.py +0 -35
- oarepo_runtime/records/systemfields/selectors.py +0 -98
- oarepo_runtime/records/systemfields/synthetic.py +0 -130
- oarepo_runtime/resources/__init__.py +0 -4
- oarepo_runtime/resources/config.py +0 -12
- oarepo_runtime/resources/file_resource.py +0 -15
- oarepo_runtime/resources/json_serializer.py +0 -27
- oarepo_runtime/resources/localized_ui_json_serializer.py +0 -54
- oarepo_runtime/resources/resource.py +0 -53
- oarepo_runtime/resources/responses.py +0 -20
- oarepo_runtime/services/components.py +0 -429
- oarepo_runtime/services/config/draft_link.py +0 -23
- oarepo_runtime/services/config/permissions_presets.py +0 -174
- oarepo_runtime/services/config/service.py +0 -117
- oarepo_runtime/services/custom_fields/__init__.py +0 -80
- oarepo_runtime/services/custom_fields/mappings.py +0 -188
- oarepo_runtime/services/entity/__init__.py +0 -0
- oarepo_runtime/services/entity/config.py +0 -14
- oarepo_runtime/services/entity/schema.py +0 -9
- oarepo_runtime/services/entity/service.py +0 -48
- oarepo_runtime/services/expansions/__init__.py +0 -0
- oarepo_runtime/services/expansions/expandable_fields.py +0 -21
- oarepo_runtime/services/expansions/service.py +0 -4
- oarepo_runtime/services/facets/base.py +0 -12
- oarepo_runtime/services/facets/date.py +0 -72
- oarepo_runtime/services/facets/enum.py +0 -11
- oarepo_runtime/services/facets/facet_groups_names.py +0 -17
- oarepo_runtime/services/facets/max_facet.py +0 -13
- oarepo_runtime/services/facets/multilingual_facet.py +0 -33
- oarepo_runtime/services/facets/nested_facet.py +0 -32
- oarepo_runtime/services/facets/year_histogram.py +0 -200
- oarepo_runtime/services/files/__init__.py +0 -8
- oarepo_runtime/services/files/components.py +0 -62
- oarepo_runtime/services/files/service.py +0 -16
- oarepo_runtime/services/generators.py +0 -10
- oarepo_runtime/services/permissions/__init__.py +0 -3
- oarepo_runtime/services/permissions/generators.py +0 -103
- oarepo_runtime/services/relations/__init__.py +0 -0
- oarepo_runtime/services/relations/components.py +0 -15
- oarepo_runtime/services/relations/errors.py +0 -18
- oarepo_runtime/services/relations/mapping.py +0 -38
- oarepo_runtime/services/schema/cf.py +0 -13
- oarepo_runtime/services/schema/i18n_validation.py +0 -7
- oarepo_runtime/services/schema/marshmallow.py +0 -44
- oarepo_runtime/services/schema/marshmallow_to_json_schema.py +0 -72
- oarepo_runtime/services/schema/oneofschema.py +0 -192
- oarepo_runtime/services/schema/polymorphic.py +0 -21
- oarepo_runtime/services/schema/rdm.py +0 -146
- oarepo_runtime/services/schema/rdm_ui.py +0 -156
- oarepo_runtime/services/schema/ui.py +0 -251
- oarepo_runtime/services/schema/validation.py +0 -70
- oarepo_runtime/services/search.py +0 -282
- oarepo_runtime/services/service.py +0 -61
- oarepo_runtime/tasks.py +0 -6
- oarepo_runtime/translations/cs/LC_MESSAGES/messages.mo +0 -0
- oarepo_runtime/translations/cs/LC_MESSAGES/messages.po +0 -95
- oarepo_runtime/translations/default_translations.py +0 -6
- oarepo_runtime/translations/en/LC_MESSAGES/messages.mo +0 -0
- oarepo_runtime/translations/en/LC_MESSAGES/messages.po +0 -97
- oarepo_runtime/translations/messages.pot +0 -100
- oarepo_runtime/uow.py +0 -146
- oarepo_runtime/utils/__init__.py +0 -0
- oarepo_runtime/utils/functools.py +0 -37
- oarepo_runtime/utils/identity_utils.py +0 -35
- oarepo_runtime/utils/index.py +0 -11
- oarepo_runtime/utils/path.py +0 -97
- oarepo_runtime-1.10.3.dist-info/RECORD +0 -163
- oarepo_runtime-1.10.3.dist-info/entry_points.txt +0 -16
- oarepo_runtime-1.10.3.dist-info/top_level.txt +0 -2
- tests/marshmallow_to_json/__init__.py +0 -0
- tests/marshmallow_to_json/test_datacite_ui_schema.py +0 -1410
- tests/marshmallow_to_json/test_simple_schema.py +0 -52
- tests/pkg_data/__init__.py +0 -0
- {oarepo_runtime-1.10.3.dist-info → oarepo_runtime-2.0.0.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -1,251 +0,0 @@
|
|
1
|
-
import datetime
|
2
|
-
import re
|
3
|
-
|
4
|
-
import marshmallow as ma
|
5
|
-
from babel.dates import format_date
|
6
|
-
from babel_edtf import format_edtf
|
7
|
-
from flask import current_app
|
8
|
-
from idutils import to_url
|
9
|
-
from invenio_rdm_records.records.systemfields.access.field.record import (
|
10
|
-
AccessStatusEnum,
|
11
|
-
)
|
12
|
-
from invenio_rdm_records.resources.serializers.ui.fields import (
|
13
|
-
UIObjectAccessStatus as InvenioUIObjectAccessStatus,
|
14
|
-
)
|
15
|
-
from invenio_rdm_records.services.schemas.parent import RDMParentSchema
|
16
|
-
from invenio_rdm_records.services.schemas.pids import PIDSchema
|
17
|
-
from invenio_rdm_records.services.schemas.record import validate_scheme
|
18
|
-
from invenio_rdm_records.services.schemas.versions import VersionsSchema
|
19
|
-
from marshmallow.fields import Dict, Nested, Raw
|
20
|
-
from marshmallow_utils.fields import (
|
21
|
-
BabelGettextDictField,
|
22
|
-
FormatDate,
|
23
|
-
FormatDatetime,
|
24
|
-
FormatEDTF,
|
25
|
-
FormatTime,
|
26
|
-
SanitizedUnicode,
|
27
|
-
)
|
28
|
-
from marshmallow_utils.fields.babel import BabelFormatField
|
29
|
-
|
30
|
-
from oarepo_runtime.i18n import gettext
|
31
|
-
from oarepo_runtime.i18n import lazy_gettext as _
|
32
|
-
|
33
|
-
from .marshmallow import RDMBaseRecordSchema
|
34
|
-
|
35
|
-
|
36
|
-
def current_default_locale():
|
37
|
-
"""Get the Flask app's default locale."""
|
38
|
-
if current_app:
|
39
|
-
return current_app.config.get("BABEL_DEFAULT_LOCALE", "en")
|
40
|
-
# Use english by default if not specified
|
41
|
-
return "en"
|
42
|
-
|
43
|
-
|
44
|
-
class LocalizedMixin:
|
45
|
-
def __init__(self, *args, locale=None, **kwargs):
|
46
|
-
super().__init__(*args, locale=locale, **kwargs)
|
47
|
-
|
48
|
-
@property
|
49
|
-
def locale(self):
|
50
|
-
if self._locale:
|
51
|
-
return self._locale
|
52
|
-
if self.parent:
|
53
|
-
if "locale" in self.context:
|
54
|
-
return self.context["locale"]
|
55
|
-
return current_default_locale()
|
56
|
-
|
57
|
-
def format_value(self, value):
|
58
|
-
"""Format the value, gracefully handling exceptions."""
|
59
|
-
try:
|
60
|
-
return super().format_value(value)
|
61
|
-
except Exception as e:
|
62
|
-
# Handle the exception gracefully
|
63
|
-
current_app.logger.error(f"Error formatting value '{value}': {e}")
|
64
|
-
return f"«Error formatting value '{value}'»"
|
65
|
-
|
66
|
-
|
67
|
-
# localized date field
|
68
|
-
class LocalizedDate(LocalizedMixin, FormatDate):
|
69
|
-
pass
|
70
|
-
|
71
|
-
|
72
|
-
class FormatTimeString(FormatTime):
|
73
|
-
def parse(self, value, as_time=False, as_date=False, as_datetime=False):
|
74
|
-
if value and isinstance(value, str) and as_time == True:
|
75
|
-
match = re.match(
|
76
|
-
r"^(\d|0\d|1\d|2[0-3]):(\d|[0-5]\d|60)(:(\d|[0-5]\d|60))?$", value
|
77
|
-
)
|
78
|
-
if match:
|
79
|
-
value = datetime.time(
|
80
|
-
hour=int(match.group(1)),
|
81
|
-
minute=int(match.group(2)),
|
82
|
-
second=int(match.group(4)) if match.group(4) else 0,
|
83
|
-
)
|
84
|
-
|
85
|
-
return super().parse(value, as_time, as_date, as_datetime)
|
86
|
-
|
87
|
-
|
88
|
-
class MultilayerFormatEDTF(BabelFormatField):
|
89
|
-
def format_value(self, value):
|
90
|
-
try:
|
91
|
-
return format_date(
|
92
|
-
self.parse(value, as_date=True), format=self._format, locale=self.locale
|
93
|
-
)
|
94
|
-
except:
|
95
|
-
return format_edtf(value, format=self._format, locale=self.locale)
|
96
|
-
|
97
|
-
def parse(self, value, **kwargs):
|
98
|
-
# standard parsing is too lenient, for example returns "2000-01-01" for input "2000"
|
99
|
-
if re.match("^[0-9]+-[0-9]+-[0-9]+", value):
|
100
|
-
return super().parse(value, **kwargs)
|
101
|
-
raise ValueError("Not a valid date")
|
102
|
-
|
103
|
-
class TimezoneMixin: #i'm not sure about where this should be used
|
104
|
-
@property
|
105
|
-
def tzinfo(self):
|
106
|
-
from oarepo_runtime.proxies import current_timezone
|
107
|
-
try:
|
108
|
-
return current_timezone.get()
|
109
|
-
except LookupError:
|
110
|
-
return
|
111
|
-
|
112
|
-
class LocalizedDateTime(TimezoneMixin, LocalizedMixin, FormatDatetime):
|
113
|
-
pass
|
114
|
-
|
115
|
-
class LocalizedTime(LocalizedMixin, FormatTimeString):
|
116
|
-
pass
|
117
|
-
|
118
|
-
|
119
|
-
class LocalizedEDTF(LocalizedMixin, MultilayerFormatEDTF):
|
120
|
-
pass
|
121
|
-
|
122
|
-
|
123
|
-
class LocalizedEDTFTime(LocalizedMixin, MultilayerFormatEDTF):
|
124
|
-
pass
|
125
|
-
|
126
|
-
|
127
|
-
class LocalizedEDTFInterval(LocalizedMixin, FormatEDTF):
|
128
|
-
pass
|
129
|
-
|
130
|
-
|
131
|
-
class LocalizedEDTFTimeInterval(LocalizedMixin, FormatEDTF):
|
132
|
-
pass
|
133
|
-
|
134
|
-
|
135
|
-
class PrefixedGettextField(BabelGettextDictField):
|
136
|
-
def __init__(self, *, value_prefix, locale, default_locale, **kwargs):
|
137
|
-
super().__init__(locale, default_locale, **kwargs)
|
138
|
-
self.value_prefix = value_prefix
|
139
|
-
|
140
|
-
def _serialize(self, value, attr, obj, **kwargs):
|
141
|
-
if value:
|
142
|
-
value = f"{self.value_prefix}{value}"
|
143
|
-
return gettext(value)
|
144
|
-
|
145
|
-
|
146
|
-
class LocalizedEnum(LocalizedMixin, PrefixedGettextField):
|
147
|
-
pass
|
148
|
-
|
149
|
-
def __init__(self, **kwargs):
|
150
|
-
super().__init__(default_locale=current_default_locale, **kwargs)
|
151
|
-
|
152
|
-
|
153
|
-
if False: # NOSONAR
|
154
|
-
# just for the makemessages to pick up the translations
|
155
|
-
translations = [_("True"), _("False")]
|
156
|
-
|
157
|
-
|
158
|
-
class InvenioUISchema(ma.Schema):
|
159
|
-
_schema = ma.fields.Str(attribute="$schema", data_key="$schema")
|
160
|
-
id = ma.fields.Str()
|
161
|
-
created = LocalizedDateTime(dump_only=True)
|
162
|
-
updated = LocalizedDateTime(dump_only=True)
|
163
|
-
links = ma.fields.Raw(dump_only=True)
|
164
|
-
revision_id = ma.fields.Integer(dump_only=True)
|
165
|
-
expanded = ma.fields.Raw(dump_only=True)
|
166
|
-
|
167
|
-
|
168
|
-
# seems not possible to avoid, as they have this hardcoded in their object,
|
169
|
-
# and translation keys are i.e. open, which gets translated to otevret
|
170
|
-
class UIObjectAccessStatus(InvenioUIObjectAccessStatus):
|
171
|
-
@property
|
172
|
-
def title(self):
|
173
|
-
"""Access status title."""
|
174
|
-
return {
|
175
|
-
AccessStatusEnum.OPEN: _("access.status.open"),
|
176
|
-
AccessStatusEnum.EMBARGOED: _("access.status.embargoed"),
|
177
|
-
AccessStatusEnum.RESTRICTED: _("access.status.restricted"),
|
178
|
-
AccessStatusEnum.METADATA_ONLY: _("access.status.metadata-only"),
|
179
|
-
}.get(self.access_status)
|
180
|
-
|
181
|
-
|
182
|
-
class AccessStatusField(ma.fields.Field):
|
183
|
-
"""Record access status."""
|
184
|
-
|
185
|
-
def _serialize(self, value, attr, obj, **kwargs):
|
186
|
-
"""Serialise access status."""
|
187
|
-
record_access_dict = obj.get("access")
|
188
|
-
_files = obj.get("files", {})
|
189
|
-
has_files = _files is not None and _files.get("enabled", False)
|
190
|
-
if record_access_dict:
|
191
|
-
record_access_status_ui = UIObjectAccessStatus(
|
192
|
-
record_access_dict, has_files
|
193
|
-
)
|
194
|
-
return {
|
195
|
-
"id": record_access_status_ui.id,
|
196
|
-
"title_l10n": record_access_status_ui.title,
|
197
|
-
"description_l10n": record_access_status_ui.description,
|
198
|
-
"icon": record_access_status_ui.icon,
|
199
|
-
"embargo_date_l10n": record_access_status_ui.embargo_date,
|
200
|
-
"message_class": record_access_status_ui.message_class,
|
201
|
-
}
|
202
|
-
|
203
|
-
|
204
|
-
# to be able to have access to entire pids object
|
205
|
-
class PIDsField(Dict):
|
206
|
-
"""Custom Dict field for PIDs that adds URLs after serialization."""
|
207
|
-
|
208
|
-
def _serialize(self, value, attr, obj, **kwargs):
|
209
|
-
"""Serialize the PIDs and add URLs to them."""
|
210
|
-
serialized = super()._serialize(value, attr, obj, **kwargs)
|
211
|
-
|
212
|
-
if serialized:
|
213
|
-
for scheme, pid in serialized.items():
|
214
|
-
if scheme and pid and isinstance(pid, dict) and pid.get("identifier"):
|
215
|
-
url = to_url(pid["identifier"], scheme.lower(), url_scheme="https")
|
216
|
-
if url:
|
217
|
-
pid["url"] = url
|
218
|
-
|
219
|
-
return serialized
|
220
|
-
|
221
|
-
|
222
|
-
class InvenioRDMParentUISchema(RDMParentSchema):
|
223
|
-
"""Parent schema."""
|
224
|
-
|
225
|
-
pids = PIDsField(
|
226
|
-
keys=SanitizedUnicode(validate=validate_scheme),
|
227
|
-
values=Nested(PIDSchema),
|
228
|
-
)
|
229
|
-
|
230
|
-
|
231
|
-
class InvenioRDMUISchema(InvenioUISchema, RDMBaseRecordSchema):
|
232
|
-
"""RDM UI schema."""
|
233
|
-
|
234
|
-
is_draft = ma.fields.Boolean(dump_only=True)
|
235
|
-
access_status = AccessStatusField(attribute="access", dump_only=True)
|
236
|
-
versions = ma.fields.Nested(VersionsSchema, dump_only=True)
|
237
|
-
pids = PIDsField(
|
238
|
-
keys=SanitizedUnicode(validate=validate_scheme),
|
239
|
-
values=Nested(PIDSchema),
|
240
|
-
)
|
241
|
-
parent = ma.fields.Nested(InvenioRDMParentUISchema)
|
242
|
-
access = ma.fields.Raw(attribute="access", data_key="access", dump_only=True)
|
243
|
-
files = ma.fields.Raw(attribute="files", data_key="files", dump_only=True)
|
244
|
-
|
245
|
-
def hide_tombstone(self, data):
|
246
|
-
"""Hide tombstone info if the record isn't deleted and metadata if it is."""
|
247
|
-
return data
|
248
|
-
|
249
|
-
def default_nested(self, data):
|
250
|
-
"""Serialize fields as empty dict for partial drafts."""
|
251
|
-
return data
|
@@ -1,70 +0,0 @@
|
|
1
|
-
import functools
|
2
|
-
import re
|
3
|
-
from datetime import datetime
|
4
|
-
from idutils import normalize_pid
|
5
|
-
from isbnlib import canonical, mask
|
6
|
-
|
7
|
-
from marshmallow.exceptions import ValidationError
|
8
|
-
from marshmallow_utils.fields.edtfdatestring import EDTFValidator
|
9
|
-
|
10
|
-
from invenio_i18n import gettext as _
|
11
|
-
|
12
|
-
|
13
|
-
def validate_identifier(value):
|
14
|
-
try:
|
15
|
-
original_identifier = (value["identifier"] or '').strip()
|
16
|
-
normalized_identifier = normalize_pid(
|
17
|
-
value["identifier"], value["scheme"].lower()
|
18
|
-
)
|
19
|
-
if original_identifier and not normalized_identifier:
|
20
|
-
# the normalize_pid library has problems with isbn - does not raise an exception
|
21
|
-
# but returns an empty string
|
22
|
-
raise ValueError()
|
23
|
-
|
24
|
-
# normalized_pid is changing from 10 length ISBN to 13 length ISBN
|
25
|
-
if value["scheme"].lower() == "isbn":
|
26
|
-
canonical_isbn = canonical(value["identifier"])
|
27
|
-
if original_identifier and not canonical_isbn: # just check in case it returns empty string
|
28
|
-
raise ValueError()
|
29
|
-
value["identifier"] = mask(canonical_isbn)
|
30
|
-
return value
|
31
|
-
|
32
|
-
value["identifier"] = normalized_identifier
|
33
|
-
except:
|
34
|
-
raise ValidationError({
|
35
|
-
"identifier": _("Invalid value %(identifier)s of identifier type %(type)s") % {"identifier": value['identifier'], "type": value['scheme']}
|
36
|
-
})
|
37
|
-
return value
|
38
|
-
|
39
|
-
|
40
|
-
def validate_date(date_format):
|
41
|
-
def validate(value):
|
42
|
-
try:
|
43
|
-
datetime.strptime(value, date_format)
|
44
|
-
except Exception as e:
|
45
|
-
raise ValidationError(
|
46
|
-
f"Invalid date/time format, expecting {date_format}, got {value}"
|
47
|
-
) from e
|
48
|
-
|
49
|
-
return validate
|
50
|
-
|
51
|
-
|
52
|
-
def validate_datetime(value):
|
53
|
-
try:
|
54
|
-
datetime.fromisoformat(value)
|
55
|
-
except Exception as e:
|
56
|
-
raise ValidationError(
|
57
|
-
f"Invalid datetime format, expecting iso format, got {value}"
|
58
|
-
) from e
|
59
|
-
|
60
|
-
|
61
|
-
class CachedMultilayerEDTFValidator(EDTFValidator):
|
62
|
-
@functools.lru_cache(maxsize=1024)
|
63
|
-
def __call__(self, value):
|
64
|
-
if re.match(r"^\d{4}$", value):
|
65
|
-
return value
|
66
|
-
try:
|
67
|
-
datetime.strptime(value, "%Y-%m-%d")
|
68
|
-
return value
|
69
|
-
except:
|
70
|
-
return super().__call__(value)
|
@@ -1,282 +0,0 @@
|
|
1
|
-
import dataclasses
|
2
|
-
import inspect
|
3
|
-
from typing import List
|
4
|
-
|
5
|
-
from invenio_rdm_records.services.config import (
|
6
|
-
RDMSearchDraftsOptions as BaseRDMSearchDraftsOptions,
|
7
|
-
)
|
8
|
-
from invenio_rdm_records.services.config import RDMSearchOptions as BaseRDMSearchOptions
|
9
|
-
from invenio_records_resources.proxies import current_service_registry
|
10
|
-
from invenio_records_resources.services.records import (
|
11
|
-
SearchOptions as InvenioSearchOptions,
|
12
|
-
)
|
13
|
-
from invenio_drafts_resources.services.records.config import SearchDraftsOptions as InvenioSearchDraftsOptions
|
14
|
-
from invenio_records_resources.services.records.params import (
|
15
|
-
FacetsParam,
|
16
|
-
PaginationParam,
|
17
|
-
QueryStrParam,
|
18
|
-
SortParam,
|
19
|
-
)
|
20
|
-
from invenio_records_resources.services.records.queryparser import SuggestQueryParser
|
21
|
-
from invenio_search.engine import dsl
|
22
|
-
from invenio_drafts_resources.services.records.search_params import AllVersionsParam
|
23
|
-
# TODO: integrate this to invenio_records_resources.services.records and remove SearchOptions class
|
24
|
-
from oarepo_runtime.i18n import lazy_gettext as _
|
25
|
-
from oarepo_runtime.records.systemfields.icu import ICUSuggestField
|
26
|
-
from oarepo_runtime.utils.functools import class_property
|
27
|
-
|
28
|
-
from .facets.params import GroupedFacetsParam, OARepoAllVersionsParam, OARepoPublishedRecordsParam
|
29
|
-
from invenio_drafts_resources.services.records.search_params import AllVersionsParam
|
30
|
-
from invenio_rdm_records.services.search_params import PublishedRecordsParam
|
31
|
-
from functools import partial
|
32
|
-
try:
|
33
|
-
from invenio_i18n import get_locale
|
34
|
-
except ImportError:
|
35
|
-
from invenio_i18n.babel import get_locale
|
36
|
-
|
37
|
-
|
38
|
-
class FuzzySuggestQueryParser(SuggestQueryParser):
|
39
|
-
def __init__(self, identity=None, extra_params=None, **kwargs):
|
40
|
-
"""Constructor."""
|
41
|
-
super().__init__(identity=identity, extra_params=extra_params)
|
42
|
-
self.fields = self.extra_params.get("fields", [])
|
43
|
-
self.extra_params.setdefault("type", "bool_prefix")
|
44
|
-
|
45
|
-
def parse(self, query_str):
|
46
|
-
"""Parse the query."""
|
47
|
-
# default behavior
|
48
|
-
multi_match_with_bool_prefix = dsl.Q(
|
49
|
-
"multi_match", query=query_str, **self.extra_params
|
50
|
-
)
|
51
|
-
# fuzziness does not seem to work with bool_prefix multimatch query, so we turn this into
|
52
|
-
# should multi match query with two clauses
|
53
|
-
multi_match_fuzzy = dsl.Q(
|
54
|
-
"multi_match", query=query_str, fields=self.fields, fuzziness="AUTO"
|
55
|
-
)
|
56
|
-
return dsl.Q("bool", should=[multi_match_with_bool_prefix, multi_match_fuzzy])
|
57
|
-
|
58
|
-
|
59
|
-
class SearchOptionsMixin:
|
60
|
-
@class_property
|
61
|
-
def params_interpreters_cls(cls):
|
62
|
-
"""Replaces FacetsParam with GroupedFacetsParam."""
|
63
|
-
params_replace_map = {FacetsParam: GroupedFacetsParam, AllVersionsParam:
|
64
|
-
OARepoAllVersionsParam.factory(["versions.is_latest", "versions.is_latest_draft"]),
|
65
|
-
PublishedRecordsParam: OARepoPublishedRecordsParam}
|
66
|
-
|
67
|
-
param_interpreters = [*super(SearchOptionsMixin, cls).params_interpreters_cls]
|
68
|
-
# replace FacetsParam with GroupedFacetsParam
|
69
|
-
for idx, interpreter in enumerate(param_interpreters):
|
70
|
-
if interpreter in params_replace_map:
|
71
|
-
param_interpreters[idx] = params_replace_map[interpreter]
|
72
|
-
elif isinstance(interpreter, partial):
|
73
|
-
fn = interpreter.func
|
74
|
-
if fn in params_replace_map:
|
75
|
-
param_interpreters[idx] = params_replace_map[fn]
|
76
|
-
return param_interpreters
|
77
|
-
|
78
|
-
sort_options = {
|
79
|
-
"title": dict(
|
80
|
-
title=_("By Title"),
|
81
|
-
fields=["metadata.title"], # ES defaults to desc on `_score` field
|
82
|
-
),
|
83
|
-
"bestmatch": dict(
|
84
|
-
title=_("Best match"),
|
85
|
-
fields=["_score"], # ES defaults to desc on `_score` field
|
86
|
-
),
|
87
|
-
"newest": dict(
|
88
|
-
title=_("Newest"),
|
89
|
-
fields=["-created"],
|
90
|
-
),
|
91
|
-
"oldest": dict(
|
92
|
-
title=_("Oldest"),
|
93
|
-
fields=["created"],
|
94
|
-
),
|
95
|
-
}
|
96
|
-
|
97
|
-
|
98
|
-
class SearchOptionsDraftMixin(SearchOptionsMixin):
|
99
|
-
sort_options = {
|
100
|
-
"bestmatch": dict(
|
101
|
-
title=_("Best match"),
|
102
|
-
fields=["_score"], # search defaults to desc on `_score` field
|
103
|
-
),
|
104
|
-
"updated-desc": dict(
|
105
|
-
title=_("Recently updated"),
|
106
|
-
fields=["-updated"],
|
107
|
-
),
|
108
|
-
"updated-asc": dict(
|
109
|
-
title=_("Least recently updated"),
|
110
|
-
fields=["updated"],
|
111
|
-
),
|
112
|
-
"newest": dict(
|
113
|
-
title=_("Newest"),
|
114
|
-
fields=["-created"],
|
115
|
-
),
|
116
|
-
"oldest": dict(
|
117
|
-
title=_("Oldest"),
|
118
|
-
fields=["created"],
|
119
|
-
),
|
120
|
-
"version": dict(
|
121
|
-
title=_("Version"),
|
122
|
-
fields=["-versions.index"],
|
123
|
-
),
|
124
|
-
}
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
class SearchOptions(SearchOptionsMixin, InvenioSearchOptions):
|
129
|
-
# TODO: should be changed
|
130
|
-
params_interpreters_cls = [
|
131
|
-
QueryStrParam,
|
132
|
-
PaginationParam,
|
133
|
-
SortParam,
|
134
|
-
GroupedFacetsParam,
|
135
|
-
]
|
136
|
-
|
137
|
-
class SearchDraftsOptions(SearchOptionsMixin, InvenioSearchDraftsOptions):
|
138
|
-
# TODO: should be changed
|
139
|
-
params_interpreters_cls = [
|
140
|
-
QueryStrParam,
|
141
|
-
PaginationParam,
|
142
|
-
SortParam,
|
143
|
-
GroupedFacetsParam,
|
144
|
-
AllVersionsParam.factory("versions.is_latest_draft")
|
145
|
-
]
|
146
|
-
|
147
|
-
|
148
|
-
class RDMSearchOptions(SearchOptionsMixin, BaseRDMSearchOptions):
|
149
|
-
pass
|
150
|
-
|
151
|
-
|
152
|
-
class RDMSearchDraftsOptions(SearchOptionsDraftMixin, BaseRDMSearchDraftsOptions):
|
153
|
-
pass
|
154
|
-
|
155
|
-
|
156
|
-
@dataclasses.dataclass
|
157
|
-
class SuggestField:
|
158
|
-
field: str
|
159
|
-
boost: int
|
160
|
-
use_ngrams: bool = True
|
161
|
-
boost_exact: float = 5
|
162
|
-
boost_2gram: float = 1
|
163
|
-
boost_3gram: float = 1
|
164
|
-
boost_prefix: float = 1
|
165
|
-
|
166
|
-
|
167
|
-
class ICUSuggestParser:
|
168
|
-
def __init__(
|
169
|
-
self,
|
170
|
-
record_cls_or_service_name,
|
171
|
-
extra_fields: List[SuggestField] = None,
|
172
|
-
default_fields: List[SuggestField] = None,
|
173
|
-
):
|
174
|
-
self.record_cls_or_service_name = record_cls_or_service_name
|
175
|
-
self.extra_fields = extra_fields or []
|
176
|
-
self.default_fields = default_fields or []
|
177
|
-
|
178
|
-
@property
|
179
|
-
def record_cls(self):
|
180
|
-
if not isinstance(self.record_cls_or_service_name, str):
|
181
|
-
return self.record_cls_or_service_name
|
182
|
-
return current_service_registry.get(
|
183
|
-
self.record_cls_or_service_name
|
184
|
-
).config.record_cls
|
185
|
-
|
186
|
-
def __get__(self, instance, owner):
|
187
|
-
search_as_you_type_fields: List[SuggestField] = []
|
188
|
-
locale = get_locale()
|
189
|
-
if locale:
|
190
|
-
language = locale.language
|
191
|
-
|
192
|
-
for fld_name, fld in inspect.getmembers(
|
193
|
-
self.record_cls, lambda x: isinstance(x, ICUSuggestField)
|
194
|
-
):
|
195
|
-
search_as_you_type_fields.append(
|
196
|
-
SuggestField(f"{fld_name}.{language}.original", 2)
|
197
|
-
)
|
198
|
-
search_as_you_type_fields.append(
|
199
|
-
SuggestField(f"{fld_name}.{language}.no_accent", 1)
|
200
|
-
)
|
201
|
-
|
202
|
-
if not search_as_you_type_fields:
|
203
|
-
search_as_you_type_fields.extend(self.default_fields)
|
204
|
-
|
205
|
-
search_as_you_type_fields.extend(self.extra_fields)
|
206
|
-
|
207
|
-
fields = []
|
208
|
-
for fld in search_as_you_type_fields:
|
209
|
-
fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
|
210
|
-
if fld.use_ngrams:
|
211
|
-
fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
|
212
|
-
fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
|
213
|
-
fields.append(
|
214
|
-
f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
|
215
|
-
)
|
216
|
-
|
217
|
-
return FuzzySuggestQueryParser.factory(fields=fields)
|
218
|
-
|
219
|
-
|
220
|
-
@dataclasses.dataclass
|
221
|
-
class SortField:
|
222
|
-
option_name: str = "title"
|
223
|
-
icu_sort_field: str = "sort"
|
224
|
-
title: str = _("By Title")
|
225
|
-
|
226
|
-
|
227
|
-
class ICUSortOptions:
|
228
|
-
def __init__(self, record_cls_or_service_name, fields: List[SortField] = None):
|
229
|
-
self.record_cls_or_service_name = record_cls_or_service_name
|
230
|
-
self.fields = fields or [SortField()]
|
231
|
-
|
232
|
-
@property
|
233
|
-
def record_cls(self):
|
234
|
-
if not isinstance(self.record_cls_or_service_name, str):
|
235
|
-
return self.record_cls_or_service_name
|
236
|
-
return current_service_registry.get(
|
237
|
-
self.record_cls_or_service_name
|
238
|
-
).config.record_cls
|
239
|
-
|
240
|
-
def __get__(self, instance, owner):
|
241
|
-
if not inspect.isclass(owner):
|
242
|
-
owner = type(owner)
|
243
|
-
super_options = {}
|
244
|
-
for mro in list(owner.mro())[1:]:
|
245
|
-
if hasattr(mro, "sort_options"):
|
246
|
-
super_options = mro.sort_options
|
247
|
-
break
|
248
|
-
|
249
|
-
ret = {
|
250
|
-
**super_options,
|
251
|
-
**getattr(owner, "extra_sort_options", {}),
|
252
|
-
}
|
253
|
-
|
254
|
-
# transform the sort options by the current language
|
255
|
-
locale = get_locale()
|
256
|
-
if not locale:
|
257
|
-
return ret
|
258
|
-
|
259
|
-
language = locale.language
|
260
|
-
|
261
|
-
for sort_field in self.fields:
|
262
|
-
icu_field = getattr(self.record_cls, sort_field.icu_sort_field)
|
263
|
-
ret[sort_field.option_name] = {
|
264
|
-
"title": sort_field.title,
|
265
|
-
"fields": [f"{icu_field.key}.{language}"],
|
266
|
-
}
|
267
|
-
return ret
|
268
|
-
|
269
|
-
|
270
|
-
class I18nSearchOptions(SearchOptions):
|
271
|
-
extra_sort_options = {}
|
272
|
-
record_cls = None
|
273
|
-
|
274
|
-
|
275
|
-
class I18nRDMSearchOptions(RDMSearchOptions):
|
276
|
-
extra_sort_options = {}
|
277
|
-
record_cls = None
|
278
|
-
|
279
|
-
|
280
|
-
class I18nRDMDraftsSearchOptions(RDMSearchDraftsOptions):
|
281
|
-
extra_sort_options = {}
|
282
|
-
record_cls = None
|
@@ -1,61 +0,0 @@
|
|
1
|
-
"""An extension to invenio RDM service that includes a search across all records
|
2
|
-
(published, draft and deleted).
|
3
|
-
without any filtering. Permission `can_read_all_records` and `search_all_records`
|
4
|
-
were added and must be used to filter the results so that no information is leaked.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from invenio_rdm_records.services import RDMRecordService
|
8
|
-
from invenio_records_resources.services import LinksTemplate
|
9
|
-
|
10
|
-
|
11
|
-
class SearchAllRecordsService(RDMRecordService):
|
12
|
-
def search_all_records(
|
13
|
-
self,
|
14
|
-
identity,
|
15
|
-
params=None,
|
16
|
-
search_preference=None,
|
17
|
-
expand=False,
|
18
|
-
extra_filter=None,
|
19
|
-
**kwargs,
|
20
|
-
):
|
21
|
-
"""Search for drafts records matching the querystring."""
|
22
|
-
self.require_permission(
|
23
|
-
identity,
|
24
|
-
"search_all_records",
|
25
|
-
params=params,
|
26
|
-
extra_filter=extra_filter,
|
27
|
-
**kwargs,
|
28
|
-
)
|
29
|
-
# Prepare and execute the search
|
30
|
-
params = params or {}
|
31
|
-
|
32
|
-
search_opts = (
|
33
|
-
getattr(self.config, "search_all", None) or self.config.search
|
34
|
-
)
|
35
|
-
|
36
|
-
search_result = self._search(
|
37
|
-
"search_all",
|
38
|
-
identity,
|
39
|
-
params,
|
40
|
-
search_preference,
|
41
|
-
record_cls=self.draft_cls,
|
42
|
-
search_opts=search_opts,
|
43
|
-
extra_filter=extra_filter,
|
44
|
-
permission_action="read_all_records",
|
45
|
-
**kwargs,
|
46
|
-
).execute()
|
47
|
-
|
48
|
-
return self.result_list(
|
49
|
-
self,
|
50
|
-
identity,
|
51
|
-
search_result,
|
52
|
-
params,
|
53
|
-
links_tpl=LinksTemplate(
|
54
|
-
getattr(self.config, "links_search_drafts")
|
55
|
-
or self.config.links_search_drafts,
|
56
|
-
context={"args": params},
|
57
|
-
),
|
58
|
-
links_item_tpl=self.links_item_tpl,
|
59
|
-
expandable_fields=self.expandable_fields,
|
60
|
-
expand=expand,
|
61
|
-
)
|
oarepo_runtime/tasks.py
DELETED
Binary file
|