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.
- udata/__init__.py +1 -1
- 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/dataset/apiv2.py +2 -0
- udata/core/dataset/models.py +1 -0
- udata/core/dataset/rdf.py +21 -1
- udata/core/metrics/commands.py +18 -3
- udata/core/metrics/models.py +2 -3
- udata/core/organization/api_fields.py +28 -3
- udata/core/organization/models.py +3 -1
- udata/core/owned.py +39 -2
- udata/core/spatial/api.py +5 -10
- udata/core/spatial/models.py +7 -2
- udata/core/spatial/tasks.py +7 -0
- udata/core/spatial/tests/test_api.py +26 -0
- udata/core/user/api.py +11 -7
- udata/core/user/models.py +13 -2
- udata/harvest/backends/dcat.py +14 -8
- udata/harvest/tests/dcat/catalog.xml +1 -0
- udata/harvest/tests/test_dcat_backend.py +3 -0
- udata/routing.py +6 -0
- udata/settings.py +4 -1
- udata/static/admin.css +2 -2
- udata/static/admin.css.map +1 -1
- udata/static/chunks/{0.6f1698738c9b0618b673.js → 0.93c3ae13b5b94753ee80.js} +3 -3
- udata/static/chunks/0.93c3ae13b5b94753ee80.js.map +1 -0
- udata/static/chunks/{14.f4037a917d5364cb564b.js → 14.e64890872b31c55fcdf7.js} +2 -2
- udata/static/chunks/14.e64890872b31c55fcdf7.js.map +1 -0
- udata/static/chunks/{2.7c89fae92899be371ed3.js → 2.614b3e73b072982fd9b1.js} +2 -2
- udata/static/chunks/2.614b3e73b072982fd9b1.js.map +1 -0
- udata/static/chunks/{5.3dc97ea195d251881552.js → 5.48417db6b33328fa9d6a.js} +2 -2
- udata/static/chunks/5.48417db6b33328fa9d6a.js.map +1 -0
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tasks.py +1 -0
- udata/tests/api/__init__.py +3 -0
- udata/tests/api/test_dataservices_api.py +236 -0
- udata/tests/api/test_organizations_api.py +78 -5
- udata/tests/api/test_user_api.py +47 -13
- udata/tests/plugin.py +5 -0
- {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/METADATA +17 -3
- {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/RECORD +51 -46
- udata/core/metrics/api.py +0 -10
- udata/static/chunks/0.6f1698738c9b0618b673.js.map +0 -1
- udata/static/chunks/14.f4037a917d5364cb564b.js.map +0 -1
- udata/static/chunks/2.7c89fae92899be371ed3.js.map +0 -1
- udata/static/chunks/5.3dc97ea195d251881552.js.map +0 -1
- {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/LICENSE +0 -0
- {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/WHEEL +0 -0
- {udata-7.0.8.dev28841.dist-info → udata-8.0.1.dist-info}/entry_points.txt +0 -0
- {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(
|
|
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(
|
|
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.
|
|
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
|
@@ -85,7 +85,7 @@ dataset_parser.add_argument(
|
|
|
85
85
|
location='args', default=25)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
@ns.route('/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/<
|
|
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/<
|
|
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/<
|
|
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':
|
|
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/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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
udata/harvest/backends/dcat.py
CHANGED
|
@@ -268,7 +268,6 @@ class CswDcatBackend(DcatBackend):
|
|
|
268
268
|
headers=headers).content)
|
|
269
269
|
|
|
270
270
|
return graphs
|
|
271
|
-
|
|
272
271
|
|
|
273
272
|
|
|
274
273
|
class CswIso19139DcatBackend(DcatBackend):
|
|
@@ -295,6 +294,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
295
294
|
transform = ET.XSLT(xsl)
|
|
296
295
|
|
|
297
296
|
# Start querying and parsing graph
|
|
297
|
+
# Filter on dataset or serie records
|
|
298
298
|
body = '''<csw:GetRecords xmlns:csw="http://www.opengis.net/cat/csw/2.0.2"
|
|
299
299
|
xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
|
300
300
|
service="CSW" version="2.0.2" resultType="results"
|
|
@@ -304,10 +304,16 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
304
304
|
<csw:ElementSetName>full</csw:ElementSetName>
|
|
305
305
|
<csw:Constraint version="1.1.0">
|
|
306
306
|
<ogc:Filter xmlns:ogc="http://www.opengis.net/ogc">
|
|
307
|
-
<ogc:
|
|
308
|
-
<ogc:
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
<ogc:Or xmlns:ogc="http://www.opengis.net/ogc">
|
|
308
|
+
<ogc:PropertyIsEqualTo>
|
|
309
|
+
<ogc:PropertyName>dc:type</ogc:PropertyName>
|
|
310
|
+
<ogc:Literal>dataset</ogc:Literal>
|
|
311
|
+
</ogc:PropertyIsEqualTo>
|
|
312
|
+
<ogc:PropertyIsEqualTo>
|
|
313
|
+
<ogc:PropertyName>dc:type</ogc:PropertyName>
|
|
314
|
+
<ogc:Literal>series</ogc:Literal>
|
|
315
|
+
</ogc:PropertyIsEqualTo>
|
|
316
|
+
</ogc:Or>
|
|
311
317
|
</ogc:Filter>
|
|
312
318
|
</csw:Constraint>
|
|
313
319
|
</csw:Query>
|
|
@@ -319,7 +325,7 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
319
325
|
start = 1
|
|
320
326
|
|
|
321
327
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
322
|
-
|
|
328
|
+
headers=headers)
|
|
323
329
|
response.raise_for_status()
|
|
324
330
|
|
|
325
331
|
tree_before_transform = ET.fromstring(response.content)
|
|
@@ -351,12 +357,12 @@ class CswIso19139DcatBackend(DcatBackend):
|
|
|
351
357
|
next_record = self.next_record_if_should_continue(start, search_results)
|
|
352
358
|
if not next_record:
|
|
353
359
|
break
|
|
354
|
-
|
|
360
|
+
|
|
355
361
|
start = next_record
|
|
356
362
|
page += 1
|
|
357
363
|
|
|
358
364
|
response = self.post(url, data=body.format(start=start, schema=self.ISO_SCHEMA),
|
|
359
|
-
|
|
365
|
+
headers=headers)
|
|
360
366
|
response.raise_for_status()
|
|
361
367
|
|
|
362
368
|
tree_before_transform = ET.fromstring(response.content)
|
|
@@ -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.
|