udata 7.0.8.dev28841__py2.py3-none-any.whl → 8.0.1__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 (56) hide show
  1. udata/__init__.py +1 -1
  2. udata/api/__init__.py +6 -4
  3. udata/api/oauth2.py +2 -1
  4. udata/api_fields.py +254 -0
  5. udata/core/badges/models.py +2 -1
  6. udata/core/dataservices/__init__.py +0 -0
  7. udata/core/dataservices/api.py +84 -0
  8. udata/core/dataservices/models.py +130 -0
  9. udata/core/dataset/apiv2.py +2 -0
  10. udata/core/dataset/models.py +1 -0
  11. udata/core/dataset/rdf.py +21 -1
  12. udata/core/metrics/commands.py +18 -3
  13. udata/core/metrics/models.py +2 -3
  14. udata/core/organization/api_fields.py +28 -3
  15. udata/core/organization/models.py +3 -1
  16. udata/core/owned.py +39 -2
  17. udata/core/spatial/api.py +5 -10
  18. udata/core/spatial/models.py +7 -2
  19. udata/core/spatial/tasks.py +7 -0
  20. udata/core/spatial/tests/test_api.py +26 -0
  21. udata/core/user/api.py +11 -7
  22. udata/core/user/models.py +13 -2
  23. udata/harvest/backends/dcat.py +14 -8
  24. udata/harvest/tests/dcat/catalog.xml +1 -0
  25. udata/harvest/tests/test_dcat_backend.py +3 -0
  26. udata/routing.py +6 -0
  27. udata/settings.py +4 -1
  28. udata/static/admin.css +2 -2
  29. udata/static/admin.css.map +1 -1
  30. udata/static/chunks/{0.6f1698738c9b0618b673.js → 0.93c3ae13b5b94753ee80.js} +3 -3
  31. udata/static/chunks/0.93c3ae13b5b94753ee80.js.map +1 -0
  32. udata/static/chunks/{14.f4037a917d5364cb564b.js → 14.e64890872b31c55fcdf7.js} +2 -2
  33. udata/static/chunks/14.e64890872b31c55fcdf7.js.map +1 -0
  34. udata/static/chunks/{2.7c89fae92899be371ed3.js → 2.614b3e73b072982fd9b1.js} +2 -2
  35. udata/static/chunks/2.614b3e73b072982fd9b1.js.map +1 -0
  36. udata/static/chunks/{5.3dc97ea195d251881552.js → 5.48417db6b33328fa9d6a.js} +2 -2
  37. udata/static/chunks/5.48417db6b33328fa9d6a.js.map +1 -0
  38. udata/static/common.js +1 -1
  39. udata/static/common.js.map +1 -1
  40. udata/tasks.py +1 -0
  41. udata/tests/api/__init__.py +3 -0
  42. udata/tests/api/test_dataservices_api.py +236 -0
  43. udata/tests/api/test_organizations_api.py +78 -5
  44. udata/tests/api/test_user_api.py +47 -13
  45. udata/tests/plugin.py +5 -0
  46. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/METADATA +17 -3
  47. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/RECORD +51 -46
  48. udata/core/metrics/api.py +0 -10
  49. udata/static/chunks/0.6f1698738c9b0618b673.js.map +0 -1
  50. udata/static/chunks/14.f4037a917d5364cb564b.js.map +0 -1
  51. udata/static/chunks/2.7c89fae92899be371ed3.js.map +0 -1
  52. udata/static/chunks/5.3dc97ea195d251881552.js.map +0 -1
  53. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/LICENSE +0 -0
  54. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/WHEEL +0 -0
  55. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/entry_points.txt +0 -0
  56. {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,8 @@
1
+ from flask import request
2
+
1
3
  from udata.api import api, fields, base_reference
2
4
  from udata.core.badges.fields import badge_fields
5
+ from udata.core.organization.permissions import OrganizationPrivatePermission
3
6
 
4
7
  from .constants import ORG_ROLES, DEFAULT_ROLE, MEMBERSHIP_STATUS, BIGGEST_LOGO_SIZE
5
8
 
@@ -27,9 +30,29 @@ org_ref_fields = api.inherit('OrganizationReference', base_reference, {
27
30
 
28
31
  from udata.core.user.api_fields import user_ref_fields # noqa: required
29
32
 
33
+ def check_can_access_email():
34
+ # This endpoint is secure, only organization member has access.
35
+ if request.endpoint == 'api.request_membership':
36
+ return True
37
+
38
+ if request.endpoint != 'api.organization':
39
+ return False
40
+
41
+ org = request.view_args.get('org')
42
+ if org is None:
43
+ return False
44
+
45
+ return OrganizationPrivatePermission(org).can()
46
+
47
+ member_user_with_email_fields = api.inherit('MemberUserWithEmail', user_ref_fields, {
48
+ 'email': fields.Raw(
49
+ attribute=lambda o: o.email if check_can_access_email() else None,
50
+ description='The user email (only present on show organization endpoint if the current user has edit permission on the org)', readonly=True),
51
+ })
52
+
30
53
  request_fields = api.model('MembershipRequest', {
31
54
  'id': fields.String(readonly=True),
32
- 'user': fields.Nested(user_ref_fields),
55
+ 'user': fields.Nested(member_user_with_email_fields),
33
56
  'created': fields.ISODateTime(
34
57
  description='The request creation date', readonly=True),
35
58
  'status': fields.String(
@@ -40,10 +63,12 @@ request_fields = api.model('MembershipRequest', {
40
63
  })
41
64
 
42
65
  member_fields = api.model('Member', {
43
- 'user': fields.Nested(user_ref_fields),
66
+ 'user': fields.Nested(member_user_with_email_fields),
44
67
  'role': fields.String(
45
68
  description='The member role in the organization', required=True,
46
- enum=list(ORG_ROLES), default=DEFAULT_ROLE)
69
+ enum=list(ORG_ROLES), default=DEFAULT_ROLE),
70
+ 'since': fields.ISODateTime(
71
+ description='The date the user joined the organization', readonly=True),
47
72
  })
48
73
 
49
74
  org_fields = api.model('Organization', {
@@ -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.models import db, BadgeMixin, WithMetrics
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 = ReferenceField('User', reverse_delete_rule=NULLIFY)
25
- organization = ReferenceField('Organization', reverse_delete_rule=NULLIFY)
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
@@ -85,7 +85,7 @@ dataset_parser.add_argument(
85
85
  location='args', default=25)
86
86
 
87
87
 
88
- @ns.route('/zones/<pathlist:ids>/', endpoint='zones')
88
+ @ns.route('/zones/<list:ids>/', endpoint='zones')
89
89
  class ZonesAPI(API):
90
90
  @api.doc('spatial_zones',
91
91
  params={'ids': 'A zone identifiers list (comma separated)'})
@@ -101,7 +101,7 @@ class ZonesAPI(API):
101
101
  }
102
102
 
103
103
 
104
- @ns.route('/zone/<path:id>/datasets/', endpoint='zone_datasets')
104
+ @ns.route('/zone/<id>/datasets/', endpoint='zone_datasets')
105
105
  class ZoneDatasetsAPI(API):
106
106
  @api.doc('spatial_zone_datasets', params={'id': 'A zone identifier'})
107
107
  @api.expect(dataset_parser)
@@ -118,7 +118,7 @@ class ZoneDatasetsAPI(API):
118
118
  return datasets
119
119
 
120
120
 
121
- @ns.route('/zone/<path:id>/', endpoint='zone')
121
+ @ns.route('/zone/<id>/', endpoint='zone')
122
122
  class ZoneAPI(API):
123
123
  @api.doc('spatial_zone', params={'id': 'A zone identifier'})
124
124
  def get(self, id):
@@ -152,7 +152,7 @@ class SpatialGranularitiesAPI(API):
152
152
  } for id, name in spatial_granularities]
153
153
 
154
154
 
155
- @ns.route('/coverage/<path:level>/', endpoint='spatial_coverage')
155
+ @ns.route('/coverage/<level>/', endpoint='spatial_coverage')
156
156
  class SpatialCoverageAPI(API):
157
157
  @api.doc('spatial_coverage')
158
158
  @api.marshal_list_with(feature_collection_fields)
@@ -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': nb_datasets
172
+ 'datasets': zone.metrics.get('datasets', 0)
178
173
  }
179
174
  })
180
175
 
@@ -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,
@@ -0,0 +1,7 @@
1
+ from udata.core.spatial.models import GeoZone
2
+ from udata.tasks import job
3
+
4
+ @job('compute-geozones-metrics')
5
+ def compute_geozones_metrics(self):
6
+ for geozone in GeoZone.objects.timeout(False):
7
+ geozone.count_datasets()
@@ -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/core/user/api.py CHANGED
@@ -226,6 +226,7 @@ class UserListAPI(API):
226
226
  fields = user_fields
227
227
  form = UserProfileForm
228
228
 
229
+ @api.secure(admin_permission)
229
230
  @api.doc('list_users')
230
231
  @api.expect(user_parser.parser)
231
232
  @api.marshal_with(user_page_fields)
@@ -269,6 +270,12 @@ class UserAvatarAPI(API):
269
270
  return {'image': user.avatar}
270
271
 
271
272
 
273
+
274
+ delete_parser = api.parser()
275
+ delete_parser.add_argument(
276
+ 'no_mail', type=bool, help='Do not send a mail to notify the user of the deletion',
277
+ location='args', default=False)
278
+
272
279
  @ns.route('/<user:user>/', endpoint='user')
273
280
  @api.response(404, 'User not found')
274
281
  @api.response(410, 'User is not active or has been deleted')
@@ -297,22 +304,19 @@ class UserAPI(API):
297
304
 
298
305
  @api.secure(admin_permission)
299
306
  @api.doc('delete_user')
307
+ @api.expect(delete_parser)
300
308
  @api.response(204, 'Object deleted')
301
309
  @api.response(403, 'When trying to delete yourself')
302
310
  def delete(self, user):
303
311
  '''Delete a user given its identifier'''
312
+ args = delete_parser.parse_args()
304
313
  if user.deleted:
305
314
  api.abort(410, 'User has already been deleted')
306
315
  if user == current_user._get_current_object():
307
316
  api.abort(403, 'You cannot delete yourself with this API. ' +
308
317
  'Use the "me" API instead.')
309
- if user.avatar.filename is not None:
310
- storage = storages.avatars
311
- storage.delete(user.avatar.filename)
312
- storage.delete(user.avatar.original)
313
- for key, value in user.avatar.thumbnails.items():
314
- storage.delete(value)
315
- user.mark_as_deleted()
318
+
319
+ user.mark_as_deleted(notify=not args['no_mail'])
316
320
  return '', 204
317
321
 
318
322
 
udata/core/user/models.py CHANGED
@@ -13,6 +13,7 @@ from mongoengine.signals import pre_save, post_save
13
13
  from werkzeug.utils import cached_property
14
14
 
15
15
  from udata import mail
16
+ from udata.core import storages
16
17
  from udata.uris import endpoint_for
17
18
  from udata.frontend.markdown import mdstrip
18
19
  from udata.i18n import lazy_gettext as _
@@ -233,7 +234,15 @@ class User(WithMetrics, UserMixin, db.Document):
233
234
  raise NotImplementedError('''This method should not be using directly.
234
235
  Use `mark_as_deleted` (or `_delete` if you know what you're doing)''')
235
236
 
236
- def mark_as_deleted(self):
237
+ def mark_as_deleted(self, notify: bool = True):
238
+ if self.avatar.filename is not None:
239
+ storage = storages.avatars
240
+ storage.delete(self.avatar.filename)
241
+ storage.delete(self.avatar.original)
242
+ for key, value in self.avatar.thumbnails.items():
243
+ storage.delete(value)
244
+
245
+
237
246
  copied_user = copy(self)
238
247
  self.email = '{}@deleted'.format(self.id)
239
248
  self.slug = 'deleted'
@@ -270,7 +279,9 @@ class User(WithMetrics, UserMixin, db.Document):
270
279
  from udata.models import ContactPoint
271
280
  ContactPoint.objects(owner=self).delete()
272
281
 
273
- mail.send(_('Account deletion'), copied_user, 'account_deleted')
282
+
283
+ if notify:
284
+ mail.send(_('Account deletion'), copied_user, 'account_deleted')
274
285
 
275
286
  def count_datasets(self):
276
287
  from udata.models import Dataset
@@ -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:PropertyIsEqualTo>
308
- <ogc:PropertyName>dc:type</ogc:PropertyName>
309
- <ogc:Literal>dataset</ogc:Literal>
310
- </ogc:PropertyIsEqualTo>
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
- headers=headers)
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
- headers=headers)
365
+ headers=headers)
360
366
  response.raise_for_status()
