udata 8.0.1.dev28882__py2.py3-none-any.whl → 8.0.1.dev28979__py2.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.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api/__init__.py +6 -3
- udata/api/oauth2.py +2 -1
- udata/api_fields.py +254 -0
- udata/core/badges/models.py +2 -1
- udata/core/dataservices/__init__.py +0 -0
- udata/core/dataservices/api.py +84 -0
- udata/core/dataservices/models.py +130 -0
- udata/core/metrics/models.py +2 -3
- udata/core/organization/models.py +3 -1
- udata/core/owned.py +39 -2
- udata/harvest/backends/dcat.py +14 -8
- udata/routing.py +6 -0
- udata/tests/api/__init__.py +3 -0
- udata/tests/api/test_dataservices_api.py +236 -0
- udata/tests/plugin.py +5 -0
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/METADATA +3 -1
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/RECORD +21 -16
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/LICENSE +0 -0
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/WHEEL +0 -0
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/entry_points.txt +0 -0
- {udata-8.0.1.dev28882.dist-info → udata-8.0.1.dev28979.dist-info}/top_level.txt +0 -0
udata/api/__init__.py
CHANGED
|
@@ -22,7 +22,7 @@ from udata.auth import (
|
|
|
22
22
|
from udata.utils import safe_unicode
|
|
23
23
|
from udata.mongo.errors import FieldValidationError
|
|
24
24
|
|
|
25
|
-
from . import fields
|
|
25
|
+
from . import fields
|
|
26
26
|
from .signals import on_api_call
|
|
27
27
|
|
|
28
28
|
|
|
@@ -129,6 +129,7 @@ class UDataApi(Api):
|
|
|
129
129
|
@wraps(func)
|
|
130
130
|
def wrapper(*args, **kwargs):
|
|
131
131
|
from udata.core.user.models import User
|
|
132
|
+
from udata.api.oauth2 import check_credentials
|
|
132
133
|
|
|
133
134
|
if current_user.is_authenticated:
|
|
134
135
|
return func(*args, **kwargs)
|
|
@@ -143,7 +144,7 @@ class UDataApi(Api):
|
|
|
143
144
|
if not login_user(user, False):
|
|
144
145
|
self.abort(401, 'Inactive user')
|
|
145
146
|
else:
|
|
146
|
-
|
|
147
|
+
check_credentials()
|
|
147
148
|
return func(*args, **kwargs)
|
|
148
149
|
return wrapper
|
|
149
150
|
|
|
@@ -324,6 +325,7 @@ def init_app(app):
|
|
|
324
325
|
import udata.core.user.api # noqa
|
|
325
326
|
import udata.core.dataset.api # noqa
|
|
326
327
|
import udata.core.dataset.apiv2 # noqa
|
|
328
|
+
import udata.core.dataservices.api # noqa
|
|
327
329
|
import udata.core.discussions.api # noqa
|
|
328
330
|
import udata.core.reuse.api # noqa
|
|
329
331
|
import udata.core.reuse.apiv2 # noqa
|
|
@@ -350,5 +352,6 @@ def init_app(app):
|
|
|
350
352
|
app.register_blueprint(apiv1_blueprint)
|
|
351
353
|
app.register_blueprint(apiv2_blueprint)
|
|
352
354
|
|
|
353
|
-
oauth2
|
|
355
|
+
from udata.api.oauth2 import init_app as oauth2_init_app
|
|
356
|
+
oauth2_init_app(app)
|
|
354
357
|
cors.init_app(app)
|
udata/api/oauth2.py
CHANGED
|
@@ -34,6 +34,7 @@ from werkzeug.security import gen_salt
|
|
|
34
34
|
|
|
35
35
|
from udata.app import csrf
|
|
36
36
|
from udata.auth import current_user, login_required, login_user
|
|
37
|
+
from udata.core.organization.models import Organization
|
|
37
38
|
from udata.i18n import I18nBlueprint, lazy_gettext as _
|
|
38
39
|
from udata.mongo import db
|
|
39
40
|
from udata.core.storages import images, default_image_basename
|
|
@@ -66,7 +67,7 @@ class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
|
66
67
|
description = db.StringField()
|
|
67
68
|
|
|
68
69
|
owner = db.ReferenceField('User')
|
|
69
|
-
organization = db.ReferenceField(
|
|
70
|
+
organization = db.ReferenceField(Organization, reverse_delete_rule=db.NULLIFY)
|
|
70
71
|
image = db.ImageField(fs=images, basename=default_image_basename,
|
|
71
72
|
thumbnails=[150, 25])
|
|
72
73
|
|
udata/api_fields.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from udata.api import api
|
|
2
|
+
import flask_restx.fields as restx_fields
|
|
3
|
+
import udata.api.fields as custom_restx_fields
|
|
4
|
+
from bson import ObjectId
|
|
5
|
+
import mongoengine
|
|
6
|
+
import mongoengine.fields as mongo_fields
|
|
7
|
+
|
|
8
|
+
from udata.mongo.errors import FieldValidationError
|
|
9
|
+
|
|
10
|
+
def convert_db_to_field(key, field, info = {}):
|
|
11
|
+
'''
|
|
12
|
+
This function maps a Mongo field to a Flask RestX field.
|
|
13
|
+
Most of the types are a simple 1-to-1 mapping except lists and references that requires
|
|
14
|
+
more work.
|
|
15
|
+
We currently only map the params that we use from Mongo to RestX (for example min_length / max_length…).
|
|
16
|
+
|
|
17
|
+
In the first part of the function we save the RestX constructor as a lambda because we need to call it with the
|
|
18
|
+
params. Since merging the params involve a litte bit of work (merging default params with read/write params and then with
|
|
19
|
+
user-supplied overrides, setting the readonly flag…), it's easier to have do this one time at the end of the function.
|
|
20
|
+
'''
|
|
21
|
+
info = { **getattr(field, '__additional_field_info__', {}), **info }
|
|
22
|
+
|
|
23
|
+
params = {}
|
|
24
|
+
params['required'] = field.required
|
|
25
|
+
|
|
26
|
+
read_params = {}
|
|
27
|
+
write_params = {}
|
|
28
|
+
|
|
29
|
+
constructor = None
|
|
30
|
+
constructor_read = None
|
|
31
|
+
constructor_write = None
|
|
32
|
+
|
|
33
|
+
if info.get('convert_to'):
|
|
34
|
+
# TODO: this is currently never used. We may remove it if the auto-conversion
|
|
35
|
+
# is always good enough.
|
|
36
|
+
return info.get('convert_to'), info.get('convert_to')
|
|
37
|
+
elif isinstance(field, mongo_fields.StringField):
|
|
38
|
+
constructor = restx_fields.String
|
|
39
|
+
params['min_length'] = field.min_length
|
|
40
|
+
params['max_length'] = field.max_length
|
|
41
|
+
elif isinstance(field, mongo_fields.FloatField):
|
|
42
|
+
constructor = restx_fields.Float
|
|
43
|
+
params['min'] = field.min # TODO min_value?
|
|
44
|
+
params['max'] = field.max
|
|
45
|
+
elif isinstance(field, mongo_fields.BooleanField):
|
|
46
|
+
constructor = restx_fields.Boolean
|
|
47
|
+
elif isinstance(field, mongo_fields.DateTimeField):
|
|
48
|
+
constructor = custom_restx_fields.ISODateTime
|
|
49
|
+
elif isinstance(field, mongo_fields.DictField):
|
|
50
|
+
constructor = restx_fields.Raw
|
|
51
|
+
elif isinstance(field, mongo_fields.ListField):
|
|
52
|
+
# For lists, we convert the inner value from Mongo to RestX then we create
|
|
53
|
+
# the `List` RestX type with this converted inner value.
|
|
54
|
+
field_read, field_write = convert_db_to_field(f"{key}.inner", field.field, info.get('inner_field_info', {}))
|
|
55
|
+
constructor_read = lambda **kwargs: restx_fields.List(field_read, **kwargs)
|
|
56
|
+
constructor_write = lambda **kwargs: restx_fields.List(field_write, **kwargs)
|
|
57
|
+
elif isinstance(field, mongo_fields.ReferenceField):
|
|
58
|
+
# For reference we accept while writing a String representing the ID of the referenced model.
|
|
59
|
+
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
60
|
+
# the referenced model, if not we return a String (and RestX will call the `str()` of the model
|
|
61
|
+
# when returning from an endpoint)
|
|
62
|
+
nested_fields = info.get('nested_fields')
|
|
63
|
+
if nested_fields is None:
|
|
64
|
+
# If there is no `nested_fields` convert the object to the string representation.
|
|
65
|
+
constructor_read = restx_fields.String
|
|
66
|
+
else:
|
|
67
|
+
constructor_read = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
68
|
+
|
|
69
|
+
write_params['description'] = "ID of the reference"
|
|
70
|
+
constructor_write = restx_fields.String
|
|
71
|
+
elif isinstance(field, mongo_fields.EmbeddedDocumentField):
|
|
72
|
+
nested_fields = info.get('nested_fields')
|
|
73
|
+
if nested_fields is None:
|
|
74
|
+
raise ValueError(f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize.")
|
|
75
|
+
|
|
76
|
+
constructor = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(f"Unsupported MongoEngine field type {field.__class__.__name__}")
|
|
79
|
+
|
|
80
|
+
read_params = {**params, **read_params, **info}
|
|
81
|
+
write_params = {**params, **write_params, **info}
|
|
82
|
+
|
|
83
|
+
read = constructor_read(**read_params) if constructor_read else constructor(**read_params)
|
|
84
|
+
if write_params.get('readonly', False):
|
|
85
|
+
write = None
|
|
86
|
+
else:
|
|
87
|
+
write = constructor_write(**write_params) if constructor_write else constructor(**write_params)
|
|
88
|
+
return read, write
|
|
89
|
+
|
|
90
|
+
def generate_fields(**kwargs):
|
|
91
|
+
'''
|
|
92
|
+
This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
|
|
93
|
+
that can be used in API endpoint inside `expect()` and `marshall_with()`.
|
|
94
|
+
'''
|
|
95
|
+
def wrapper(cls):
|
|
96
|
+
read_fields = {}
|
|
97
|
+
write_fields = {}
|
|
98
|
+
sortables = []
|
|
99
|
+
|
|
100
|
+
read_fields['id'] = restx_fields.String(required=True)
|
|
101
|
+
|
|
102
|
+
for key, field in cls._fields.items():
|
|
103
|
+
info = getattr(field, '__additional_field_info__', None)
|
|
104
|
+
if info is None: continue
|
|
105
|
+
|
|
106
|
+
if info.get('sortable', False):
|
|
107
|
+
sortables.append(key)
|
|
108
|
+
|
|
109
|
+
read, write = convert_db_to_field(key, field)
|
|
110
|
+
|
|
111
|
+
if read:
|
|
112
|
+
read_fields[key] = read
|
|
113
|
+
if write:
|
|
114
|
+
write_fields[key] = write
|
|
115
|
+
|
|
116
|
+
# The goal of this loop is to fetch all functions (getters) of the class
|
|
117
|
+
# If a function has an `__additional_field_info__` attribute it means
|
|
118
|
+
# it has been decorated with `@function_field()` and should be included
|
|
119
|
+
# in the API response.
|
|
120
|
+
for method_name in dir(cls):
|
|
121
|
+
if method_name == 'objects': continue
|
|
122
|
+
if method_name.startswith('_'): continue
|
|
123
|
+
if method_name in read_fields: continue # Do not override if the attribute is also callable like for Extras
|
|
124
|
+
|
|
125
|
+
method = getattr(cls, method_name)
|
|
126
|
+
if not callable(method): continue
|
|
127
|
+
|
|
128
|
+
info = getattr(method, '__additional_field_info__', None)
|
|
129
|
+
if info is None: continue
|
|
130
|
+
|
|
131
|
+
def make_lambda(method):
|
|
132
|
+
'''
|
|
133
|
+
Factory function to create a lambda with the correct scope.
|
|
134
|
+
If we don't have this factory function, the `method` will be the
|
|
135
|
+
last method assigned in this loop?
|
|
136
|
+
'''
|
|
137
|
+
return lambda o: method(o)
|
|
138
|
+
|
|
139
|
+
read_fields[method_name] = restx_fields.String(attribute=make_lambda(method), **{ 'readonly':True, **info })
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
|
|
143
|
+
cls.__write_fields__ = api.model(f"{cls.__name__} (write)", write_fields, **kwargs)
|
|
144
|
+
|
|
145
|
+
mask = kwargs.pop('mask', None)
|
|
146
|
+
if mask is not None:
|
|
147
|
+
mask = 'data{{{0}}},*'.format(mask)
|
|
148
|
+
cls.__page_fields__ = api.model(f"{cls.__name__}Page", custom_restx_fields.pager(cls.__read_fields__), mask=mask, **kwargs)
|
|
149
|
+
|
|
150
|
+
# Parser for index sort/filters
|
|
151
|
+
paginable = kwargs.get('paginable', True)
|
|
152
|
+
parser = api.parser()
|
|
153
|
+
|
|
154
|
+
if paginable:
|
|
155
|
+
parser.add_argument('page', type=int, location='args', default=1, help='The page to display')
|
|
156
|
+
parser.add_argument('page_size', type=int, location='args', default=20, help='The page size')
|
|
157
|
+
|
|
158
|
+
if sortables:
|
|
159
|
+
choices = sortables + ['-' + k for k in sortables]
|
|
160
|
+
parser.add_argument('sort', type=str, location='args', choices=choices, help='The field (and direction) on which sorting apply')
|
|
161
|
+
|
|
162
|
+
cls.__index_parser__ = parser
|
|
163
|
+
def apply_sort_filters_and_pagination(base_query):
|
|
164
|
+
args = cls.__index_parser__.parse_args()
|
|
165
|
+
|
|
166
|
+
if sortables and args['sort']:
|
|
167
|
+
base_query = base_query.order_by(args['sort'])
|
|
168
|
+
|
|
169
|
+
if paginable:
|
|
170
|
+
base_query = base_query.paginate(args['page'], args['page_size'])
|
|
171
|
+
|
|
172
|
+
return base_query
|
|
173
|
+
|
|
174
|
+
cls.apply_sort_filters_and_pagination = apply_sort_filters_and_pagination
|
|
175
|
+
return cls
|
|
176
|
+
return wrapper
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def function_field(**info):
|
|
180
|
+
def inner(func):
|
|
181
|
+
func.__additional_field_info__ = info
|
|
182
|
+
return func
|
|
183
|
+
|
|
184
|
+
return inner
|
|
185
|
+
|
|
186
|
+
def field(inner, **kwargs):
|
|
187
|
+
'''
|
|
188
|
+
Simple decorator to mark a field as visible for the API fields.
|
|
189
|
+
We can pass additional arguments that will be forward to the RestX field constructor.
|
|
190
|
+
'''
|
|
191
|
+
inner.__additional_field_info__ = kwargs
|
|
192
|
+
return inner
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def patch(obj, request):
|
|
196
|
+
'''
|
|
197
|
+
Patch the object with the data from the request.
|
|
198
|
+
Only fields decorated with the `field()` decorator will be read (and not readonly).
|
|
199
|
+
'''
|
|
200
|
+
for key, value in request.json.items():
|
|
201
|
+
field = obj.__write_fields__.get(key)
|
|
202
|
+
if field is not None and not field.readonly:
|
|
203
|
+
model_attribute = getattr(obj.__class__, key)
|
|
204
|
+
if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(model_attribute.field, mongoengine.fields.ReferenceField):
|
|
205
|
+
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
206
|
+
value = [wrap_primary_key(key, model_attribute.field, id) for id in value]
|
|
207
|
+
if isinstance(model_attribute, mongoengine.fields.ReferenceField):
|
|
208
|
+
value = wrap_primary_key(key, model_attribute, value)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
info = getattr(model_attribute, '__additional_field_info__', {})
|
|
212
|
+
|
|
213
|
+
# `check` field attribute allows to do validation from the request before setting
|
|
214
|
+
# the attribute
|
|
215
|
+
check = info.get('check', None)
|
|
216
|
+
if check is not None:
|
|
217
|
+
check(**{key: value}) # TODO add other model attributes in function parameters
|
|
218
|
+
|
|
219
|
+
setattr(obj, key, value)
|
|
220
|
+
|
|
221
|
+
return obj
|
|
222
|
+
|
|
223
|
+
def wrap_primary_key(field_name: str, foreign_field: mongoengine.fields.ReferenceField, value: str):
|
|
224
|
+
'''
|
|
225
|
+
We need to wrap the `String` inside an `ObjectId` most of the time. If the foreign ID is a `String` we need to get
|
|
226
|
+
a `DBRef` from the database.
|
|
227
|
+
|
|
228
|
+
TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
|
|
229
|
+
'''
|
|
230
|
+
document_type = foreign_field.document_type()
|
|
231
|
+
id_field_name = document_type.__class__._meta["id_field"]
|
|
232
|
+
|
|
233
|
+
id_field = getattr(document_type.__class__, id_field_name)
|
|
234
|
+
|
|
235
|
+
# Get the foreign document from MongoDB because the othewise it fails during read
|
|
236
|
+
# Also useful to get a DBRef for non ObjectId references (see below)
|
|
237
|
+
foreign_document = document_type.__class__.objects(**{id_field_name: value}).first()
|
|
238
|
+
if foreign_document is None:
|
|
239
|
+
raise FieldValidationError(field=field_name, message=f"Unknown reference '{value}'")
|
|
240
|
+
|
|
241
|
+
if isinstance(id_field, mongoengine.fields.ObjectIdField):
|
|
242
|
+
return ObjectId(value)
|
|
243
|
+
elif isinstance(id_field, mongoengine.fields.StringField):
|
|
244
|
+
# Right now I didn't find a simpler way to make mongoengine happy.
|
|
245
|
+
# For references, it expects `ObjectId`, `DBRef`, `LazyReference` or `document` but since
|
|
246
|
+
# the primary key a StringField (not an `ObjectId`) we cannot create an `ObjectId`, I didn't find
|
|
247
|
+
# a way to create a `DBRef` nor a `LazyReference` so I create a simple document with only the ID field.
|
|
248
|
+
# We could use a simple dict as follow instead:
|
|
249
|
+
# { 'id': value }
|
|
250
|
+
# … but it may be important to check before-hand that the reference point to a correct document.
|
|
251
|
+
return foreign_document.to_dbref()
|
|
252
|
+
else:
|
|
253
|
+
raise ValueError(f"Unknown ID field type {id_field.__class__} for {document_type.__class__} (ID field name is {id_field_name}, value was {value})")
|
|
254
|
+
|
udata/core/badges/models.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import weakref
|
|
3
2
|
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
|
|
6
5
|
from mongoengine.signals import post_save
|
|
7
6
|
|
|
7
|
+
from udata.api_fields import field
|
|
8
8
|
from udata.auth import current_user
|
|
9
9
|
from udata.mongo import db
|
|
10
10
|
|
|
11
11
|
from .signals import on_badge_added, on_badge_removed
|
|
12
|
+
from .fields import badge_fields
|
|
12
13
|
|
|
13
14
|
log = logging.getLogger(__name__)
|
|
14
15
|
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from flask import request
|
|
3
|
+
from flask_login import current_user
|
|
4
|
+
import mongoengine
|
|
5
|
+
|
|
6
|
+
from udata.api import api, API
|
|
7
|
+
from udata.api_fields import patch
|
|
8
|
+
from udata.core.dataset.permissions import OwnablePermission
|
|
9
|
+
from .models import Dataservice
|
|
10
|
+
from udata.models import db
|
|
11
|
+
|
|
12
|
+
ns = api.namespace('dataservices', 'Dataservices related operations (beta)')
|
|
13
|
+
|
|
14
|
+
@ns.route('/', endpoint='dataservices')
|
|
15
|
+
class DataservicesAPI(API):
|
|
16
|
+
'''Dataservices collection endpoint'''
|
|
17
|
+
@api.doc('list_dataservices')
|
|
18
|
+
@api.expect(Dataservice.__index_parser__)
|
|
19
|
+
@api.marshal_with(Dataservice.__page_fields__)
|
|
20
|
+
def get(self):
|
|
21
|
+
'''List or search all dataservices'''
|
|
22
|
+
query = Dataservice.objects.visible()
|
|
23
|
+
|
|
24
|
+
return Dataservice.apply_sort_filters_and_pagination(query)
|
|
25
|
+
|
|
26
|
+
@api.secure
|
|
27
|
+
@api.doc('create_dataservice', responses={400: 'Validation error'})
|
|
28
|
+
@api.expect(Dataservice.__write_fields__)
|
|
29
|
+
@api.marshal_with(Dataservice.__read_fields__, code=201)
|
|
30
|
+
def post(self):
|
|
31
|
+
dataservice = patch(Dataservice(), request)
|
|
32
|
+
if not dataservice.owner and not dataservice.organization:
|
|
33
|
+
dataservice.owner = current_user._get_current_object()
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
dataservice.save()
|
|
37
|
+
except mongoengine.errors.ValidationError as e:
|
|
38
|
+
api.abort(400, e.message)
|
|
39
|
+
|
|
40
|
+
return dataservice, 201
|
|
41
|
+
|
|
42
|
+
@ns.route('/<dataservice:dataservice>/', endpoint='dataservice')
|
|
43
|
+
class DataserviceAPI(API):
|
|
44
|
+
@api.doc('get_dataservice')
|
|
45
|
+
@api.marshal_with(Dataservice.__read_fields__)
|
|
46
|
+
def get(self, dataservice):
|
|
47
|
+
if dataservice.deleted_at and not OwnablePermission(dataservice).can():
|
|
48
|
+
api.abort(410, 'Dataservice has been deleted')
|
|
49
|
+
return dataservice
|
|
50
|
+
|
|
51
|
+
@api.secure
|
|
52
|
+
@api.doc('update_dataservice', responses={400: 'Validation error'})
|
|
53
|
+
@api.expect(Dataservice.__write_fields__)
|
|
54
|
+
@api.marshal_with(Dataservice.__read_fields__)
|
|
55
|
+
def patch(self, dataservice):
|
|
56
|
+
if dataservice.deleted_at:
|
|
57
|
+
api.abort(410, 'dataservice has been deleted')
|
|
58
|
+
|
|
59
|
+
OwnablePermission(dataservice).test()
|
|
60
|
+
|
|
61
|
+
patch(dataservice, request)
|
|
62
|
+
dataservice.modified_at = datetime.utcnow()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
dataservice.save()
|
|
66
|
+
return dataservice
|
|
67
|
+
except mongoengine.errors.ValidationError as e:
|
|
68
|
+
api.abort(400, e.message)
|
|
69
|
+
|
|
70
|
+
@api.secure
|
|
71
|
+
@api.doc('delete_dataservice')
|
|
72
|
+
@api.response(204, 'dataservice deleted')
|
|
73
|
+
def delete(self, dataservice):
|
|
74
|
+
if dataservice.deleted_at:
|
|
75
|
+
api.abort(410, 'dataservice has been deleted')
|
|
76
|
+
|
|
77
|
+
OwnablePermission(dataservice).test()
|
|
78
|
+
|
|
79
|
+
dataservice.deleted_at = datetime.utcnow()
|
|
80
|
+
dataservice.modified_at = datetime.utcnow()
|
|
81
|
+
dataservice.save()
|
|
82
|
+
|
|
83
|
+
return '', 204
|
|
84
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from udata.api_fields import field, function_field, generate_fields
|
|
3
|
+
from udata.core.dataset.models import Dataset
|
|
4
|
+
from udata.core.metrics.models import WithMetrics
|
|
5
|
+
from udata.core.owned import Owned, OwnedQuerySet
|
|
6
|
+
from udata.i18n import lazy_gettext as _
|
|
7
|
+
import udata.core.contact_point.api_fields as contact_api_fields
|
|
8
|
+
import udata.core.dataset.api_fields as datasets_api_fields
|
|
9
|
+
|
|
10
|
+
from udata.models import db
|
|
11
|
+
from udata.uris import endpoint_for
|
|
12
|
+
|
|
13
|
+
# "frequency"
|
|
14
|
+
# "harvest"
|
|
15
|
+
# "internal"
|
|
16
|
+
# "page"
|
|
17
|
+
# "quality" # Peut-être pas dans une v1 car la qualité sera probablement calculé différemment
|
|
18
|
+
# "datasets" # objet : liste de datasets liés à une API
|
|
19
|
+
# "spatial"
|
|
20
|
+
# "temporal_coverage"
|
|
21
|
+
|
|
22
|
+
DATASERVICE_FORMATS = ['REST', 'WMS', 'WSL']
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataserviceQuerySet(OwnedQuerySet):
|
|
26
|
+
def visible(self):
|
|
27
|
+
return self(archived_at=None, deleted_at=None, private=False)
|
|
28
|
+
|
|
29
|
+
def hidden(self):
|
|
30
|
+
return self(db.Q(private=True) |
|
|
31
|
+
db.Q(deleted_at__ne=None) |
|
|
32
|
+
db.Q(archived_at__ne=None))
|
|
33
|
+
|
|
34
|
+
@generate_fields()
|
|
35
|
+
class Dataservice(WithMetrics, Owned, db.Document):
|
|
36
|
+
meta = {
|
|
37
|
+
'indexes': [
|
|
38
|
+
'$title',
|
|
39
|
+
] + Owned.meta['indexes'],
|
|
40
|
+
'queryset_class': DataserviceQuerySet,
|
|
41
|
+
'auto_create_index_on_save': True
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
title = field(
|
|
45
|
+
db.StringField(required=True),
|
|
46
|
+
example="My awesome API",
|
|
47
|
+
sortable=True,
|
|
48
|
+
)
|
|
49
|
+
acronym = field(
|
|
50
|
+
db.StringField(max_length=128),
|
|
51
|
+
)
|
|
52
|
+
# /!\ do not set directly the slug when creating or updating a dataset
|
|
53
|
+
# this will break the search indexation
|
|
54
|
+
slug = field(
|
|
55
|
+
db.SlugField(max_length=255, required=True, populate_from='title', update=True, follow=True),
|
|
56
|
+
readonly=True,
|
|
57
|
+
)
|
|
58
|
+
description = field(
|
|
59
|
+
db.StringField(default=''),
|
|
60
|
+
description="In markdown"
|
|
61
|
+
)
|
|
62
|
+
base_api_url = field(
|
|
63
|
+
db.URLField(required=True),
|
|
64
|
+
sortable=True,
|
|
65
|
+
)
|
|
66
|
+
endpoint_description_url = field(db.URLField())
|
|
67
|
+
authorization_request_url = field(db.URLField())
|
|
68
|
+
availability= field(
|
|
69
|
+
db.FloatField(min=0, max=100),
|
|
70
|
+
example='99.99'
|
|
71
|
+
)
|
|
72
|
+
rate_limiting = field(db.StringField())
|
|
73
|
+
is_restricted = field(db.BooleanField())
|
|
74
|
+
has_token = field(db.BooleanField())
|
|
75
|
+
format = field(db.StringField(choices=DATASERVICE_FORMATS))
|
|
76
|
+
|
|
77
|
+
license = field(
|
|
78
|
+
db.ReferenceField('License'),
|
|
79
|
+
allow_null=True,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
tags = field(
|
|
83
|
+
db.TagListField(),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
private = field(
|
|
87
|
+
db.BooleanField(default=False),
|
|
88
|
+
description='Is the dataservice private to the owner or the organization'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
extras = field(db.ExtrasField())
|
|
92
|
+
|
|
93
|
+
contact_point = field(
|
|
94
|
+
db.ReferenceField('ContactPoint', reverse_delete_rule=db.NULLIFY),
|
|
95
|
+
nested_fields=contact_api_fields.contact_point_fields,
|
|
96
|
+
allow_null=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
created_at = field(
|
|
100
|
+
db.DateTimeField(verbose_name=_('Creation date'), default=datetime.utcnow, required=True),
|
|
101
|
+
readonly=True,
|
|
102
|
+
)
|
|
103
|
+
metadata_modified_at = field(
|
|
104
|
+
db.DateTimeField(verbose_name=_('Last modification date'), default=datetime.utcnow, required=True),
|
|
105
|
+
readonly=True,
|
|
106
|
+
)
|
|
107
|
+
deleted_at = field(db.DateTimeField(), readonly=True)
|
|
108
|
+
archived_at = field(db.DateTimeField(), readonly=True)
|
|
109
|
+
|
|
110
|
+
datasets = field(
|
|
111
|
+
db.ListField(
|
|
112
|
+
field(
|
|
113
|
+
db.ReferenceField(Dataset),
|
|
114
|
+
nested_fields=datasets_api_fields.dataset_fields,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@function_field(description="Link to the API endpoint for this dataservice")
|
|
120
|
+
def self_api_url(self):
|
|
121
|
+
return endpoint_for('api.dataservice', dataservice=self, _external=True)
|
|
122
|
+
|
|
123
|
+
def self_web_url():
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
# TODO
|
|
127
|
+
# frequency = db.StringField(choices=list(UPDATE_FREQUENCIES.keys()))
|
|
128
|
+
# temporal_coverage = db.EmbeddedDocumentField(db.DateRange)
|
|
129
|
+
# spatial = db.EmbeddedDocumentField(SpatialCoverage)
|
|
130
|
+
# harvest = db.EmbeddedDocumentField(HarvestDatasetMetadata)
|
udata/core/metrics/models.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from udata.api_fields import field
|
|
3
2
|
from udata.mongo import db
|
|
4
3
|
|
|
5
4
|
|
|
@@ -7,7 +6,7 @@ __all__ = ('WithMetrics',)
|
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class WithMetrics(object):
|
|
10
|
-
metrics = db.DictField()
|
|
9
|
+
metrics = field(db.DictField())
|
|
11
10
|
|
|
12
11
|
__metrics_keys__ = []
|
|
13
12
|
|
|
@@ -5,9 +5,11 @@ from blinker import Signal
|
|
|
5
5
|
from mongoengine.signals import pre_save, post_save
|
|
6
6
|
from werkzeug.utils import cached_property
|
|
7
7
|
|
|
8
|
+
from udata.core.badges.models import BadgeMixin
|
|
9
|
+
from udata.core.metrics.models import WithMetrics
|
|
8
10
|
from udata.core.storages import avatars, default_image_basename
|
|
9
11
|
from udata.frontend.markdown import mdstrip
|
|
10
|
-
from udata.
|
|
12
|
+
from udata.mongo import db
|
|
11
13
|
from udata.i18n import lazy_gettext as _
|
|
12
14
|
from udata.uris import endpoint_for
|
|
13
15
|
from .constants import ASSOCIATION, CERTIFIED, COMPANY, LOCAL_AUTHORITY, LOGO_SIZES, ORG_BID_SIZE_LIMIT, ORG_ROLES, DEFAULT_ROLE, MEMBERSHIP_STATUS, LOGO_MAX_SIZE, PUBLIC_SERVICE
|
udata/core/owned.py
CHANGED
|
@@ -4,7 +4,15 @@ from blinker import signal
|
|
|
4
4
|
from mongoengine import NULLIFY, Q, post_save
|
|
5
5
|
from mongoengine.fields import ReferenceField
|
|
6
6
|
|
|
7
|
+
from udata.api_fields import field
|
|
8
|
+
from udata.core.organization.models import Organization
|
|
9
|
+
from udata.core.user.models import User
|
|
7
10
|
from udata.mongo.queryset import UDataQuerySet
|
|
11
|
+
from udata.core.user.api_fields import user_ref_fields
|
|
12
|
+
from udata.core.organization.api_fields import org_ref_fields
|
|
13
|
+
from udata.core.organization.permissions import OrganizationPrivatePermission
|
|
14
|
+
from udata.mongo.errors import FieldValidationError
|
|
15
|
+
from udata.i18n import lazy_gettext as _
|
|
8
16
|
|
|
9
17
|
log = logging.getLogger(__name__)
|
|
10
18
|
|
|
@@ -15,14 +23,42 @@ class OwnedQuerySet(UDataQuerySet):
|
|
|
15
23
|
for owner in owners:
|
|
16
24
|
qs |= Q(owner=owner) | Q(organization=owner)
|
|
17
25
|
return self(qs)
|
|
26
|
+
|
|
27
|
+
def check_owner_is_current_user(owner):
|
|
28
|
+
from udata.auth import current_user, admin_permission
|
|
29
|
+
if current_user.is_authenticated and owner and not admin_permission and current_user.id != owner:
|
|
30
|
+
raise FieldValidationError(_('You can only set yourself as owner'), field="owner")
|
|
31
|
+
|
|
32
|
+
def check_organization_is_valid_for_current_user(organization):
|
|
33
|
+
from udata.auth import current_user
|
|
34
|
+
from udata.models import Organization
|
|
35
|
+
|
|
36
|
+
org = Organization.objects(id=organization).first()
|
|
37
|
+
if org is None:
|
|
38
|
+
raise FieldValidationError(_("Unknown organization"), field="organization")
|
|
39
|
+
|
|
40
|
+
if current_user.is_authenticated and org and not OrganizationPrivatePermission(org).can():
|
|
41
|
+
raise FieldValidationError(_("Permission denied for this organization"), field="organization")
|
|
18
42
|
|
|
19
43
|
|
|
20
44
|
class Owned(object):
|
|
21
45
|
'''
|
|
22
46
|
A mixin to factorize owning behvaior between users and organizations.
|
|
23
47
|
'''
|
|
24
|
-
owner =
|
|
25
|
-
|
|
48
|
+
owner = field(
|
|
49
|
+
ReferenceField(User, reverse_delete_rule=NULLIFY),
|
|
50
|
+
nested_fields=user_ref_fields,
|
|
51
|
+
description="Only present if organization is not set. Can only be set to the current authenticated user.",
|
|
52
|
+
check=check_owner_is_current_user,
|
|
53
|
+
allow_null=True,
|
|
54
|
+
)
|
|
55
|
+
organization = field(
|
|
56
|
+
ReferenceField(Organization, reverse_delete_rule=NULLIFY),
|
|
57
|
+
nested_fields=org_ref_fields,
|
|
58
|
+
description="Only present if owner is not set. Can only be set to an organization of the current authenticated user.",
|
|
59
|
+
check=check_organization_is_valid_for_current_user,
|
|
60
|
+
allow_null=True,
|
|
61
|
+
)
|
|
26
62
|
|
|
27
63
|
on_owner_change = signal('Owned.on_owner_change')
|
|
28
64
|
|
|
@@ -38,6 +74,7 @@ class Owned(object):
|
|
|
38
74
|
'''
|
|
39
75
|
Verify owner consistency and fetch original owner before the new one erase it.
|
|
40
76
|
'''
|
|
77
|
+
|
|
41
78
|
changed_fields = self._get_changed_fields()
|
|
42
79
|
if 'organization' in changed_fields and 'owner' in changed_fields:
|
|
43
80
|
# Ownership changes (org to owner or the other way around) have already been made
|
udata/harvest/backends/dcat.py
CHANGED
|
@@ -268,7 +268,6 @@ class CswDcatBackend(DcatBackend):
|
|
|
268
268
|
headers=headers).content)
|
|
269
269
|
|
|
270
270
|
return graphs
|
|
271
|
-
|
|
272
271
|
|
|
273
272
|
|
|
274
273
|
class CswIso19139DcatBackend(DcatBackend):
|
|
@@ -295,6 +294,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
295
294
|
transform = ET.XSLT(xsl)
|
|
296
295
|
|
|
297
296
|
# Start querying and parsing graph
|
|
297
|
+
# Filter on dataset or serie records
|
|
298
298
|
body = '''<csw:GetRecords xmlns:csw="http://www.opengis.net/cat/csw/2.0.2"
|
|
299
299
|
xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
|
300
300
|
service="CSW" version="2.0.2" resultType="results"
|
|
@@ -304,10 +304,16 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
304
304
|
<csw:ElementSetName>full</csw:ElementSetName>
|
|
305
305
|
<csw:Constraint version="1.1.0">
|
|
306
306
|
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
|
|
307
|
-
<ogc:
|
|
308
|
-
<ogc:
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
<ogc:Or xmlns:ogc="http://www.opengis.net/ogc">
|
|
308
|
+
<ogc:PropertyIsEqualTo>
|
|
309
|
+
<ogc:PropertyName>dc:type</ogc:PropertyName>
|
|
310
|
+
<ogc:Literal>dataset</ogc:Literal>
|
|
311
|
+
</ogc:PropertyIsEqualTo>
|
|
312
|
+
<ogc:PropertyIsEqualTo>
|
|
313
|
+
<ogc:PropertyName>dc:type</ogc:PropertyName>
|
|
314
|
+
<ogc:Literal>series</ogc:Literal>
|
|
315
|
+
</ogc:PropertyIsEqualTo>
|
|
316
|
+
</ogc:Or>
|
|
311
317
|
</ogc:Filter>
|
|
312
318
|
</csw:Constraint>
|
|
313
319
|
</csw:Query>
|
|
@@ -319,7 +325,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
319
325
|
start = 1
|
|
320
326
|
|
|
321
327
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
322
|
-
|
|
328
|
+
headers=headers)
|
|
323
329
|
response.raise_for_status()
|
|
324
330
|
|
|
325
331
|
tree_before_transform = ET.fromstring(response.content)
|
|
@@ -351,12 +357,12 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
351
357
|
next_record = self.next_record_if_should_continue(start, search_results)
|
|
352
358
|
if not next_record:
|
|
353
359
|
break
|
|
354
|
-
|
|
360
|
+
|
|
355
361
|
start = next_record
|
|
356
362
|
page += 1
|
|
357
363
|
|
|
358
364
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
359
|
-
|
|
365
|
+
headers=headers)
|
|
360
366
|
response.raise_for_status()
|
|
361
367
|
|
|
362
368
|
tree_before_transform = ET.fromstring(response.content)
|
udata/routing.py
CHANGED
|
@@ -10,6 +10,7 @@ from werkzeug.urls import url_quote
|
|
|
10
10
|
from udata import models
|
|
11
11
|
from udata.mongo import db
|
|
12
12
|
from udata.core.spatial.models import GeoZone
|
|
13
|
+
from udata.core.dataservices.models import Dataservice
|
|
13
14
|
from udata.i18n import ISO_639_1_CODES
|
|
14
15
|
|
|
15
16
|
|
|
@@ -121,6 +122,10 @@ class DatasetConverter(ModelConverter):
|
|
|
121
122
|
model = models.Dataset
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
class DataserviceConverter(ModelConverter):
|
|
126
|
+
model = Dataservice
|
|
127
|
+
|
|
128
|
+
|
|
124
129
|
class CommunityResourceConverter(ModelConverter):
|
|
125
130
|
model = models.CommunityResource
|
|
126
131
|
|
|
@@ -222,6 +227,7 @@ def init_app(app):
|
|
|
222
227
|
app.url_map.converters['pathlist'] = PathListConverter
|
|
223
228
|
app.url_map.converters['uuid'] = UUIDConverter
|
|
224
229
|
app.url_map.converters['dataset'] = DatasetConverter
|
|
230
|
+
app.url_map.converters['dataservice'] = DataserviceConverter
|
|
225
231
|
app.url_map.converters['crid'] = CommunityResourceConverter
|
|
226
232
|
app.url_map.converters['org'] = OrganizationConverter
|
|
227
233
|
app.url_map.converters['reuse'] = ReuseConverter
|
udata/tests/api/__init__.py
CHANGED
|
@@ -29,5 +29,8 @@ class APITestCase(FrontTestCase):
|
|
|
29
29
|
def put(self, url, data=None, json=True, *args, **kwargs):
|
|
30
30
|
return self.api.put(url, data=data, json=json, *args, **kwargs)
|
|
31
31
|
|
|
32
|
+
def patch(self, url, data=None, json=True, *args, **kwargs):
|
|
33
|
+
return self.api.patch(url, data=data, json=json, *args, **kwargs)
|
|
34
|
+
|
|
32
35
|
def delete(self, url, data=None, *args, **kwargs):
|
|
33
36
|
return self.api.delete(url, data=data, *args, **kwargs)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from pprint import pprint
|
|
2
|
+
from flask import url_for
|
|
3
|
+
|
|
4
|
+
from udata.core.dataservices.models import Dataservice
|
|
5
|
+
from udata.core.dataset.factories import (DatasetFactory, LicenseFactory)
|
|
6
|
+
from udata.i18n import gettext as _
|
|
7
|
+
from udata.core.user.factories import UserFactory
|
|
8
|
+
from udata.core.organization.factories import OrganizationFactory
|
|
9
|
+
from udata.core.organization.models import Member
|
|
10
|
+
|
|
11
|
+
from . import APITestCase
|
|
12
|
+
|
|
13
|
+
class DataserviceAPITest(APITestCase):
|
|
14
|
+
modules = []
|
|
15
|
+
|
|
16
|
+
def test_dataservice_api_create(self):
|
|
17
|
+
user = self.login()
|
|
18
|
+
datasets = DatasetFactory.create_batch(3)
|
|
19
|
+
license = LicenseFactory.create()
|
|
20
|
+
|
|
21
|
+
response = self.post(url_for('api.dataservices'), {
|
|
22
|
+
'title': 'My API',
|
|
23
|
+
'base_api_url': 'https://example.org',
|
|
24
|
+
})
|
|
25
|
+
self.assert201(response)
|
|
26
|
+
self.assertEqual(Dataservice.objects.count(), 1)
|
|
27
|
+
|
|
28
|
+
dataservice = Dataservice.objects.first()
|
|
29
|
+
|
|
30
|
+
response = self.get(url_for('api.dataservice', dataservice=dataservice))
|
|
31
|
+
self.assert200(response)
|
|
32
|
+
|
|
33
|
+
self.assertEqual(response.json['title'], 'My API')
|
|
34
|
+
self.assertEqual(response.json['base_api_url'], 'https://example.org')
|
|
35
|
+
self.assertEqual(response.json['owner']['id'], str(user.id))
|
|
36
|
+
|
|
37
|
+
response = self.patch(url_for('api.dataservice', dataservice=dataservice), {
|
|
38
|
+
'title': 'Updated title',
|
|
39
|
+
'tags': ['hello', 'world'],
|
|
40
|
+
'private': True,
|
|
41
|
+
'datasets': [datasets[0].id, datasets[2].id],
|
|
42
|
+
'license': license.id,
|
|
43
|
+
'extras': {
|
|
44
|
+
'foo': 'bar',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
self.assert200(response)
|
|
48
|
+
|
|
49
|
+
self.assertEqual(response.json['title'], 'Updated title')
|
|
50
|
+
self.assertEqual(response.json['base_api_url'], 'https://example.org')
|
|
51
|
+
self.assertEqual(response.json['tags'], ['hello', 'world'])
|
|
52
|
+
self.assertEqual(response.json['private'], True)
|
|
53
|
+
self.assertEqual(response.json['datasets'][0]['title'], datasets[0].title)
|
|
54
|
+
self.assertEqual(response.json['datasets'][1]['title'], datasets[2].title)
|
|
55
|
+
self.assertEqual(response.json['extras'], {
|
|
56
|
+
'foo': 'bar',
|
|
57
|
+
})
|
|
58
|
+
self.assertEqual(response.json['license'], license.title)
|
|
59
|
+
self.assertEqual(response.json['self_api_url'], 'http://local.test/api/1/dataservices/updated-title/')
|
|
60
|
+
dataservice.reload()
|
|
61
|
+
self.assertEqual(dataservice.title, 'Updated title')
|
|
62
|
+
self.assertEqual(dataservice.base_api_url, 'https://example.org')
|
|
63
|
+
self.assertEqual(dataservice.tags, ['hello', 'world'])
|
|
64
|
+
self.assertEqual(dataservice.private, True)
|
|
65
|
+
self.assertEqual(dataservice.datasets[0].title, datasets[0].title)
|
|
66
|
+
self.assertEqual(dataservice.datasets[1].title, datasets[2].title)
|
|
67
|
+
self.assertEqual(dataservice.extras, {
|
|
68
|
+
'foo': 'bar',
|
|
69
|
+
})
|
|
70
|
+
self.assertEqual(dataservice.license.title, license.title)
|
|
71
|
+
self.assertEqual(dataservice.self_api_url(), 'http://local.test/api/1/dataservices/updated-title/')
|
|
72
|
+
|
|
73
|
+
response = self.delete(url_for('api.dataservice', dataservice=dataservice))
|
|
74
|
+
self.assert204(response)
|
|
75
|
+
|
|
76
|
+
self.assertEqual(Dataservice.objects.count(), 1)
|
|
77
|
+
|
|
78
|
+
dataservice.reload()
|
|
79
|
+
self.assertEqual(dataservice.title, 'Updated title')
|
|
80
|
+
self.assertEqual(dataservice.base_api_url, 'https://example.org')
|
|
81
|
+
self.assertIsNotNone(dataservice.deleted_at)
|
|
82
|
+
|
|
83
|
+
# response = self.get(url_for('api.dataservice', dataservice=dataservice))
|
|
84
|
+
# self.assert410(response)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_dataservice_api_index(self):
|
|
88
|
+
self.login()
|
|
89
|
+
self.post(url_for('api.dataservices'), {
|
|
90
|
+
'title': 'B',
|
|
91
|
+
'base_api_url': 'https://example.org/B',
|
|
92
|
+
})
|
|
93
|
+
self.post(url_for('api.dataservices'), {
|
|
94
|
+
'title': 'C',
|
|
95
|
+
'base_api_url': 'https://example.org/C',
|
|
96
|
+
})
|
|
97
|
+
self.post(url_for('api.dataservices'), {
|
|
98
|
+
'title': 'A',
|
|
99
|
+
'base_api_url': 'https://example.org/A',
|
|
100
|
+
})
|
|
101
|
+
response = self.post(url_for('api.dataservices'), {
|
|
102
|
+
'title': 'X',
|
|
103
|
+
'base_api_url': 'https://example.org/X',
|
|
104
|
+
'private': True,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
self.assertEqual(Dataservice.objects.count(), 4)
|
|
108
|
+
|
|
109
|
+
response = self.get(url_for('api.dataservices'))
|
|
110
|
+
self.assert200(response)
|
|
111
|
+
|
|
112
|
+
self.assertEqual(response.json['previous_page'], None)
|
|
113
|
+
self.assertEqual(response.json['next_page'], None)
|
|
114
|
+
self.assertEqual(response.json['page'], 1)
|
|
115
|
+
self.assertEqual(response.json['total'], 3)
|
|
116
|
+
self.assertEqual(len(response.json['data']), 3)
|
|
117
|
+
self.assertEqual(response.json['data'][0]['title'], 'B')
|
|
118
|
+
self.assertEqual(response.json['data'][1]['title'], 'C')
|
|
119
|
+
self.assertEqual(response.json['data'][2]['title'], 'A')
|
|
120
|
+
|
|
121
|
+
response = self.get(url_for('api.dataservices', sort='title'))
|
|
122
|
+
self.assert200(response)
|
|
123
|
+
|
|
124
|
+
self.assertEqual(response.json['previous_page'], None)
|
|
125
|
+
self.assertEqual(response.json['next_page'], None)
|
|
126
|
+
self.assertEqual(response.json['page'], 1)
|
|
127
|
+
self.assertEqual(response.json['total'], 3)
|
|
128
|
+
self.assertEqual(len(response.json['data']), 3)
|
|
129
|
+
self.assertEqual(response.json['data'][0]['title'], 'A')
|
|
130
|
+
self.assertEqual(response.json['data'][1]['title'], 'B')
|
|
131
|
+
self.assertEqual(response.json['data'][2]['title'], 'C')
|
|
132
|
+
|
|
133
|
+
response = self.get(url_for('api.dataservices', sort='-title'))
|
|
134
|
+
self.assert200(response)
|
|
135
|
+
|
|
136
|
+
self.assertEqual(response.json['previous_page'], None)
|
|
137
|
+
self.assertEqual(response.json['next_page'], None)
|
|
138
|
+
self.assertEqual(response.json['page'], 1)
|
|
139
|
+
self.assertEqual(response.json['total'], 3)
|
|
140
|
+
self.assertEqual(len(response.json['data']), 3)
|
|
141
|
+
self.assertEqual(response.json['data'][0]['title'], 'C')
|
|
142
|
+
self.assertEqual(response.json['data'][1]['title'], 'B')
|
|
143
|
+
self.assertEqual(response.json['data'][2]['title'], 'A')
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
response = self.get(url_for('api.dataservices', page_size=1))
|
|
147
|
+
self.assert200(response)
|
|
148
|
+
|
|
149
|
+
self.assertEqual(response.json['previous_page'], None)
|
|
150
|
+
assert response.json['next_page'].endswith(url_for('api.dataservices', page_size=1, page=2))
|
|
151
|
+
self.assertEqual(response.json['page'], 1)
|
|
152
|
+
self.assertEqual(response.json['total'], 3)
|
|
153
|
+
self.assertEqual(len(response.json['data']), 1)
|
|
154
|
+
self.assertEqual(response.json['data'][0]['title'], 'B')
|
|
155
|
+
|
|
156
|
+
def test_dataservice_api_create_with_validation_error(self):
|
|
157
|
+
self.login()
|
|
158
|
+
response = self.post(url_for('api.dataservices'), {
|
|
159
|
+
'base_api_url': 'https://example.org',
|
|
160
|
+
})
|
|
161
|
+
self.assert400(response)
|
|
162
|
+
self.assertEqual(Dataservice.objects.count(), 0)
|
|
163
|
+
|
|
164
|
+
def test_dataservice_api_create_with_unkwown_license(self):
|
|
165
|
+
self.login()
|
|
166
|
+
response = self.post(url_for('api.dataservices'), {
|
|
167
|
+
'title': 'My title',
|
|
168
|
+
'base_api_url': 'https://example.org',
|
|
169
|
+
'license': 'unwkown-license',
|
|
170
|
+
})
|
|
171
|
+
self.assert400(response)
|
|
172
|
+
self.assertEqual(response.json['errors']['license'], ["Unknown reference 'unwkown-license'"])
|
|
173
|
+
self.assertEqual(Dataservice.objects.count(), 0)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_dataservice_api_create_with_unkwown_contact_point(self):
|
|
177
|
+
self.login()
|
|
178
|
+
|
|
179
|
+
response = self.post(url_for('api.dataservices'), {
|
|
180
|
+
'title': 'My title',
|
|
181
|
+
'base_api_url': 'https://example.org',
|
|
182
|
+
'contact_point': '66212433e42ab56639ad516e',
|
|
183
|
+
})
|
|
184
|
+
self.assert400(response)
|
|
185
|
+
self.assertEqual(response.json['errors']['contact_point'], ["Unknown reference '66212433e42ab56639ad516e'"])
|
|
186
|
+
self.assertEqual(Dataservice.objects.count(), 0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_dataservice_api_create_with_custom_user_or_org(self):
|
|
190
|
+
other = UserFactory()
|
|
191
|
+
other_member = Member(user=other, role='editor')
|
|
192
|
+
other_org = OrganizationFactory(members=[other_member])
|
|
193
|
+
|
|
194
|
+
me = self.login()
|
|
195
|
+
me_member = Member(user=me, role='editor')
|
|
196
|
+
me_org = OrganizationFactory(members=[me_member])
|
|
197
|
+
|
|
198
|
+
response = self.post(url_for('api.dataservices'), {
|
|
199
|
+
'title': 'My title',
|
|
200
|
+
'base_api_url': 'https://example.org',
|
|
201
|
+
'owner': other.id,
|
|
202
|
+
})
|
|
203
|
+
self.assert400(response)
|
|
204
|
+
self.assertEqual(response.json['errors']['owner'], [_("You can only set yourself as owner")])
|
|
205
|
+
self.assertEqual(Dataservice.objects.count(), 0)
|
|
206
|
+
|
|
207
|
+
response = self.post(url_for('api.dataservices'), {
|
|
208
|
+
'title': 'My title',
|
|
209
|
+
'base_api_url': 'https://example.org',
|
|
210
|
+
'organization': other_org.id,
|
|
211
|
+
})
|
|
212
|
+
self.assert400(response)
|
|
213
|
+
self.assertEqual(response.json['errors']['organization'], [_("Permission denied for this organization")])
|
|
214
|
+
self.assertEqual(Dataservice.objects.count(), 0)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
response = self.post(url_for('api.dataservices'), {
|
|
218
|
+
'title': 'My title',
|
|
219
|
+
'base_api_url': 'https://example.org',
|
|
220
|
+
'owner': me.id,
|
|
221
|
+
})
|
|
222
|
+
self.assert201(response)
|
|
223
|
+
dataservice = Dataservice.objects(id=response.json['id']).first()
|
|
224
|
+
self.assertEqual(dataservice.owner.id, me.id)
|
|
225
|
+
self.assertEqual(dataservice.organization, None)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
response = self.post(url_for('api.dataservices'), {
|
|
229
|
+
'title': 'My title',
|
|
230
|
+
'base_api_url': 'https://example.org',
|
|
231
|
+
'organization': me_org.id,
|
|
232
|
+
})
|
|
233
|
+
self.assert201(response)
|
|
234
|
+
dataservice = Dataservice.objects(id=response.json['id']).first()
|
|
235
|
+
self.assertEqual(dataservice.owner, None)
|
|
236
|
+
self.assertEqual(dataservice.organization.id, me_org.id)
|
udata/tests/plugin.py
CHANGED
|
@@ -192,6 +192,11 @@ class ApiClient(object):
|
|
|
192
192
|
return self.client.put(url, data or {}, *args, **kwargs)
|
|
193
193
|
return self.perform('put', url, data=data or {}, *args, **kwargs)
|
|
194
194
|
|
|
195
|
+
def patch(self, url, data=None, json=True, *args, **kwargs):
|
|
196
|
+
if not json:
|
|
197
|
+
return self.client.patch(url, data or {}, *args, **kwargs)
|
|
198
|
+
return self.perform('patch', url, data=data or {}, *args, **kwargs)
|
|
199
|
+
|
|
195
200
|
def delete(self, url, data=None, *args, **kwargs):
|
|
196
201
|
return self.perform('delete', url, data=data or {}, *args, **kwargs)
|
|
197
202
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: udata
|
|
3
|
-
Version: 8.0.1.
|
|
3
|
+
Version: 8.0.1.dev28979
|
|
4
4
|
Summary: Open data portal
|
|
5
5
|
Home-page: https://github.com/opendatateam/udata
|
|
6
6
|
Author: Opendata Team
|
|
@@ -137,8 +137,10 @@ It is collectively taken care of by members of the
|
|
|
137
137
|
|
|
138
138
|
## Current (in progress)
|
|
139
139
|
|
|
140
|
+
- Add dataservices in beta [#2986](https://github.com/opendatateam/udata/pull/2986)
|
|
140
141
|
- Remove deprecated `metrics_for` route [#3022](https://github.com/opendatateam/udata/pull/3022)
|
|
141
142
|
- Fix spatial coverage fetching perfs. Need to schedule `compute-geozones-metrics` [#3018](https://github.com/opendatateam/udata/pull/3018)
|
|
143
|
+
- Allow for series in CSW ISO 19139 DCAT backend [#3028](https://github.com/opendatateam/udata/pull/3028)
|
|
142
144
|
|
|
143
145
|
## 8.0.0 (2024-04-23)
|
|
144
146
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
tasks/__init__.py,sha256=CnVhb_TV-6nMhxVR6itnBmvuU2OSCs02AfNB4irVBTE,8132
|
|
2
2
|
tasks/helpers.py,sha256=k_HiuiEJNgQLvWdeHqczPOAcrYpFjEepBeKo7EQzY8M,994
|
|
3
3
|
udata/__init__.py,sha256=qPn-jr9CTcAEJ6KD95BxAliMbSxwtelLc8fyakV7UO0,101
|
|
4
|
+
udata/api_fields.py,sha256=myHkndhedV3g9cn2FWBu1Hx24Fy-fxaGedEP1ZMOoi0,11506
|
|
4
5
|
udata/app.py,sha256=6upwrImLaWrSYtsXPW1zH84_oRxp3B6XFuocMe2D6NU,7329
|
|
5
6
|
udata/assets.py,sha256=aMa-MnAEXVSTavptSn2V8sUE6jL_N0MrYCQ6_QpsuHs,645
|
|
6
7
|
udata/entrypoints.py,sha256=8bZUvZ8ICO3kZxa8xDUaen6YS_OAGNKHFvaD7d8BOk0,2687
|
|
@@ -9,7 +10,7 @@ udata/factories.py,sha256=6en2qdHHoR6rEvNEUeH_0-T-vR7x7GUBGgNEm9vuylE,492
|
|
|
9
10
|
udata/i18n.py,sha256=E-JoY57Cv4vgPvChkWjpwZAbuYVI8e6BO6raeu_3_Pw,8352
|
|
10
11
|
udata/mail.py,sha256=dAMcbEtk5e54alpQezvF5adDrRPgdaT36QEdHD_5v50,2145
|
|
11
12
|
udata/rdf.py,sha256=QiHcK2iklbtPkl6UFdBxTj0oaiY41en9k-kmbIVJ8Xw,9893
|
|
12
|
-
udata/routing.py,sha256=
|
|
13
|
+
udata/routing.py,sha256=Qhpf5p97fs1SoJXtDctc1FPk0MeOKLn_C0Z1dP4ZNJA,7234
|
|
13
14
|
udata/sentry.py,sha256=KiZz0PpmYpZMvykH9UAbHpF4xBY0Q-8DeiEbXEHDUdw,2683
|
|
14
15
|
udata/settings.py,sha256=5cU5_oiPiuS8RS98F9qfCp37qbjjN_g8ipoyZjDQP0M,17298
|
|
15
16
|
udata/sitemap.py,sha256=pjtR2lU3gRHvK8l1Lw8B9wrqMMTYC5adUfFh0pUp_Q4,977
|
|
@@ -23,11 +24,11 @@ udata/worker.py,sha256=K-Wafye5-uXP4kQlffRKws2J9YbJ6m6n2QjcVsY8Nsg,118
|
|
|
23
24
|
udata/wsgi.py,sha256=P7AJvZ5JqY4uRSBOzaFiBniChWIU9RVQ-Y0PN4vCCMY,77
|
|
24
25
|
udata/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
26
|
udata/admin/views.py,sha256=wMlpnC1aINW-6JDk6-kQXhcTYBZH-5wajEuWzVDcIKA,331
|
|
26
|
-
udata/api/__init__.py,sha256=
|
|
27
|
+
udata/api/__init__.py,sha256=I40g3PLG4s-zGNQfjB8_KQGzfs3ZyUrZdajd20vQ9ks,11388
|
|
27
28
|
udata/api/commands.py,sha256=oK2p1VdUvULDdYuvYYpYvY_bdkPJy-KfROfoX71oOuA,3277
|
|
28
29
|
udata/api/errors.py,sha256=Sy_f3WVrNTUPZjCOogIVgocDdUjnKz149KDi4mMA_Lg,240
|
|
29
30
|
udata/api/fields.py,sha256=l-Fa27-easR86qgof2bk130jq1N1pNUgGmQzok1UI3Q,3094
|
|
30
|
-
udata/api/oauth2.py,sha256=
|
|
31
|
+
udata/api/oauth2.py,sha256=LTtkpkNYrPl_7xf-JmP-TLwrk_RHajnHDFKo-3NPEd8,11780
|
|
31
32
|
udata/api/parsers.py,sha256=Fo4krCaFao0D1QNqKpjWiFVvKVLd9b_2mon6RbbOXls,1485
|
|
32
33
|
udata/api/signals.py,sha256=9zcw4NDdpJwhgsS5UyLtnls1w_RfqghYFULza6d4-cw,162
|
|
33
34
|
udata/auth/__init__.py,sha256=ziR6gzkE1V4__3ynjE-_IPWHzmapOsZIrnaDAjDijsM,1958
|
|
@@ -52,7 +53,7 @@ udata/commands/worker.py,sha256=zek8YRxCESrZIULhx9D9RQOPFc4WwhYm2sF7-PPbEKY,4028
|
|
|
52
53
|
udata/commands/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
54
|
udata/commands/tests/fixtures.py,sha256=h78grSpZDgOwvmezyD3qBpiSnpxaQ9tDeQOr3sB8EiY,1581
|
|
54
55
|
udata/core/__init__.py,sha256=n7TBP0lkYJl-BLoc67V5fyaawrWmsrWG_mLIF49UFOY,385
|
|
55
|
-
udata/core/owned.py,sha256=
|
|
56
|
+
udata/core/owned.py,sha256=ZOoRacTWHiCgZDEENu_7vRcoemZrNLmVVI3PVLuLK5E,4453
|
|
56
57
|
udata/core/activity/__init__.py,sha256=4vB92owvzwn2VVxbFWGNcfESb6izDzvbj6lmLH4ssrU,299
|
|
57
58
|
udata/core/activity/api.py,sha256=ohDEbzhbsmJ6YHQh3Aa8wYctaDQGuwKdg0kjh3lC028,3276
|
|
58
59
|
udata/core/activity/models.py,sha256=kzy9kcixX3DUo-2YpVjGXVHR4cOZMl7AsPiDBZ52EDs,2118
|
|
@@ -64,7 +65,7 @@ udata/core/badges/commands.py,sha256=NMihQH6ABUlkYpR2lNFW4dkeaWOjpijW7voudww8zW4
|
|
|
64
65
|
udata/core/badges/factories.py,sha256=rDQC28fKLtYA6l69EqiyOQyN4C2jVj87EdKBEo4sYsw,297
|
|
65
66
|
udata/core/badges/fields.py,sha256=rpeyZcw5-oU8FZXPnzU9-rGrwW5vCyhwgei7aAR-n3I,255
|
|
66
67
|
udata/core/badges/forms.py,sha256=8z28_rln9duvJ7IvKx9IFO9DAMe3cU2bv0FQRdiMFEw,511
|
|
67
|
-
udata/core/badges/models.py,sha256=
|
|
68
|
+
udata/core/badges/models.py,sha256=GtfqdeyPkve4HZa4mOIhVcHIXr5IOFnASEFqazi-oQg,3229
|
|
68
69
|
udata/core/badges/permissions.py,sha256=5jFKFG4OlVp0pL0xSa7AGO0Ttn0QMsy9xEY2jTfKVmE,552
|
|
69
70
|
udata/core/badges/signals.py,sha256=T12kgKO1lmIbdXlKnRmedYRnJzC2PZIMkrtcA1U2mNo,237
|
|
70
71
|
udata/core/badges/tasks.py,sha256=LA1Sd0B0uUa6OANqmx0t-kDNK99uGePwwtilsdj9Evw,431
|
|
@@ -77,6 +78,9 @@ udata/core/contact_point/api_fields.py,sha256=KQADF7Drnd1uT9ZWjLRKQPJh6GdGUXJkYf
|
|
|
77
78
|
udata/core/contact_point/factories.py,sha256=ATuV1seBCGKY3CzvPDG5nxfBBqHu-3YtER0_fiQJx6c,248
|
|
78
79
|
udata/core/contact_point/forms.py,sha256=ggLhSJ1IRn5MclrhydckjAxwr4fFZxgAD4huSSucSsA,598
|
|
79
80
|
udata/core/contact_point/models.py,sha256=NlNKureCpzgTLJuGviZPjNx-ABYRp4j2L-ur9Gmixao,324
|
|
81
|
+
udata/core/dataservices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
|
+
udata/core/dataservices/api.py,sha256=j0_fQdq_O3zBqY7PGapmNbeMjb2sS4x8jr3cTM-4i_Q,2803
|
|
83
|
+
udata/core/dataservices/models.py,sha256=G8m-YKHf32DdA-3Q4m-hlQCLZ8ZZUFKaRFD0YZtqvbE,4037
|
|
80
84
|
udata/core/dataset/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
85
|
udata/core/dataset/actions.py,sha256=3pzBg_qOR-w7fwPpTOKUHXWC9lkjALbOn1UQFmmT-s0,1199
|
|
82
86
|
udata/core/dataset/activities.py,sha256=qQnHNL0hOB1IGtQl7JsnVOiUsWT0gm-pts9uDyR3bvU,1536
|
|
@@ -123,7 +127,7 @@ udata/core/jobs/forms.py,sha256=CB_z8JSyvSSJX1XhohsQ1S3kdOhTMgzsFTofso7AD60,1495
|
|
|
123
127
|
udata/core/jobs/models.py,sha256=3vRfBp8e7KqJgKxTSSeMgfsMu07rAz1o_IfsYBFk9o4,1325
|
|
124
128
|
udata/core/metrics/__init__.py,sha256=Q3nZVl7no0E1ygz7zNJH-KCa-m9FV6wAH2_y6zimcek,397
|
|
125
129
|
udata/core/metrics/commands.py,sha256=74L6PUJyaTnr4w-_redN9ucY0v-3Vi4QumWHKZ__DYE,5143
|
|
126
|
-
udata/core/metrics/models.py,sha256=
|
|
130
|
+
udata/core/metrics/models.py,sha256=k4kgKjAViQ1XO1F6n-iRfF4eT9cHoGqjQP4e8vC2vmk,291
|
|
127
131
|
udata/core/metrics/signals.py,sha256=Xs_k-9lWzsoZU5Joli-41FNgvIVvbAejkjSFB6DHxh0,176
|
|
128
132
|
udata/core/metrics/tasks.py,sha256=Z7La3-zPB5j6qJoPKr_MgjNZhqscZCmNLPa711ZBkdY,797
|
|
129
133
|
udata/core/organization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -137,7 +141,7 @@ udata/core/organization/csv.py,sha256=x0afMfhWYvx_karwuw5jXqBhMbzkrsiEuAq1wTVurZ
|
|
|
137
141
|
udata/core/organization/factories.py,sha256=5BABVcDhEChRhJsDfCDm8WyJG4l9j3H1_OFZa3VtlVs,646
|
|
138
142
|
udata/core/organization/forms.py,sha256=JXXv4tQGbIbICti7RXLVZdnc6VujATmLhDrHIsFxBME,3550
|
|
139
143
|
udata/core/organization/metrics.py,sha256=45NDcsFV-oJJQUuq6AyIDXjR-RNubwYWF5-Ke8KrPDY,777
|
|
140
|
-
udata/core/organization/models.py,sha256=
|
|
144
|
+
udata/core/organization/models.py,sha256=aLEqb7F1J7I9TKzSrODMB2Ecf7IxMqYmlgrZvLIx1k8,8387
|
|
141
145
|
udata/core/organization/notifications.py,sha256=j-2LIHZ5y5QuVietWAWOrAqf4v1HMCtSDZ0w7V-z_1c,763
|
|
142
146
|
udata/core/organization/permissions.py,sha256=cNIPiPgXVW2hvKqbuKHA_62tX2xaT8jiVJ3BEfnsHn0,1299
|
|
143
147
|
udata/core/organization/rdf.py,sha256=ZSoxyZDj_5g6hv7lbTIy6bIW3xwvQy27rWNgJwtZ6LE,1762
|
|
@@ -273,7 +277,7 @@ udata/harvest/signals.py,sha256=wlXTi1E7rIVyNvxw0yUqyN5gF3thg276LAOmAF9vDJY,1338
|
|
|
273
277
|
udata/harvest/tasks.py,sha256=0VhefKCQJSU_puTpdKOpvt3WORXHAFWGEB-R_MhB12M,1981
|
|
274
278
|
udata/harvest/backends/__init__.py,sha256=qcLhHKWO97TeWd93ZwymG_Cc9FO7sMM7h4fs6XYdtS8,447
|
|
275
279
|
udata/harvest/backends/base.py,sha256=oaPQcQ0onIXH5ofUtWH5sM6_5_wSBLawHSOjeeoG6jQ,12258
|
|
276
|
-
udata/harvest/backends/dcat.py,sha256=
|
|
280
|
+
udata/harvest/backends/dcat.py,sha256=q5v6sUm8xBFYH437S3MUTAu40Ecka1Y6Oj6VB6xl2B4,15300
|
|
277
281
|
udata/harvest/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
278
282
|
udata/harvest/tests/factories.py,sha256=CbQORC1OJ1_Agtv_3LjCXysNumjMYlROwZPSEAHo8sM,2005
|
|
279
283
|
udata/harvest/tests/test_actions.py,sha256=7xSpouCAcf5p_bd38zHCyPN7sKWUUZXA7IlpI-yNVrQ,27603
|
|
@@ -560,7 +564,7 @@ udata/tests/__init__.py,sha256=BezijRRI6dPPiEWWjLsJFLrhhfsTZgcctcxhVfp4j70,2332
|
|
|
560
564
|
udata/tests/es-fake-result.json,sha256=z0CX9Gs-NRj49dmtdFvw8ZKsAbMhDt97Na6JX3ucX34,3155
|
|
561
565
|
udata/tests/helpers.py,sha256=aaifyevJ1Z8CZ8htRrl8OCG5hGcaHfj0lL8iMEKds9w,6022
|
|
562
566
|
udata/tests/models.py,sha256=_V0smMb1Z6p3aZv6PorzNN-HiNt_B46Ox1fqXrTJEqk,238
|
|
563
|
-
udata/tests/plugin.py,sha256=
|
|
567
|
+
udata/tests/plugin.py,sha256=DXP0H1Sm2fc-okGSKKBOgH8D8x4fl4_1OVhakgQLz4w,11278
|
|
564
568
|
udata/tests/schemas.json,sha256=szM1jDpkogfOG4xWbjIGjLgG8l9-ZyE3JKQtecJyD1E,4990
|
|
565
569
|
udata/tests/test_activity.py,sha256=spWfhueuLze0kD-pAnegiL3_Kv5io155jQuFI4sjN7I,3258
|
|
566
570
|
udata/tests/test_discussions.py,sha256=zPvKOdcTNGXrvHFp9zqjhKE2fqgUkhb_1F98egXYCL0,31036
|
|
@@ -579,10 +583,11 @@ udata/tests/test_topics.py,sha256=r7Y0BW0Z5obld9ASs2Ck9AhykgBtmMedZmL2Bfz_rDw,13
|
|
|
579
583
|
udata/tests/test_transfer.py,sha256=Y0adyR3CswK2Vvao7DgpP_19b8K6XwNO2dvbKlmnlD0,8036
|
|
580
584
|
udata/tests/test_uris.py,sha256=FQJvz7RXkr21tZ7rZp2uwgZUHk88PxhYId-jXSAl9GM,8519
|
|
581
585
|
udata/tests/test_utils.py,sha256=5awzhJlbnLga0mRXNR2mBGW_kGuAIoQLEZEMQRKuaIM,7944
|
|
582
|
-
udata/tests/api/__init__.py,sha256=
|
|
586
|
+
udata/tests/api/__init__.py,sha256=gIIB9OPiSs1A-u1bEIHlOesaCQzJ08SOLPuJd34LesE,1097
|
|
583
587
|
udata/tests/api/test_auth_api.py,sha256=3Zhn2A29poZIcCJ_R9_-LkR3xOFUTw1aTquiZVXQ2F0,20306
|
|
584
588
|
udata/tests/api/test_base_api.py,sha256=DRX5nuFIj51GFmMIAxUzoW1yiq1apNgr1vS4U4agzeg,2319
|
|
585
589
|
udata/tests/api/test_contact_points.py,sha256=MJm8B06iaUqIZCqxll3NViFwUCxwqZZ4u9e9s1h8MgU,1056
|
|
590
|
+
udata/tests/api/test_dataservices_api.py,sha256=eIo0b18BSSRocyf9pTChzDnKotxxLMq8R1tX53JdH_M,9743
|
|
586
591
|
udata/tests/api/test_datasets_api.py,sha256=zqRtC61NvYUNum4kBTjA1W87JAIobA4k564rm-U6c98,81704
|
|
587
592
|
udata/tests/api/test_fields.py,sha256=OW85Z5MES5HeWOpapeem8OvR1cIcrqW-xMWpdZO4LZ8,1033
|
|
588
593
|
udata/tests/api/test_follow_api.py,sha256=0h54P_Dfbo07u6tg0Rbai1WWgWb19ZLN2HGv4oLCWfg,3383
|
|
@@ -676,9 +681,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=uttB2K8VsqzkEQG-5HfTtFms_3LtV9
|
|
|
676
681
|
udata/translations/pt/LC_MESSAGES/udata.po,sha256=8Ql1Lp7Z9KLnvp-qRxw-NhFu1p35Xj-q6Jg9JHsYhcw,43733
|
|
677
682
|
udata/translations/sr/LC_MESSAGES/udata.mo,sha256=US8beNIMPxP5h-zD_jfP1TheDDd4DdRVS5UIiY5XVZ8,28553
|
|
678
683
|
udata/translations/sr/LC_MESSAGES/udata.po,sha256=TM0yMDvKRljyOzgZZMlTX6OfpF6OC4Ngf_9Zc8n6ayA,50313
|
|
679
|
-
udata-8.0.1.
|
|
680
|
-
udata-8.0.1.
|
|
681
|
-
udata-8.0.1.
|
|
682
|
-
udata-8.0.1.
|
|
683
|
-
udata-8.0.1.
|
|
684
|
-
udata-8.0.1.
|
|
684
|
+
udata-8.0.1.dev28979.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
|
|
685
|
+
udata-8.0.1.dev28979.dist-info/METADATA,sha256=fpTuwFUj0yuxHPnVq5azKMEVD0IBEYgrwABWlmCkFbI,122487
|
|
686
|
+
udata-8.0.1.dev28979.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
687
|
+
udata-8.0.1.dev28979.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
|
|
688
|
+
udata-8.0.1.dev28979.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
|
|
689
|
+
udata-8.0.1.dev28979.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|