-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathworkflow.py
More file actions
337 lines (285 loc) · 13 KB
/
Copy pathworkflow.py
File metadata and controls
337 lines (285 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import logging
import jsonschema
import requests
from django.db import transaction
from django.utils import timezone
from django.core.files.base import ContentFile
from rest_framework import viewsets, views, status
from rest_framework.response import Response
from rest_framework import filters
from rest_framework.exceptions import ParseError
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rest_framework.serializers import ValidationError
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from share.models import RawDatum, NormalizedData, Source, SourceConfig, Transformer, ShareUser
from share.tasks import disambiguate
from share.harvest.serialization import DictSerializer
from share.harvest.base import FetchResult
from share.util import IDObfuscator
from api import schemas
from api.pagination import CursorPagination
from api.authentication import APIV1TokenBackPortAuthentication
from api.permissions import ReadOnlyOrTokenHasScopeOrIsAuthenticated
from api.serializers import FullNormalizedDataSerializer, BasicNormalizedDataSerializer, RawDatumSerializer, ShareUserSerializer, SourceSerializer
logger = logging.getLogger(__name__)
__all__ = ('NormalizedDataViewSet', 'RawDatumViewSet', 'ShareUserViewSet', 'SourceViewSet', 'V1DataView')
class ShareUserViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns details about the currently logged in user
"""
serializer_class = ShareUserSerializer
def get_queryset(self):
return ShareUser.objects.filter(pk=self.request.user.pk)
class SourceViewSet(viewsets.ReadOnlyModelViewSet):
filter_backends = (filters.OrderingFilter, )
ordering = ('id', )
ordering_fields = ('long_title', )
serializer_class = SourceSerializer
permission_classes = [DjangoModelPermissionsOrAnonReadOnly, ]
queryset = Source.objects.none() # Required for DjangoModelPermissions
VALID_IMAGE_TYPES = ('image/png', 'image/jpeg')
def get_queryset(self):
return Source.objects.exclude(icon='').exclude(is_deleted=True)
def create(self, request, *args, **kwargs):
try:
long_title = request.data['long_title']
icon = request.data['icon']
except KeyError as e:
raise ValidationError('{} is a required attribute.'.format(e))
try:
r = requests.get(icon, timeout=5)
header_type = r.headers['content-type'].split(';')[0].lower()
if header_type not in self.VALID_IMAGE_TYPES:
raise ValidationError('Invalid image type.')
icon_file = ContentFile(r.content)
except Exception as e:
logger.warning('Exception occured while downloading icon %s', e)
raise ValidationError('Could not download/process image.')
label = long_title.replace(' ', '_').lower()
user_serializer = ShareUserSerializer(
data={'username': label, 'is_trusted': True},
context={'request': request}
)
user_serializer.is_valid(raise_exception=True)
with transaction.atomic():
user_instance = user_serializer.save()
source_instance = Source(
user_id=user_instance.id,
long_title=long_title,
home_page=request.data.get('home_page', None),
name=label,
)
source_instance.icon.save(label, content=icon_file)
source_config_instance = SourceConfig.objects.create(source_id=source_instance.id, label=label)
return Response(
{
'id': IDObfuscator.encode(source_instance),
'type': 'Source',
'attributes': {
'long_title': source_instance.long_title,
'name': source_instance.name,
'home_page': source_instance.home_page
},
'relationships': {
'share_user': {
'data': {
'id': IDObfuscator.encode(user_instance),
'type': 'ShareUser',
'attributes': {
'username': user_instance.username,
'authorization_token': user_instance.accesstoken_set.first().token
}
}
},
'source_config': {
'data': {
'id': IDObfuscator.encode(source_config_instance),
'type': 'SourceConfig',
'attributes': {
'label': source_config_instance.label
}
}
}
}
},
status=status.HTTP_201_CREATED
)
class NormalizedDataViewSet(viewsets.ModelViewSet):
"""View showing all normalized data in the SHARE Dataset.
## Submitting changes to the SHARE dataset
Changes, whether they are additions or modifications, are submitted as a subset of [JSON-LD graphs](https://www.w3.org/TR/json-ld/#named-graphs).
Each [node](https://www.w3.org/TR/json-ld/#dfn-node) of the graph MUST contain both an `@id` and `@type` key.
Method: POST
Body (JSON): {
'data': {
'type': 'NormalizedData'
'attributes': {
'data': {
'@graph': [{
'@type': <type of document, exp: person>,
'@id': <_:random>,
<attribute_name>: <value>,
<relationship_name>: {
'@type': <type>,
'@id': <id>
}
}]
}
}
}
}
Success: 200 OK
"""
ordering = ('-id', )
pagination_class = CursorPagination
permission_classes = [ReadOnlyOrTokenHasScopeOrIsAuthenticated, ]
required_scopes = ['upload_normalized_manuscript', ]
resource_name = 'NormalizedData'
def get_serializer_class(self):
if not self.request.user.is_authenticated:
return BasicNormalizedDataSerializer
elif self.request.user.is_robot:
return FullNormalizedDataSerializer
return BasicNormalizedDataSerializer
def get_queryset(self):
return NormalizedData.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer_class()(data=request.data, context={'request': request})
if serializer.is_valid(raise_exception=True):
nm_instance = serializer.save()
async_result = disambiguate.delay(nm_instance.id)
# TODO Fix Me
return Response({
'id': nm_instance.id,
'type': 'NormalizedData',
'attributes': {'task': async_result.id}
}, status=status.HTTP_202_ACCEPTED)
class RawDatumViewSet(viewsets.ReadOnlyModelViewSet):
"""
Raw data, exactly as harvested from the data source.
## Query by object
To get all the raw data corresponding to a Share object, use the query
parameters `object_id=<@id>` and `object_type=<@type>`
"""
ordering = ('-id', )
pagination_class = CursorPagination
serializer_class = RawDatumSerializer
def get_queryset(self):
object_id = self.request.query_params.get('object_id', None)
object_type = self.request.query_params.get('object_type', None)
if object_id and object_type:
return RawDatum.objects.filter(
normalizeddata__changeset__changes__target_id=object_id,
normalizeddata__changeset__changes__target_type__model=object_type
).distinct('id').select_related('suid')
else:
return RawDatum.objects.all().select_related('suid')
class V1DataView(views.APIView):
"""View allowing sources to post SHARE v1 formatted metadata directly to the SHARE Dataset.
## Submit Data in SHARE v1 Format
Please note that this endpoint is to ease the transition from SHARE v1 to SHARE v2 and sources
are encouraged to transition to submitting metadata in the SHARE v2 format.
Submitting data through the normalizeddata endpoint is strongly preferred as support for
the v1 format will not be continued.
v1 Format
For the full format please see https://github.com/erinspace/shareregistration/blob/master/push_endpoint/schemas.py
Required Fields: [
"title",
"contributors",
"uris",
"providerUpdatedDateTime"
],
Create
Method: POST
Body (JSON): {
{
"jsonData": {
"publisher":{
"name": <publisher name>,
"uri": <publisher uri>
},
"description": <description>,
"contributors":[
{
"name":<contributor name>,
"email": <email>,
"sameAs": <uri>
},
{
"name":<contributor name>
}
],
"title": <title>,
"tags":[
<tag>,
<tag>
],
"languages":[
<language>
],
"providerUpdatedDateTime": <time submitted>,
"uris": {
"canonicalUri": <uri>,
"providerUris":[
<uri>
]
}
}
}
}
Success: 200 OK
"""
authentication_classes = (APIV1TokenBackPortAuthentication, )
permission_classes = [ReadOnlyOrTokenHasScopeOrIsAuthenticated, ]
serializer_class = BasicNormalizedDataSerializer
renderer_classes = (JSONRenderer, )
parser_classes = (JSONParser,)
def post(self, request, *args, **kwargs):
try:
jsonschema.validate(request.data, schemas.v1_push_schema)
except (jsonschema.exceptions.ValidationError) as error:
raise ParseError(detail=error.message)
try:
prelim_data = request.data['jsonData']
except ParseError as error:
return Response(
'Invalid JSON - {0}'.format(error.message),
status=status.HTTP_400_BAD_REQUEST
)
# store raw data, assuming you can only submit one at a time
with transaction.atomic():
try:
doc_id = prelim_data['uris']['canonicalUri']
except KeyError:
return Response({'errors': 'Canonical URI not found in uris.', 'data': prelim_data}, status=status.HTTP_400_BAD_REQUEST)
config = self._get_source_config(request.user)
raw = RawDatum.objects.store_data(config, FetchResult(doc_id, DictSerializer(pretty=False).serialize(prelim_data), timezone.now()))
transformed_data = config.get_transformer().transform(raw.datum)
data = {}
data['data'] = transformed_data
serializer = BasicNormalizedDataSerializer(data=data, context={'request': request})
if serializer.is_valid():
nm_instance = serializer.save()
async_result = disambiguate.delay(nm_instance.id)
return Response({'task_id': async_result.id}, status=status.HTTP_202_ACCEPTED)
return Response({'errors': serializer.errors, 'data': prelim_data}, status=status.HTTP_400_BAD_REQUEST)
def _get_source_config(self, user):
config_label = '{}.v1_push'.format(user.username)
try:
return SourceConfig.objects.get(label=config_label)
except SourceConfig.DoesNotExist:
source, _ = Source.objects.get_or_create(
user=user,
defaults={
'name': user.username,
'long_title': user.username,
}
)
config = SourceConfig(
label=config_label,
source=source,
transformer=Transformer.objects.get(key='v1_push'),
)
config.save()
return config