udata 10.2.1.dev34761__py2.py3-none-any.whl → 10.2.1.dev34775__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.

Files changed (41) hide show
  1. udata/api/__init__.py +41 -8
  2. udata/api_fields.py +1 -5
  3. udata/core/contact_point/api.py +2 -6
  4. udata/core/dataservices/api.py +3 -10
  5. udata/core/dataset/api.py +2 -6
  6. udata/core/dataset/apiv2.py +1 -4
  7. udata/core/dataset/forms.py +1 -1
  8. udata/core/organization/apiv2.py +1 -5
  9. udata/core/reports/api.py +1 -6
  10. udata/core/reuse/api.py +1 -4
  11. udata/core/spatial/tests/test_api.py +56 -0
  12. udata/harvest/actions.py +3 -14
  13. udata/harvest/api.py +13 -9
  14. udata/harvest/models.py +5 -0
  15. udata/harvest/tests/test_actions.py +1 -54
  16. udata/harvest/tests/test_api.py +33 -1
  17. udata/mongo/errors.py +9 -2
  18. udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.8a2f7828175824bcd74b.js} +3 -3
  19. udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.8a2f7828175824bcd74b.js.map} +1 -1
  20. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.39e106d56f794ebd06a0.js} +2 -2
  21. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.39e106d56f794ebd06a0.js.map} +1 -1
  22. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.70cbb4a91b002338007e.js} +2 -2
  23. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.70cbb4a91b002338007e.js.map} +1 -1
  24. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.df16abde17a42033a7f8.js} +3 -3
  25. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
  26. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.5660483641193b7f8295.js} +3 -3
  27. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.5660483641193b7f8295.js.map} +1 -1
  28. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.30dce49d17db07600b06.js} +3 -3
  29. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.30dce49d17db07600b06.js.map} +1 -1
  30. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
  31. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
  32. udata/static/common.js +1 -1
  33. udata/static/common.js.map +1 -1
  34. udata/tests/api/test_dataservices_api.py +5 -8
  35. udata/tests/test_api_fields.py +2 -2
  36. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/METADATA +3 -1
  37. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/RECORD +41 -41
  38. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/LICENSE +0 -0
  39. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/WHEEL +0 -0
  40. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/entry_points.txt +0 -0
  41. {udata-10.2.1.dev34761.dist-info → udata-10.2.1.dev34775.dist-info}/top_level.txt +0 -0
udata/api/__init__.py CHANGED
@@ -4,6 +4,7 @@ import urllib.parse
4
4
  from functools import wraps
5
5
  from importlib import import_module
6
6
 
