Skip to content

Commit 1d868b2

Browse files
committed
services: metadata input on zenodo deposit creation
* adds serializers/validation for metadata input * closes #1952 Signed-off-by: Ilias Koutsakis <[email protected]>
1 parent 32f7798 commit 1d868b2

File tree

6 files changed

+188
-10
lines changed

6 files changed

+188
-10
lines changed

cap/modules/deposit/api.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@
6161
from cap.modules.repos.tasks import download_repo, download_repo_file
6262
from cap.modules.repos.utils import (create_webhook, disconnect_subscriber,
6363
parse_git_url)
64+
from cap.modules.services.serializers.zenodo import ZenodoUploadSchema
6465
from cap.modules.schemas.resolvers import (resolve_schema_by_url,
6566
schema_name_to_url)
6667
from cap.modules.user.errors import DoesNotExistInLDAP
6768
from cap.modules.user.utils import (get_existing_or_register_role,
6869
get_existing_or_register_user)
6970

7071
from .errors import (DepositValidationError, UpdateDepositPermissionsError,
71-
ReviewError)
72+
ReviewError, InputValidationError)
7273
from .fetchers import cap_deposit_fetcher
7374
from .minters import cap_deposit_minter
7475
from .permissions import (AdminDepositPermission, CloneDepositPermission,
@@ -269,12 +270,22 @@ def upload(self, pid, *args, **kwargs):
269270
'Please connect your Zenodo account '
270271
'before creating a deposit.')
271272

272-
files = data.get('files')
273+
files = data.get('files', [])
273274
bucket = data.get('bucket')
274-
zenodo_data = data.get('zenodo_data', {})
275+
zenodo_data = data.get('zenodo_data')
276+
277+
input = {'files': files, 'bucket': bucket}
278+
if zenodo_data:
279+
input['data'] = zenodo_data
275280

276281
if files and bucket:
277-
zenodo_deposit = create_zenodo_deposit(token, zenodo_data) # noqa
282+
payload, errors = ZenodoUploadSchema().load(input)
283+
if errors:
284+
raise InputValidationError(
285+
'Validation error in Zenodo input data.',
286+
errors=errors)
287+
288+
zenodo_deposit = create_zenodo_deposit(token, payload)
278289
self.setdefault('_zenodo', []).append(zenodo_deposit)
279290
self.commit()
280291

cap/modules/deposit/errors.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ def __init__(self, description, errors=None, **kwargs):
138138
self.errors = [FieldError(e[0], e[1]) for e in errors.items()]
139139

140140

141+
class InputValidationError(RESTValidationError):
142+
"""Review validation error exception."""
143+
144+
code = 400
145+
146+
description = "Validation error. Try again with valid data"
147+
148+
def __init__(self, description, errors=None, **kwargs):
149+
"""Initialize exception."""
150+
super(InputValidationError, self).__init__(**kwargs)
151+
152+
self.description = description or self.description
153+
self.errors = [FieldError(e[0], e[1]) for e in errors.items()]
154+
155+
141156
class DataValidationError(RESTValidationError):
142157
"""Review validation error exception."""
143158

cap/modules/deposit/tasks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import requests
2929
from flask import current_app
3030
from celery import shared_task
31-
from invenio_db import db
3231
from invenio_files_rest.models import FileInstance, ObjectVersion
3332

3433

cap/modules/deposit/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ def add_api_to_links(links):
8282
return response
8383

8484

