udata 8.0.1.dev28859__py2.py3-none-any.whl → 8.0.1.dev28928__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 -4
- 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/commands.py +18 -3
- udata/core/metrics/models.py +2 -3
- udata/core/organization/models.py +3 -1
- udata/core/owned.py +39 -2
- udata/core/spatial/api.py +1 -6
- udata/core/spatial/models.py +7 -2
- udata/core/spatial/tasks.py +7 -0
- udata/core/spatial/tests/test_api.py +26 -0
- udata/routing.py +6 -0
- udata/tasks.py +1 -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.dev28859.dist-info → udata-8.0.1.dev28928.dist-info}/METADATA +4 -2
- {udata-8.0.1.dev28859.dist-info → udata-8.0.1.dev28928.dist-info}/RECORD +26 -21
- udata/core/metrics/api.py +0 -10
- {udata-8.0.1.dev28859.dist-info → udata-8.0.1.dev28928.dist-info}/LICENSE +0 -0
- {udata-8.0.1.dev28859.dist-info → udata-8.0.1.dev28928.dist-info}/WHEEL +0 -0
- {udata-8.0.1.dev28859.dist-info → udata-8.0.1.dev28928.dist-info}/entry_points.txt +0 -0
- {udata-8.0.1.dev28859.dist-info → udata-8.0.1.dev28928.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
|
|
|
@@ -321,10 +322,10 @@ def init_app(app):
|
|
|
321
322
|
# Load all core APIs
|
|
322
323
|
import udata.core.activity.api # noqa
|
|
323
324
|
import udata.core.spatial.api # noqa
|
|
324
|
-
import udata.core.metrics.api # noqa
|
|
325
325
|
import udata.core.user.api # noqa
|
|
326
326
|
import udata.core.dataset.api # noqa
|
|
327
327
|
import udata.core.dataset.apiv2 # noqa
|
|
328
|
+
import udata.core.dataservices.api # noqa
|
|
328
329
|
import udata.core.discussions.api # noqa
|
|
329
330
|
import udata.core.reuse.api # noqa
|
|
330
331
|
import udata.core.reuse.apiv2 # noqa
|
|
@@ -351,5 +352,6 @@ def init_app(app):
|
|
|
351
352
|
app.register_blueprint(apiv1_blueprint)
|
|
352
353
|
app.register_blueprint(apiv2_blueprint)
|
|
353
354
|
|
|
354
|
-
oauth2
|
|
355
|
+
from udata.api.oauth2 import init_app as oauth2_init_app
|
|
356
|
+
oauth2_init_app(app)
|
|
355
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/commands.py
CHANGED
|
@@ -5,7 +5,7 @@ import click
|
|
|
5
5
|
from flask import current_app
|
|
6
6
|
|
|
7
7
|
from udata.commands import cli, success
|
|
8
|
-
from udata.models import User, Dataset, Reuse, Organization, Site
|
|
8
|
+
from udata.models import User, Dataset, Reuse, Organization, Site, GeoZone
|
|
9
9
|
|
|
10
10
|
log = logging.getLogger(__name__)
|
|
11
11
|
|
|
@@ -24,11 +24,12 @@ def grp():
|
|
|
24
24
|
help='Compute datasets metrics')
|
|
25
25
|
@click.option('-r', '--reuses', is_flag=True, help='Compute reuses metrics')
|
|
26
26
|
@click.option('-u', '--users', is_flag=True, help='Compute users metrics')
|
|
27
|
+
@click.option('-g', '--geozones', is_flag=True, help='Compute geo levels metrics')
|
|
27
28
|
@click.option('--drop', is_flag=True, help='Clear old metrics before computing new ones')
|
|
28
29
|
def update(site=False, organizations=False, users=False, datasets=False,
|
|
29
|
-
reuses=False, drop=False):
|
|
30
|
+
reuses=False, geozones = False, drop=False):
|
|
30
31
|
'''Update all metrics for the current date'''
|
|
31
|
-
do_all = not any((site, organizations, users, datasets, reuses))
|
|
32
|
+
do_all = not any((site, organizations, users, datasets, reuses, geozones))
|
|
32
33
|
|
|
33
34
|
if do_all or site:
|
|
34
35
|
log.info('Update site metrics')
|
|
@@ -114,4 +115,18 @@ def update(site=False, organizations=False, users=False, datasets=False,
|
|
|
114
115
|
except Exception as e:
|
|
115
116
|
log.info(f'Error during update: {e}')
|
|
116
117
|
continue
|
|
118
|
+
|
|
119
|
+
if do_all or geozones:
|
|
120
|
+
log.info('Update GeoZone metrics')
|
|
121
|
+
all_geozones = GeoZone.objects.timeout(False)
|
|
122
|
+
with click.progressbar(all_geozones, length=GeoZone.objects.count()) as geozones_bar:
|
|
123
|
+
for geozone in geozones_bar:
|
|
124
|
+
try:
|
|
125
|
+
if drop:
|
|
126
|
+
geozone.metrics.clear()
|
|
127
|
+
geozone.count_datasets()
|
|
128
|
+
except Exception as e:
|
|
129
|
+
log.info(f'Error during update: {e}')
|
|
130
|
+
continue
|
|
131
|
+
|
|
117
132
|
success('All metrics have been updated')
|
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/core/spatial/api.py
CHANGED
|
@@ -162,11 +162,6 @@ class SpatialCoverageAPI(API):
|
|
|
162
162
|
features = []
|
|
163
163
|
|
|
164
164
|
for zone in GeoZone.objects(level=level.id):
|
|
165
|
-
# fetch nested levels IDs
|
|
166
|
-
ids = []
|
|
167
|
-
ids.append(zone.id)
|
|
168
|
-
# Count datasets in zone
|
|
169
|
-
nb_datasets = Dataset.objects(spatial__zones__in=ids).count()
|
|
170
165
|
features.append({
|
|
171
166
|
'id': zone.id,
|
|
172
167
|
'type': 'Feature',
|
|
@@ -174,7 +169,7 @@ class SpatialCoverageAPI(API):
|
|
|
174
169
|
'name': _(zone.name),
|
|
175
170
|
'code': zone.code,
|
|
176
171
|
'uri': zone.uri,
|
|
177
|
-
'datasets':
|
|
172
|
+
'datasets': zone.metrics.get('datasets', 0)
|
|
178
173
|
}
|
|
179
174
|
})
|
|
180
175
|
|
udata/core/spatial/models.py
CHANGED
|
@@ -3,6 +3,7 @@ from werkzeug.local import LocalProxy
|
|
|
3
3
|
from werkzeug.utils import cached_property
|
|
4
4
|
|
|
5
5
|
from udata.app import cache
|
|
6
|
+
from udata.core.metrics.models import WithMetrics
|
|
6
7
|
from udata.uris import endpoint_for
|
|
7
8
|
from udata.i18n import _, get_locale, language
|
|
8
9
|
from udata.mongo import db
|
|
@@ -21,7 +22,6 @@ class GeoLevel(db.Document):
|
|
|
21
22
|
max_value=ADMIN_LEVEL_MAX,
|
|
22
23
|
default=100)
|
|
23
24
|
|
|
24
|
-
|
|
25
25
|
class GeoZoneQuerySet(db.BaseQuerySet):
|
|
26
26
|
|
|
27
27
|
def resolve(self, geoid, id_only=False):
|
|
@@ -40,7 +40,7 @@ class GeoZoneQuerySet(db.BaseQuerySet):
|
|
|
40
40
|
return result.id if id_only and result else result
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
class GeoZone(db.Document):
|
|
43
|
+
class GeoZone(WithMetrics, db.Document):
|
|
44
44
|
SEPARATOR = ':'
|
|
45
45
|
|
|
46
46
|
id = db.StringField(primary_key=True)
|
|
@@ -101,6 +101,11 @@ class GeoZone(db.Document):
|
|
|
101
101
|
def external_url(self):
|
|
102
102
|
return endpoint_for('territories.territory', territory=self, _external=True)
|
|
103
103
|
|
|
104
|
+
def count_datasets(self):
|
|
105
|
+
from udata.models import Dataset
|
|
106
|
+
self.metrics['datasets'] = Dataset.objects(spatial__zones=self.id).visible().count()
|
|
107
|
+
self.save()
|
|
108
|
+
|
|
104
109
|
def toGeoJSON(self):
|
|
105
110
|
return {
|
|
106
111
|
'id': self.id,
|
|
@@ -10,6 +10,7 @@ from udata.core.dataset.factories import DatasetFactory
|
|
|
10
10
|
from udata.core.spatial.factories import (
|
|
11
11
|
SpatialCoverageFactory, GeoZoneFactory, GeoLevelFactory
|
|
12
12
|
)
|
|
13
|
+
from udata.core.spatial.tasks import compute_geozones_metrics
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class SpatialApiTest(APITestCase):
|
|
@@ -229,6 +230,31 @@ class SpatialApiTest(APITestCase):
|
|
|
229
230
|
'features': [],
|
|
230
231
|
})
|
|
231
232
|
|
|
233
|
+
def test_coverage_datasets_count(self):
|
|
234
|
+
GeoLevelFactory(id='fr:commune')
|
|
235
|
+
paris = GeoZoneFactory(
|
|
236
|
+
id='fr:commune:75056', level='fr:commune',
|
|
237
|
+
name='Paris', code='75056')
|
|
238
|
+
arles = GeoZoneFactory(
|
|
239
|
+
id='fr:commune:13004', level='fr:commune',
|
|
240
|
+
name='Arles', code='13004')
|
|
241
|
+
|
|
242
|
+
for _ in range(3):
|
|
243
|
+
DatasetFactory(
|
|
244
|
+
spatial=SpatialCoverageFactory(zones=[paris.id]))
|
|
245
|
+
for _ in range(2):
|
|
246
|
+
DatasetFactory(
|
|
247
|
+
spatial=SpatialCoverageFactory(zones=[arles.id]))
|
|
248
|
+
|
|
249
|
+
compute_geozones_metrics()
|
|
250
|
+
|
|
251
|
+
response = self.get(url_for('api.spatial_coverage', level='fr:commune'))
|
|
252
|
+
self.assert200(response)
|
|
253
|
+
self.assertEqual(response.json['features'][0]['id'], 'fr:commune:13004')
|
|
254
|
+
self.assertEqual(response.json['features'][0]['properties']['datasets'], 2)
|
|
255
|
+
self.assertEqual(response.json['features'][1]['id'], 'fr:commune:75056')
|
|
256
|
+
self.assertEqual(response.json['features'][1]['properties']['datasets'], 3)
|
|
257
|
+
|
|
232
258
|
|
|
233
259
|
class SpatialTerritoriesApiTest(APITestCase):
|
|
234
260
|
modules = []
|
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/tasks.py
CHANGED
|
@@ -162,6 +162,7 @@ def init_app(app):
|
|
|
162
162
|
import udata.core.tags.tasks # noqa
|
|
163
163
|
import udata.core.activity.tasks # noqa
|
|
164
164
|
import udata.core.dataset.tasks # noqa
|
|
165
|
+
import udata.core.spatial.tasks # noqa
|
|
165
166
|
import udata.core.reuse.tasks # noqa
|
|
166
167
|
import udata.core.user.tasks # noqa
|
|
167
168
|
import udata.core.organization.tasks # noqa
|
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.dev28928
|
|
4
4
|
Summary: Open data portal
|
|
5
5
|
Home-page: https://github.com/opendatateam/udata
|
|
6
6
|
Author: Opendata Team
|
|
@@ -137,7 +137,9 @@ It is collectively taken care of by members of the
|
|
|
137
137
|
|
|
138
138
|
## Current (in progress)
|
|
139
139
|
|
|
140
|
-
-
|
|
140
|
+
- Add dataservices in beta [#2986](https://github.com/opendatateam/udata/pull/2986)
|
|
141
|
+
- Remove deprecated `metrics_for` route [#3022](https://github.com/opendatateam/udata/pull/3022)
|
|
142
|
+
- Fix spatial coverage fetching perfs. Need to schedule `compute-geozones-metrics` [#3018](https://github.com/opendatateam/udata/pull/3018)
|
|
141
143
|
|
|
142
144
|
## 8.0.0 (2024-04-23)
|
|
143
145
|
|
|
@@ -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,12 +10,12 @@ 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
|
|
16
17
|
udata/tags.py,sha256=8MvgyjfUz8O71tCV0V1fQV4_0C2P-SLFrm2IA2QfNDs,595
|
|
17
|
-
udata/tasks.py,sha256=
|
|
18
|
+
udata/tasks.py,sha256=Dyjoxp6_IuPZSnKi6KmaFrFvnyCM5EF5g_W-jG0qYIo,4930
|
|
18
19
|
udata/terms.md,sha256=nFx978tUQ3vTEv6POykXaZvcQ5e_gcvmO4ZgcfbSWXo,187
|
|
19
20
|
udata/tracking.py,sha256=iaGqN9NCqcEAayemJe9a-3N8fO--ixBXCUvQ7jRxbhA,359
|
|
20
21
|
udata/uris.py,sha256=_RaQSy6ghs2IHJQkDFaYpM2e--VA3p1D6awm5ybueAQ,3489
|
|
@@ -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
|
|
@@ -122,9 +126,8 @@ udata/core/jobs/commands.py,sha256=kYoSluhbIG7MwnCSyO7ptD1V9LGwNjlabqlXSQ4hZq4,4
|
|
|
122
126
|
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
|
-
udata/core/metrics/
|
|
126
|
-
udata/core/metrics/
|
|
127
|
-
udata/core/metrics/models.py,sha256=v0xlLxl2Y9QdILW4DTZ1WzUy2a4XTNepdLNKmb8T8LY,287
|
|
129
|
+
udata/core/metrics/commands.py,sha256=74L6PUJyaTnr4w-_redN9ucY0v-3Vi4QumWHKZ__DYE,5143
|
|
130
|
+
udata/core/metrics/models.py,sha256=k4kgKjAViQ1XO1F6n-iRfF4eT9cHoGqjQP4e8vC2vmk,291
|
|
128
131
|
udata/core/metrics/signals.py,sha256=Xs_k-9lWzsoZU5Joli-41FNgvIVvbAejkjSFB6DHxh0,176
|
|
129
132
|
udata/core/metrics/tasks.py,sha256=Z7La3-zPB5j6qJoPKr_MgjNZhqscZCmNLPa711ZBkdY,797
|
|
130
133
|
udata/core/organization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -138,7 +141,7 @@ udata/core/organization/csv.py,sha256=x0afMfhWYvx_karwuw5jXqBhMbzkrsiEuAq1wTVurZ
|
|
|
138
141
|
udata/core/organization/factories.py,sha256=5BABVcDhEChRhJsDfCDm8WyJG4l9j3H1_OFZa3VtlVs,646
|
|
139
142
|
udata/core/organization/forms.py,sha256=JXXv4tQGbIbICti7RXLVZdnc6VujATmLhDrHIsFxBME,3550
|
|
140
143
|
udata/core/organization/metrics.py,sha256=45NDcsFV-oJJQUuq6AyIDXjR-RNubwYWF5-Ke8KrPDY,777
|
|
141
|
-
udata/core/organization/models.py,sha256=
|
|
144
|
+
udata/core/organization/models.py,sha256=aLEqb7F1J7I9TKzSrODMB2Ecf7IxMqYmlgrZvLIx1k8,8387
|
|
142
145
|
udata/core/organization/notifications.py,sha256=j-2LIHZ5y5QuVietWAWOrAqf4v1HMCtSDZ0w7V-z_1c,763
|
|
143
146
|
udata/core/organization/permissions.py,sha256=cNIPiPgXVW2hvKqbuKHA_62tX2xaT8jiVJ3BEfnsHn0,1299
|
|
144
147
|
udata/core/organization/rdf.py,sha256=ZSoxyZDj_5g6hv7lbTIy6bIW3xwvQy27rWNgJwtZ6LE,1762
|
|
@@ -184,17 +187,18 @@ udata/core/spam/signals.py,sha256=4VVLyC2I39LFAot4P56nHzY0xumjMBDz_N0Ff_kgBd0,15
|
|
|
184
187
|
udata/core/spam/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
185
188
|
udata/core/spam/tests/test_spam.py,sha256=W1Ck_rsnURhFi0fy5xOO0CPpW9MuUFbr-NmPZdk5R4Q,676
|
|
186
189
|
udata/core/spatial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
187
|
-
udata/core/spatial/api.py,sha256=
|
|
190
|
+
udata/core/spatial/api.py,sha256=8viSuSSfjZY6GrPj5Ax9WJIYr9KD4YBWAqSd3vhQz50,5666
|
|
188
191
|
udata/core/spatial/api_fields.py,sha256=ymGK3FyE2KNlASOU5GiPgZGeBjjOqnOyCOcRCbuw3D8,2629
|
|
189
192
|
udata/core/spatial/commands.py,sha256=qdJ4mUg3NUTrGx6L5MKPh1usvxmeCn8xMCZNGKyjWYE,6914
|
|
190
193
|
udata/core/spatial/constants.py,sha256=u_4OwAAVYqT0VX6nw_Hc4wIRUPcYww19EFJcnWuMZNo,146
|
|
191
194
|
udata/core/spatial/factories.py,sha256=GxUU8nRtSoGPw2m3Q-al0PYps2oSnlqC-bM7Eeu14rk,3288
|
|
192
195
|
udata/core/spatial/forms.py,sha256=tXlYKge5Rc8L8pOjfUo1-9howgVJZDAh7q8xTIlbBww,3218
|
|
193
196
|
udata/core/spatial/geoids.py,sha256=UqCto4dtQYPXOxyG7sx_0npzM6yvId40ngw3eAlmioQ,1159
|
|
194
|
-
udata/core/spatial/models.py,sha256=
|
|
197
|
+
udata/core/spatial/models.py,sha256=xAc44TRnoThYvHvHVHvKZWHHQVfTWbT6HQr_Q79oSnM,5268
|
|
198
|
+
udata/core/spatial/tasks.py,sha256=UPdq3bWR-xW87BWaPVbWVi1IEC3jJyiuFoW2NmvEYQs,228
|
|
195
199
|
udata/core/spatial/translations.py,sha256=7BjZAMPObm1fwM5U_wnrlpcG-1FtMoS6Z5E3h3Y3sog,533
|
|
196
200
|
udata/core/spatial/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
197
|
-
udata/core/spatial/tests/test_api.py,sha256=
|
|
201
|
+
udata/core/spatial/tests/test_api.py,sha256=eU-hwsUFtzguN4JfqA-R_qH_7ZNv5odhvRYPU6nTvvE,11317
|
|
198
202
|
udata/core/spatial/tests/test_fields.py,sha256=23MBhrkGhYkQtWcRbj8TsPsUPT5QWhR-C9sohEKpQrU,9744
|
|
199
203
|
udata/core/spatial/tests/test_geoid.py,sha256=ovVphCxHb5a-iWl7eoLASRAFUY0CGfNEq-MuqHqTPgE,1533
|
|
200
204
|
udata/core/spatial/tests/test_models.py,sha256=SZx3NhngXWgVon5BKtP0pPPEdvxGTvHQlYFE-fewzTU,951
|
|
@@ -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.dev28928.dist-info/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
|
|
685
|
+
udata-8.0.1.dev28928.dist-info/METADATA,sha256=tB-T4SHz3x5TYr-Ztafc3klwqZmf9NdtIQD6Ak1GYzY,122381
|
|
686
|
+
udata-8.0.1.dev28928.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
687
|
+
udata-8.0.1.dev28928.dist-info/entry_points.txt,sha256=3SKiqVy4HUqxf6iWspgMqH8d88Htk6KoLbG1BU-UddQ,451
|
|
688
|
+
udata-8.0.1.dev28928.dist-info/top_level.txt,sha256=39OCg-VWFWOq4gCKnjKNu-s3OwFlZIu_dVH8Gl6ndHw,12
|
|
689
|
+
udata-8.0.1.dev28928.dist-info/RECORD,,
|
udata/core/metrics/api.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from flask import abort
|
|
2
|
-
|
|
3
|
-
from udata.api import api, API
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@api.route('/metrics/<id>', endpoint='metrics')
|
|
7
|
-
class MetricsAPI(API):
|
|
8
|
-
@api.doc('metrics_for')
|
|
9
|
-
def get(self, id):
|
|
10
|
-
abort(501, 'This endpoint was deprecated because of metrics refactoring')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|