7
+ import mongoengine
7
8
  from flask import (
8
9
  Blueprint,
9
10
  current_app,
@@ -22,7 +23,6 @@ from udata import entrypoints, tracking
22
23
  from udata.app import csrf
23
24
  from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
24
25
  from udata.i18n import get_locale
25
- from udata.mongo.errors import FieldValidationError
26
26
  from udata.utils import safe_unicode
27
27
 
28
28
  from . import fields
@@ -258,17 +258,50 @@ def handle_unauthorized_file_type(error):
258
258
  return {"message": msg}, 400
259
259
 
260
260
 
261
- validation_error_fields = api.model("ValidationError", {"errors": fields.Raw})
261
+ validation_error_fields = api.model(
262
+ "ValidationError",
263
+ {"errors": fields.Raw, "message": fields.String},
264
+ )
262
265
 
266
+ validation_error_fields_v2 = apiv2.inherit("ValidationError", validation_error_fields)
263
267
 
264
- @api.errorhandler(FieldValidationError)
265
- @api.marshal_with(validation_error_fields, code=400)
266
- def handle_validation_error(error: FieldValidationError):
267
- """A validation error"""
268
+
269
+ def convert_object_of_exceptions_to_object_of_strings(exceptions: dict):
268
270
  errors = {}
269
- errors[error.field] = [error.message]
271
+ for key, exception in exceptions.items():
272
+ if isinstance(exception, Exception):
273
+ errors[key] = str(exception)
274
+ elif isinstance(exception, dict):
275
+ errors[key] = convert_object_of_exceptions_to_object_of_strings(exception)
276
+ elif isinstance(exception, str):
277
+ errors[key] = exception
278
+ else:
279
+ log.warning(
280
+ f"Unknown type in `convert_object_of_exceptions_to_object_of_strings`: {exception}"
281
+ )
282
+ errors[key] = str(exception)
283
+
284
+ return errors
285
+
286
+
287
+ @api.errorhandler(mongoengine.errors.ValidationError)
288
+ @api.marshal_with(validation_error_fields, code=400)
289
+ def handle_validation_error(error: mongoengine.errors.ValidationError):
290
+ """Error returned when validation failed."""
291
+ return (
292
+ {
293
+ "errors": convert_object_of_exceptions_to_object_of_strings(error.errors),
294
+ "message": str(error),
295
+ },
296
+ 400,
297
+ )
298
+
270
299
 
271
- return {"errors": errors}, 400
300
+ @apiv2.errorhandler(mongoengine.errors.ValidationError)
301
+ @apiv2.marshal_with(validation_error_fields_v2, code=400)
302
+ def handle_validation_error_v2(error: mongoengine.errors.ValidationError):
303
+ """Error returned when validation failed."""
304
+ return handle_validation_error(error)
272
305
 
273
306
 
274
307
  class API(Resource): # Avoid name collision as resource is a core model
udata/api_fields.py CHANGED
@@ -567,11 +567,7 @@ def is_value_modified(old_value, new_value) -> bool:
567
567
 
568
568
  def patch_and_save(obj, request) -> type:
569
569
  obj = patch(obj, request)
570
-
571
- try:
572
- obj.save()
573
- except mongoengine.errors.ValidationError as e:
574
- api.abort(400, e.message)
570
+ obj.save()
575
571
 
576
572
  return obj
577
573
 
@@ -1,5 +1,3 @@
1
- import mongoengine
2
-
3
1
  from udata.api import API, api
4
2
  from udata.api.parsers import ModelApiParser
5
3
  from udata.core.dataset.permissions import OwnablePermission
@@ -33,10 +31,8 @@ class ContactPointsListAPI(API):
33
31
  def post(self):
34
32
  """Creates a contact point"""
35
33
  form = api.validate(ContactPointForm)
36
- try:
37
- contact_point = form.save()
38
- except mongoengine.errors.ValidationError as e:
39
- api.abort(400, e.message)
34
+ contact_point = form.save()
35
+
40
36
  return contact_point, 201
41
37
 
42
38
 
@@ -45,11 +45,7 @@ class DataservicesAPI(API):
45
45
  if not dataservice.owner and not dataservice.organization:
46
46
  dataservice.owner = current_user._get_current_object()
47
47
 
48
- try:
49
- dataservice.save()
50
- except mongoengine.errors.ValidationError as e:
51
- api.abort(400, e.message)
52
-
48
+ dataservice.save()
53
49
  return dataservice, 201
54
50
 
55
51
 
@@ -78,11 +74,8 @@ class DataserviceAPI(API):
78
74
  patch(dataservice, request)
79
75
  dataservice.metadata_modified_at = datetime.utcnow()
80
76
 
81
- try:
82
- dataservice.save()
83
- return dataservice
84
- except mongoengine.errors.ValidationError as e:
85
- api.abort(400, e.message)
77
+ dataservice.save()
78
+ return dataservice
86
79
 
87
80
  @api.secure
88
81
  @api.doc("delete_dataservice")
udata/core/dataset/api.py CHANGED
@@ -264,12 +264,8 @@ class DatasetAPI(API):
264
264
  DatasetEditPermission(dataset).test()
265
265
  dataset.last_modified_internal = datetime.utcnow()
266
266
  form = api.validate(DatasetForm, dataset)
267
- # As validation for some fields (ie. extras) is at model
268
- # level instead form level, we use mongoengine errors here.
269
- try:
270
- return form.save()
271
- except mongoengine.errors.ValidationError as e:
272
- api.abort(400, e.message)
267
+
268
+ return form.save()
273
269
 
274
270
  @api.secure
275
271
  @api.doc("delete_dataset")
@@ -340,10 +340,7 @@ class DatasetExtrasAPI(API):
340
340
  data.pop(key)
341
341
  # then update the extras with the remaining payload
342
342
  dataset.extras.update(data)
343
- try:
344
- dataset.save(signal_kwargs={"ignores": ["post_save"]})
345
- except mongoengine.errors.ValidationError as e:
346
- apiv2.abort(400, e.message)
343
+ dataset.save(signal_kwargs={"ignores": ["post_save"]})
347
344
  return dataset.extras
348
345
 
349
346
  @apiv2.secure
@@ -54,7 +54,7 @@ class SchemaForm(ModelForm):
54
54
  )
55
55
  except FieldValidationError as err:
56
56
  field = getattr(self, err.field)
57
- field.errors.append(err.message)
57
+ field.errors.append(str(err))
58
58
  return False
59
59
 
60
60
  return validation
@@ -1,4 +1,3 @@
1
- import mongoengine
2
1
  from flask import request
3
2
 