85-
def create_zenodo_deposit(token, data):
85+
def create_zenodo_deposit(token, data=None):
8686
"""Create a Zenodo deposit using the logged in user's credentials."""
8787
zenodo_url = current_app.config.get("ZENODO_SERVER_URL")
8888
deposit = requests.post(
8989
url=f'{zenodo_url}/deposit/depositions',
9090
params=dict(access_token=token),
91-
json={'metadata': data},
91+
json={'metadata': data} if data else {},
9292
headers={'Content-Type': 'application/json'}
9393
)
9494

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# This file is part of CERN Analysis Preservation Framework.
4+
# Copyright (C) 2020 CERN.
5+
#
6+
# CERN Analysis Preservation Framework is free software; you can redistribute
7+
# it and/or modify it under the terms of the GNU General Public License as
8+
# published by the Free Software Foundation; either version 2 of the
9+
# License, or (at your option) any later version.
10+
#
11+
# CERN Analysis Preservation Framework is distributed in the hope that it will
12+
# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with CERN Analysis Preservation Framework; if not, write to the
18+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
19+
# MA 02111-1307, USA.
20+
#
21+
# In applying this license, CERN does not
22+
# waive the privileges and immunities granted to it by virtue of its status
23+
# as an Intergovernmental Organization or submit itself to any jurisdiction.
24+
# or submit itself to any jurisdiction.
25+
26+
"""Zenodo Serializer/Validator."""
27+
28+
import arrow
29+
from marshmallow import Schema, fields, ValidationError, validate, validates, \
30+
validates_schema
31+
32+
from invenio_files_rest.models import ObjectVersion
33+
34+
DATE_REGEX = r'\d{4}-\d{2}-\d{2}'
35+
DATE_ERROR = 'The date should follow the pattern YYYY-mm-dd.'
36+
37+
UPLOAD_TYPES = [
38+
'publication',
39+
'poster',
40+
'presentation',
41+
'dataset',
42+
'image',
43+
'video',
44+
'software',
45+
'lesson',
46+
'physicalobject',
47+
'other'
48+
]
49+
LICENSES = [
50+
'CC-BY-4.0',
51+
'CC-BY-1.0',
52+
'CC-BY-2.0',
53+
'CC-BY-3.0'
54+
]
55+
ACCESS_RIGHTS = [
56+
'open',
57+
'embargoed',
58+
'restricted',
59+
'closed'
60+
]
61+
62+
63+
class ZenodoCreatorsSchema(Schema):
64+
name = fields.String(required=True)
65+
affiliation = fields.String()
66+
orcid = fields.String()
67+
68+
69+
class ZenodoDepositMetadataSchema(Schema):
70+
title = fields.String(required=True)
71+
description = fields.String(required=True)
72+
version = fields.String()
73+
74+
keywords = fields.List(fields.String())
75+
creators = fields.List(
76+
fields.Nested(ZenodoCreatorsSchema), required=True)
77+
78+
upload_type = fields.String(
79+
required=True, validate=validate.OneOf(UPLOAD_TYPES))
80+
license = fields.String(
81+
required=True, validate=validate.OneOf(LICENSES))
82+
access_right = fields.String(
83+
required=True, validate=validate.OneOf(ACCESS_RIGHTS))
84+
85+
publication_date = fields.String(
86+
required=True, validate=validate.Regexp(DATE_REGEX, error=DATE_ERROR))
87+
embargo_date = fields.String(
88+
validate=validate.Regexp(DATE_REGEX, error=DATE_ERROR))
89+
access_conditions = fields.String()
90+
91+
@validates('embargo_date')
92+
def validate_embargo_date(self, value):
93+
"""Validate that embargo date is in the future."""
94+
if arrow.get(value).date() <= arrow.utcnow().date():
95+
raise ValidationError(
96+
'Embargo date must be in the future.',
97+
field_names=['embargo_date']
98+
)
99+
100+
@validates_schema()
101+
def validate_license(self, data, **kwargs):
102+
"""Validate license."""
103+
access = data.get('access_right')
104+
if access in ['open', 'embargoed'] and 'license' not in data:
105+
raise ValidationError(
106+
'Required when access right is open or embargoed.',
107+
field_names=['license']
108+
)
109+
if access == 'embargoed' and 'embargo_date' not in data:
110+
raise ValidationError(
111+
'Required when access right is embargoed.',
112+
field_names=['embargo_date']
113+
)
114+
if access == 'restricted' and 'access_conditions' not in data:
115+
raise ValidationError(
116+
'Required when access right is restricted.',
117+
field_names=['access_conditions']
118+
)
119+
120+
121+
class ZenodoUploadSchema(Schema):
122+
files = fields.List(fields.String(), required=True)
123+
data = fields.Nested(ZenodoDepositMetadataSchema, default=dict())
124+
bucket = fields.String(required=True)
125+
126+
@validates_schema()
127+
def validate_files(self, data, **kwargs):
128+
bucket = data['bucket']
129+
files = data['files']
130+
131+
for _file in files:
132+
obj = ObjectVersion.get(bucket, _file)
133+
if not obj:
134+
raise ValidationError(
135+
f'File {_file} not found in bucket.',
136+
field_names=['files']
137+
)

tests/integration/test_zenodo_upload.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ def test_create_and_upload_to_zenodo_with_data(mock_token, app, users, deposit_w
173173
files=['test-file.txt'],
174174
zenodo_data={
175175
'title': 'test-title',
176-
'description': 'This is my first upload'
176+
'description': 'This is my first upload',
177+
'upload_type': 'poster',
178+
'creators': [
179+
{'name': 'User Tester', 'affiliation': 'Zenodo CAP'}
180+
],
181+
'access_right': 'open',
182+
'license': 'CC-BY-4.0',
183+
'publication_date': '2020-11-20',
184+
'embargo_date': '2050-09-09'
177185
})),
178186
headers=headers)
179187
assert resp.status_code == 201
@@ -218,8 +226,16 @@ def test_create_deposit_with_wrong_data(mock_token, app, users, deposit_with_fil
218226
zenodo_data={'test': 'test'})),
219227
headers=headers)
220228
assert resp.status_code == 400
221-
assert resp.json['message'] == 'Validation error on creating the Zenodo deposit.'
222-
assert resp.json['errors'] == [{'field': 'test', 'message': 'Unknown field name.'}]
229+
assert resp.json['message'] == 'Validation error in Zenodo input data.'
230+
assert resp.json['errors'][0]['message'] == {
231+
'license': ['Missing data for required field.'],
232+
'publication_date': ['Missing data for required field.'],
233+
'upload_type': ['Missing data for required field.'],
234+
'title': ['Missing data for required field.'],
235+
'access_right': ['Missing data for required field.'],
236+
'creators': ['Missing data for required field.'],
237+
'description': ['Missing data for required field.']
238+
}
223239

224240

225241
@patch('cap.modules.deposit.api._fetch_token', return_value='test-token')

0 commit comments

Comments
 (0)