361
367
 
362
368
  tree_before_transform = ET.fromstring(response.content)
@@ -23,6 +23,7 @@
23
23
  <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2016-12-14T18:59:02.737480</dcterms:issued>
24
24
  <dcterms:description>Dataset 3 description</dcterms:description>
25
25
  <dcat:keyword>Tag 1</dcat:keyword>
26
+ <dcat:theme rdf:resource="http://data.europa.eu/bna/c_dd313021"/>
26
27
  <dcat:distribution rdf:resource="datasets/3/resources/1"/>
27
28
  <dct:license>Licence Ouverte Version 2.0</dct:license>
28
29
  <dct:accessRights rdf:resource="http://inspire.ec.europa.eu/metadata-codelist/LimitationsOnPublicAccess/INSPIRE_Directive_Article13_1e"/>
@@ -440,6 +440,9 @@ class DcatBackendTest:
440
440
  assert dataset.extras["harvest"]["dct:accessRights"] == "http://inspire.ec.europa.eu/metadata-codelist/LimitationsOnPublicAccess/INSPIRE_Directive_Article13_1e"
441
441
  assert dataset.extras["harvest"]["dct:provenance"] == ["Description de la provenance des données"]
442
442
 
443
+ assert 'observation-de-la-terre-et-environnement' in dataset.tags
444
+ assert 'hvd' in dataset.tags
445
+
443
446
  dataset = Dataset.objects.get(harvest__dct_identifier='1')
444
447
  # test html abstract description support
445
448
  assert dataset.description == '# h1 title\n\n## h2 title\n\n **and bold text**'
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/settings.py CHANGED
@@ -269,7 +269,10 @@ class Defaults(object):
269
269
  # S3 connection details
270
270
  S3_URL = None
271
271
  S3_ACCESS_KEY_ID = None
272
- S3_SECRET_ACCESS_KEY = None
272
+ S3_SECRET_ACCESS_KEY = None
273
+
274
+ # Specific support for hvd (map HVD categories URIs to keywords)
275
+ HVD_SUPPORT = True
273
276
 
274
277
  ACTIVATE_TERRITORIES = False
275
278
  # The order is important to compute parents/children, smaller first.