4
3
  from udata import search
@@ -65,10 +64,7 @@ class OrganizationExtrasAPI(API):
65
64
 
66
65
  # then update the extras with the remaining payload
67
66
  org.extras.update(data)
68
- try:
69
- org.save()
70
- except mongoengine.errors.ValidationError as e:
71
- apiv2.abort(400, e.message)
67
+ org.save()
72
68
  return org.extras
73
69
 
74
70
  @apiv2.secure
udata/core/reports/api.py CHANGED
@@ -1,4 +1,3 @@
1
- import mongoengine
2
1
  from flask import request
3
2
  from flask_login import current_user
4
3
 
@@ -31,11 +30,7 @@ class ReportsAPI(API):
31
30
  if current_user.is_authenticated:
32
31
  report.by = current_user._get_current_object()
33
32
 
34
- try:
35
- report.save()
36
- except mongoengine.errors.ValidationError as e:
37
- api.abort(400, e.message)
38
-
33
+ report.save()
39
34
  return report, 201
40
35
 
41
36
 
udata/core/reuse/api.py CHANGED
@@ -125,10 +125,7 @@ class ReuseListAPI(API):
125
125
  if not reuse.owner and not reuse.organization:
126
126
  reuse.owner = current_user._get_current_object()
127
127
 
128
- try:
129
- reuse.save()
130
- except mongoengine.errors.ValidationError as e:
131
- api.abort(400, e.message)
128
+ reuse.save()
132
129
 
133
130
  return patch_and_save(reuse, request), 201
134
131
 
@@ -1,14 +1,17 @@
1
1
  from flask import url_for
2
2
 
3
3
  from udata.core.dataset.factories import DatasetFactory
4
+ from udata.core.dataset.models import Dataset
4
5
  from udata.core.organization.factories import OrganizationFactory
5
6
  from udata.core.spatial.factories import (
6
7
  GeoLevelFactory,
7
8
  GeoZoneFactory,
8
9
  SpatialCoverageFactory,
9
10
  )
11
+ from udata.core.spatial.models import spatial_granularities
10
12
  from udata.core.spatial.tasks import compute_geozones_metrics
11
13
  from udata.tests.api import APITestCase
14
+ from udata.tests.api.test_datasets_api import SAMPLE_GEOM
12
15
  from udata.tests.features.territories import (
13
16
  TerritoriesSettings,
14
17
  create_geozones_fixtures,
@@ -286,3 +289,56 @@ class SpatialTerritoriesApiTest(APITestCase):
286
289
  self.assert200(response)
287
290
  # No dynamic datasets given that they are added by udata-front extension.
288
291
  self.assertEqual(len(response.json), 2)
292
+
293
+
294
+ class DatasetsSpatialAPITest(APITestCase):
295
+ modules = []
296
+
297
+ def test_create_spatial_zones(self):
298
+ paca, _, _ = create_geozones_fixtures()
299
+ granularity = spatial_granularities[0][0]
300
+ data = DatasetFactory.as_dict()
301
+ data["spatial"] = {
302
+ "zones": [paca.id],
303
+ "granularity": granularity,
304
+ }
305
+ self.login()
306
+ response = self.post(url_for("api.datasets"), data)
307
+ self.assert201(response)
308
+ self.assertEqual(Dataset.objects.count(), 1)
309
+ dataset = Dataset.objects.first()
310
+ self.assertEqual([str(z) for z in dataset.spatial.zones], [paca.id])
311
+ self.assertEqual(dataset.spatial.geom, None)
312
+ self.assertEqual(dataset.spatial.granularity, granularity)
313
+
314
+ def test_create_spatial_geom(self):
315
+ granularity = spatial_granularities[0][0]
316
+ data = DatasetFactory.as_dict()
317
+ data["spatial"] = {
318
+ "geom": SAMPLE_GEOM,
319
+ "granularity": granularity,
320
+ }
321
+ self.login()
322
+ response = self.post(url_for("api.datasets"), data)
323
+ self.assert201(response)
324
+ self.assertEqual(Dataset.objects.count(), 1)
325
+ dataset = Dataset.objects.first()
326
+ self.assertEqual(dataset.spatial.zones, [])
327
+ self.assertEqual(dataset.spatial.geom, SAMPLE_GEOM)
328
+ self.assertEqual(dataset.spatial.granularity, granularity)
329
+
330
+ def test_cannot_create_both_geom_and_zones(self):
331
+ paca, _, _ = create_geozones_fixtures()
332
+
333
+ granularity = spatial_granularities[0][0]
334
+ data = DatasetFactory.as_dict()
335
+ data["spatial"] = {
336
+ "zones": [paca.id],
337
+ "geom": SAMPLE_GEOM,
338
+ "granularity": granularity,
339
+ }
340
+ self.login()
341
+
342
+ response = self.post(url_for("api.datasets"), data)
343
+ self.assert400(response)
344
+ self.assertEqual(Dataset.objects.count(), 0)
udata/harvest/actions.py CHANGED
@@ -34,25 +34,14 @@ def list_backends():
34
34
  return backends.get_all(current_app).values()
35
35
 
36
36
 
37
- def _sources_queryset(owner=None, deleted=False):
37
+ def list_sources(owner=None, deleted=False):
38
+ """List all harvest sources"""
38
39
  sources = HarvestSource.objects
39
40
  if not deleted:
40
41
  sources = sources.visible()
41
42
  if owner:
42
43
  sources = sources.owned_by(owner)
43
- return sources
44
-
45
-
46
- def list_sources(owner=None, deleted=False):
47
- """List all harvest sources"""
48
- return list(_sources_queryset(owner=owner, deleted=deleted))
49
-
50
-
51
- def paginate_sources(owner=None, page=1, page_size=DEFAULT_PAGE_SIZE, deleted=False):
52
- """Paginate harvest sources"""
53
- sources = _sources_queryset(owner=owner, deleted=deleted)
54
- page = max(page or 1, 1)
55
- return sources.paginate(page, page_size)
44
+ return list(sources)
56
45
 
57
46
 
58
47
  def get_source(ident):
udata/harvest/api.py CHANGED
@@ -1,4 +1,3 @@
1
- from bson import ObjectId
2
1
  from flask import current_app, request
3
2
  from werkzeug.exceptions import BadRequest
4
3
 
@@ -253,6 +252,7 @@ source_parser.add_argument(
253
252
  source_parser.add_argument(
254
253
  "deleted", type=bool, location="args", default=False, help="Include sources flaggued as deleted"
255
254
  )
255
+ source_parser.add_argument("q", type=str, location="args", help="The search query")
256
256
 
257
257
 
258
258
  @ns.route("/sources/", endpoint="harvest_sources")
@@ -264,15 +264,19 @@ class SourcesAPI(API):
264
264
  """List all harvest sources"""
265
265
  args = source_parser.parse_args()
266
266
 
267
- if args.get("owner") and not ObjectId.is_valid(args.get("owner")):
268
- api.abort(400, "`owner` arg must be an identifier")
267
+ sources = HarvestSource.objects()
269
268
 
270
- return actions.paginate_sources(
271
- args.get("owner"),
272
- page=args["page"],
273
- page_size=args["page_size"],
274
- deleted=args["deleted"],
275
- )
269
+ if not args["deleted"]:
270
+ sources = sources.visible()
271
+
272
+ if args["owner"]:
273
+ sources = sources.owned_by(args["owner"])
274
+
275
+ if args["q"]:
276
+ phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
277
+ sources = sources.search_text(phrase_query)
278
+
279
+ return sources.paginate(args["page"], args["page_size"])
276
280
 
277
281
  @api.secure
278
282
  @api.doc("create_harvest_source")
udata/harvest/models.py CHANGED
@@ -155,6 +155,11 @@ class HarvestSource(Owned, db.Document):
155
155
 
156
156
  meta = {
157
157
  "indexes": [
158
+ {
159
+ "fields": ["$name", "$url"],
160
+ "default_language": "french",
161
+ "weights": {"name": 10, "url": 5},
162
+ },
158
163
  "-created_at",
159
164
  "slug",
160
165
  ("deleted", "-created_at"),
@@ -14,7 +14,7 @@ from udata.core.organization.factories import OrganizationFactory
14
14
  from udata.core.user.factories import UserFactory
15
15
  from udata.models import Dataset, PeriodicTask
16
16
  from udata.tests.helpers import assert_emit, assert_equal_dates
17
- from udata.utils import Paginable, faker
17
+ from udata.utils import faker
18
18
 
19
19
  from .. import actions, signals
20
20
  from ..backends import BaseBackend
@@ -113,59 +113,6 @@ class HarvestActionsTest:
113
113
  for source in sources:
114
114
  assert source in result
115
115
 
116
- def test_paginate_sources(self):
117
- result = actions.paginate_sources()
118
- assert isinstance(result, Paginable)
119
- assert result.page == 1
120
- assert result.page_size == actions.DEFAULT_PAGE_SIZE
121
- assert result.total == 0
122
- assert len(result.objects) == 0
123
-
124
- HarvestSourceFactory.create_batch(3)
125
-
126
- result = actions.paginate_sources(page_size=2)
127
- assert isinstance(result, Paginable)
128
- assert result.page == 1
129
- assert result.page_size == 2
130
- assert result.total == 3
131
- assert len(result.objects) == 2
132
-
133
- result = actions.paginate_sources(page=2, page_size=2)
134
- assert isinstance(result, Paginable)
135
- assert result.page == 2
136
- assert result.page_size == 2
137
- assert result.total == 3
138
- assert len(result.objects) == 1
139
-
140
- def test_paginate_sources_exclude_deleted(self):
141
- HarvestSourceFactory.create_batch(2)
142
- HarvestSourceFactory(deleted=datetime.utcnow())
143
-
144
- result = actions.paginate_sources(page_size=2)
145
- assert isinstance(result, Paginable)
146
- assert result.page == 1
147
- assert result.page_size == 2
148
- assert result.total == 2
149
- assert len(result.objects) == 2
150
-
151
- def test_paginate_sources_include_deleted(self):
152
- HarvestSourceFactory.create_batch(2)
153
- HarvestSourceFactory(deleted=datetime.utcnow())
154
-
155
- result = actions.paginate_sources(page_size=2, deleted=True)
156
- assert isinstance(result, Paginable)
157
- assert result.page == 1
158
- assert result.page_size == 2
159
- assert result.total == 3
160
- assert len(result.objects) == 2
161
-
162
- result = actions.paginate_sources(page=2, page_size=2, deleted=True)
163
- assert isinstance(result, Paginable)
164
- assert result.page == 2
165
- assert result.page_size == 2
166
- assert result.total == 3
167
- assert len(result.objects) == 1
168
-
169
116
  def test_create_source(self):
170
117
  source_url = faker.url()
171
118
 
@@ -8,7 +8,7 @@ from pytest_mock import MockerFixture
8
8
  from udata.core.organization.factories import OrganizationFactory
9
9
  from udata.core.user.factories import AdminFactory, UserFactory
10
10
  from udata.models import Member, PeriodicTask
11
- from udata.tests.helpers import assert200, assert201, assert204, assert400, assert403
11
+ from udata.tests.helpers import assert200, assert201, assert204, assert400, assert403, assert404
12
12
  from udata.tests.plugin import ApiClient
13
13
  from udata.utils import faker
14
14
 
@@ -85,6 +85,38 @@ class HarvestAPITest(MockBackendsMixin):
85
85
 
86
86
  assert len(response.json["data"]) == len(sources)
87
87
 
88
+ def test_list_sources_search(self, api):
89
+ HarvestSourceFactory.create_batch(3)
90
+ source = HarvestSourceFactory(name="Moissonneur GeoNetwork de la ville de Rennes")
91
+
92
+ url = url_for("api.harvest_sources", q="geonetwork rennes")
93
+ response = api.get(url)
94
+ assert200(response)
95
+
96
+ assert len(response.json["data"]) == 1
97
+ assert response.json["data"][0]["id"] == str(source.id)
98
+
99
+ def test_list_sources_paginate(self, api):
100
+ total = 25
101
+ page_size = 20
102
+ HarvestSourceFactory.create_batch(total)
103
+
104
+ url = url_for("api.harvest_sources", page=1, page_size=page_size)
105
+ response = api.get(url)
106
+ assert200(response)
107
+ assert len(response.json["data"]) == page_size
108
+ assert response.json["total"] == total
109
+
110
+ url = url_for("api.harvest_sources", page=2, page_size=page_size)
111
+ response = api.get(url)
112
+ assert200(response)
113
+ assert len(response.json["data"]) == total - page_size
114
+ assert response.json["total"] == total
115
+
116
+ url = url_for("api.harvest_sources", page=3, page_size=page_size)
117
+ response = api.get(url)
118
+ assert404(response)
119
+
88
120
  def test_create_source_with_owner(self, api):
89
121
  """It should create and attach a new source to an owner"""
90
122
  user = api.login()
udata/mongo/errors.py CHANGED
@@ -3,7 +3,14 @@ from mongoengine.errors import ValidationError
3
3
 
4
4
  class FieldValidationError(ValidationError):
5
5
  field: str
6
+ raw_message: str
6
7
 
7
- def __init__(self, *args, field: str, **kwargs):
8
- self.field = field
8
+ def __init__(self, message: str, *args, field: str, **kwargs):
9
9
  super().__init__(*args, **kwargs)
10
+
11
+ self.raw_message = message # It's sad but ValidationError do some stuff with the message…
12
+ self.field = field
13
+ self.errors[self.field] = message
14
+
15
+ def __str__(self):
16
+ return str(self.raw_message)