udata 7.0.4.dev27777__py2.py3-none-any.whl → 7.0.5__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 +1 -1
- udata/core/dataset/api.py +14 -14
- udata/core/dataset/api_fields.py +7 -7
- udata/core/dataset/apiv2.py +3 -3
- udata/core/dataset/rdf.py +43 -1
- udata/core/organization/csv.py +27 -1
- udata/core/organization/models.py +20 -1
- udata/core/organization/tasks.py +61 -1
- udata/core/spatial/commands.py +26 -2
- udata/core/topic/api.py +6 -0
- udata/core/topic/apiv2.py +6 -0
- udata/core/topic/forms.py +5 -0
- udata/core/topic/models.py +3 -5
- udata/forms/fields.py +10 -0
- udata/frontend/csv.py +8 -8
- udata/harvest/actions.py +11 -0
- udata/harvest/api.py +3 -3
- udata/harvest/backends/dcat.py +42 -5
- udata/harvest/tests/dcat/bnodes.xml +16 -2
- udata/harvest/tests/test_dcat_backend.py +87 -1
- udata/settings.py +9 -0
- udata/static/chunks/{11.c0ccea08914b6b41568e.js → 11.a23c110811a9ac943478.js} +3 -3
- udata/static/chunks/{11.c0ccea08914b6b41568e.js.map → 11.a23c110811a9ac943478.js.map} +1 -1
- udata/static/chunks/{13.526a25163ababaa44409.js → 13.0889e093f8664e38568c.js} +2 -2
- udata/static/chunks/{13.526a25163ababaa44409.js.map → 13.0889e093f8664e38568c.js.map} +1 -1
- udata/static/chunks/{16.7901839b4227881947f6.js → 16.f41599478d3e97ad9a30.js} +2 -2
- udata/static/chunks/{16.7901839b4227881947f6.js.map → 16.f41599478d3e97ad9a30.js.map} +1 -1
- udata/static/chunks/{19.471d5a2a08eef6e5338a.js → 19.2b534a26af8b17e9170b.js} +3 -3
- udata/static/chunks/{19.471d5a2a08eef6e5338a.js.map → 19.2b534a26af8b17e9170b.js.map} +1 -1
- udata/static/chunks/{5.534e0531d0e2b150146f.js → 5.7115454a1183e5c12eef.js} +3 -3
- udata/static/chunks/{5.534e0531d0e2b150146f.js.map → 5.7115454a1183e5c12eef.js.map} +1 -1
- udata/static/chunks/{6.e56975229e6065f68d2a.js → 6.16bb24fb8240f2746488.js} +3 -3
- udata/static/chunks/{6.e56975229e6065f68d2a.js.map → 6.16bb24fb8240f2746488.js.map} +1 -1
- udata/static/chunks/{9.534426728626f11f4571.js → 9.3e752966ff14e47e11f2.js} +2 -2
- udata/static/chunks/{9.534426728626f11f4571.js.map → 9.3e752966ff14e47e11f2.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/storage/__init__.py +0 -0
- udata/storage/s3.py +54 -0
- udata/templates/mail/badge_added_association.html +33 -0
- udata/templates/mail/badge_added_association.txt +11 -0
- udata/templates/mail/badge_added_company.html +33 -0
- udata/templates/mail/badge_added_company.txt +11 -0
- udata/templates/mail/badge_added_local_authority.html +33 -0
- udata/templates/mail/badge_added_local_authority.txt +11 -0
- udata/tests/api/test_datasets_api.py +27 -0
- udata/tests/api/test_topics_api.py +31 -1
- udata/tests/apiv2/test_topics.py +4 -0
- udata/tests/organization/test_csv_adapter.py +43 -0
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +223 -174
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +224 -175
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +223 -174
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +227 -178
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +224 -175
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +224 -175
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +225 -176
- udata/translations/udata.pot +91 -45
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/METADATA +20 -3
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/RECORD +71 -62
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/LICENSE +0 -0
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/WHEEL +0 -0
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/entry_points.txt +0 -0
- {udata-7.0.4.dev27777.dist-info → udata-7.0.5.dist-info}/top_level.txt +0 -0
udata/__init__.py
CHANGED
udata/api/__init__.py
CHANGED
|
@@ -147,7 +147,7 @@ class UDataApi(Api):
|
|
|
147
147
|
|
|
148
148
|
def validate(self, form_cls, obj=None):
|
|
149
149
|
'''Validate a form from the request and handle errors'''
|
|
150
|
-
if 'application/json' not in request.headers.get('Content-Type'):
|
|
150
|
+
if 'application/json' not in request.headers.get('Content-Type', ''):
|
|
151
151
|
errors = {'Content-Type': 'expecting application/json'}
|
|
152
152
|
self.abort(400, errors=errors)
|
|
153
153
|
form = form_cls.from_json(request.json, obj=obj, instance=obj,
|
udata/core/dataset/api.py
CHANGED
|
@@ -197,7 +197,7 @@ class DatasetListAPI(API):
|
|
|
197
197
|
@api.secure
|
|
198
198
|
@api.doc('create_dataset', responses={400: 'Validation error'})
|
|
199
199
|
@api.expect(dataset_fields)
|
|
200
|
-
@api.marshal_with(dataset_fields)
|
|
200
|
+
@api.marshal_with(dataset_fields, code=201)
|
|
201
201
|
def post(self):
|
|
202
202
|
'''Create a new dataset'''
|
|
203
203
|
form = api.validate(DatasetForm)
|
|
@@ -344,9 +344,9 @@ class ResourceRedirectAPI(API):
|
|
|
344
344
|
@ns.route('/<dataset:dataset>/resources/', endpoint='resources')
|
|
345
345
|
class ResourcesAPI(API):
|
|
346
346
|
@api.secure
|
|
347
|
-
@api.doc('create_resource', **common_doc)
|
|
347
|
+
@api.doc('create_resource', **common_doc, responses={400: 'Validation error'})
|
|
348
348
|
@api.expect(resource_fields)
|
|
349
|
-
@api.marshal_with(resource_fields)
|
|
349
|
+
@api.marshal_with(resource_fields, code=201)
|
|
350
350
|
def post(self, dataset):
|
|
351
351
|
'''Create a new resource for a given dataset'''
|
|
352
352
|
ResourceEditPermission(dataset).test()
|
|
@@ -361,7 +361,7 @@ class ResourcesAPI(API):
|
|
|
361
361
|
return resource, 201
|
|
362
362
|
|
|
363
363
|
@api.secure
|
|
364
|
-
@api.doc('update_resources', **common_doc)
|
|
364
|
+
@api.doc('update_resources', **common_doc, responses={400: 'Validation error'})
|
|
365
365
|
@api.expect([resource_fields])
|
|
366
366
|
@api.marshal_list_with(resource_fields)
|
|
367
367
|
def put(self, dataset):
|
|
@@ -397,9 +397,9 @@ class UploadMixin(object):
|
|
|
397
397
|
@api.doc(**common_doc)
|
|
398
398
|
class UploadNewDatasetResource(UploadMixin, API):
|
|
399
399
|
@api.secure
|
|
400
|
-
@api.doc('upload_new_dataset_resource')
|
|
400
|
+
@api.doc('upload_new_dataset_resource', responses={415: 'Incorrect file content type', 400: 'Upload error'})
|
|
401
401
|
@api.expect(upload_parser)
|
|
402
|
-
@api.marshal_with(upload_fields)
|
|
402
|
+
@api.marshal_with(upload_fields, code=201)
|
|
403
403
|
def post(self, dataset):
|
|
404
404
|
'''Upload a new dataset resource'''
|
|
405
405
|
ResourceEditPermission(dataset).test()
|
|
@@ -416,9 +416,9 @@ class UploadNewDatasetResource(UploadMixin, API):
|
|
|
416
416
|
@api.doc(**common_doc)
|
|
417
417
|
class UploadNewCommunityResources(UploadMixin, API):
|
|
418
418
|
@api.secure
|
|
419
|
-
@api.doc('upload_new_community_resource')
|
|
419
|
+
@api.doc('upload_new_community_resource', responses={415: 'Incorrect file content type', 400: 'Upload error'})
|
|
420
420
|
@api.expect(upload_parser)
|
|
421
|
-
@api.marshal_with(upload_fields)
|
|
421
|
+
@api.marshal_with(upload_fields, code=201)
|
|
422
422
|
def post(self, dataset):
|
|
423
423
|
'''Upload a new community resource'''
|
|
424
424
|
infos = self.handle_upload(dataset)
|
|
@@ -442,7 +442,7 @@ class ResourceMixin(object):
|
|
|
442
442
|
@api.param('rid', 'The resource unique identifier')
|
|
443
443
|
class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
444
444
|
@api.secure
|
|
445
|
-
@api.doc('upload_dataset_resource')
|
|
445
|
+
@api.doc('upload_dataset_resource', responses={415: 'Incorrect file content type', 400: 'Upload error'})
|
|
446
446
|
@api.marshal_with(upload_fields)
|
|
447
447
|
def post(self, dataset, rid):
|
|
448
448
|
'''Upload a file related to a given resource on a given dataset'''
|
|
@@ -465,7 +465,7 @@ class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
|
465
465
|
@api.param('community', 'The community resource unique identifier')
|
|
466
466
|
class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
467
467
|
@api.secure
|
|
468
|
-
@api.doc('upload_community_resource')
|
|
468
|
+
@api.doc('upload_community_resource', responses={415: 'Incorrect file content type', 400: 'Upload error'})
|
|
469
469
|
@api.marshal_with(upload_fields)
|
|
470
470
|
def post(self, community):
|
|
471
471
|
'''Update the file related to a given community resource'''
|
|
@@ -493,7 +493,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
493
493
|
return resource
|
|
494
494
|
|
|
495
495
|
@api.secure
|
|
496
|
-
@api.doc('update_resource')
|
|
496
|
+
@api.doc('update_resource', responses={400: 'Validation error'})
|
|
497
497
|
@api.expect(resource_fields)
|
|
498
498
|
@api.marshal_with(resource_fields)
|
|
499
499
|
def put(self, dataset, rid):
|
|
@@ -546,9 +546,9 @@ class CommunityResourcesAPI(API):
|
|
|
546
546
|
.paginate(args['page'], args['page_size']))
|
|
547
547
|
|
|
548
548
|
@api.secure
|
|
549
|
-
@api.doc('create_community_resource')
|
|
549
|
+
@api.doc('create_community_resource', responses={400: 'Validation error'})
|
|
550
550
|
@api.expect(community_resource_fields)
|
|
551
|
-
@api.marshal_with(community_resource_fields)
|
|
551
|
+
@api.marshal_with(community_resource_fields, code=201)
|
|
552
552
|
def post(self):
|
|
553
553
|
'''Create a new community resource'''
|
|
554
554
|
form = api.validate(CommunityResourceForm)
|
|
@@ -578,7 +578,7 @@ class CommunityResourceAPI(API):
|
|
|
578
578
|
return community
|
|
579
579
|
|
|
580
580
|
@api.secure
|
|
581
|
-
@api.doc('update_community_resource')
|
|
581
|
+
@api.doc('update_community_resource', responses={400: 'Validation error'})
|
|
582
582
|
@api.expect(community_resource_fields)
|
|
583
583
|
@api.marshal_with(community_resource_fields)
|
|
584
584
|
def put(self, community):
|
udata/core/dataset/api_fields.py
CHANGED
|
@@ -33,9 +33,9 @@ schema_fields = api.model('Schema', {
|
|
|
33
33
|
dataset_harvest_fields = api.model('HarvestDatasetMetadata', {
|
|
34
34
|
'backend': fields.String(description='Harvest backend used', allow_null=True),
|
|
35
35
|
'created_at': fields.ISODateTime(description='The dataset harvested creation date',
|
|
36
|
-
allow_null=True),
|
|
36
|
+
allow_null=True, readonly=True),
|
|
37
37
|
'modified_at': fields.ISODateTime(description='The dataset harvest last modification date',
|
|
38
|
-
allow_null=True),
|
|
38
|
+
allow_null=True, readonly=True),
|
|
39
39
|
'source_id': fields.String(description='The harvester id', allow_null=True),
|
|
40
40
|
'remote_id': fields.String(description='The dataset remote id on the source portal',
|
|
41
41
|
allow_null=True),
|
|
@@ -54,9 +54,9 @@ dataset_harvest_fields = api.model('HarvestDatasetMetadata', {
|
|
|
54
54
|
|
|
55
55
|
resource_harvest_fields = api.model('HarvestResourceMetadata', {
|
|
56
56
|
'created_at': fields.ISODateTime(description='The resource harvested creation date',
|
|
57
|
-
allow_null=True),
|
|
57
|
+
allow_null=True, readonly=True),
|
|
58
58
|
'modified_at': fields.ISODateTime(description='The resource harvest last modification date',
|
|
59
|
-
allow_null=True),
|
|
59
|
+
allow_null=True, readonly=True),
|
|
60
60
|
'uri': fields.String(description='The resource harvest uri', allow_null=True)
|
|
61
61
|
})
|
|
62
62
|
|
|
@@ -199,10 +199,10 @@ dataset_fields = api.model('Dataset', {
|
|
|
199
199
|
'description': fields.Markdown(
|
|
200
200
|
description='The dataset description in markdown', required=True),
|
|
201
201
|
'created_at': fields.ISODateTime(
|
|
202
|
-
description='This date is computed between harvested creation date if any and site\'s internal creation date' , required=True),
|
|
202
|
+
description='This date is computed between harvested creation date if any and site\'s internal creation date' , required=True, readonly=True),
|
|
203
203
|
'last_modified': fields.ISODateTime(
|
|
204
|
-
description='The dataset last modification date', required=True),
|
|
205
|
-
'deleted': fields.ISODateTime(description='The deletion date if deleted'),
|
|
204
|
+
description='The dataset last modification date', required=True, readonly=True),
|
|
205
|
+
'deleted': fields.ISODateTime(description='The deletion date if deleted', readonly=True),
|
|
206
206
|
'archived': fields.ISODateTime(description='The archival date if archived'),
|
|
207
207
|
'featured': fields.Boolean(description='Is the dataset featured'),
|
|
208
208
|
'private': fields.Boolean(
|
udata/core/dataset/apiv2.py
CHANGED
|
@@ -73,10 +73,10 @@ dataset_fields = apiv2.model('Dataset', {
|
|
|
73
73
|
'description': fields.Markdown(
|
|
74
74
|
description='The dataset description in markdown', required=True),
|
|
75
75
|
'created_at': fields.ISODateTime(
|
|
76
|
-
description='The dataset creation date', required=True),
|
|
76
|
+
description='The dataset creation date', required=True, readonly=True),
|
|
77
77
|
'last_modified': fields.ISODateTime(
|
|
78
|
-
description='The dataset last modification date', required=True),
|
|
79
|
-
'deleted': fields.ISODateTime(description='The deletion date if deleted'),
|
|
78
|
+
description='The dataset last modification date', required=True, readonly=True),
|
|
79
|
+
'deleted': fields.ISODateTime(description='The deletion date if deleted', readonly=True),
|
|
80
80
|
'archived': fields.ISODateTime(description='The archival date if archived'),
|
|
81
81
|
'featured': fields.Boolean(description='Is the dataset featured'),
|
|
82
82
|
'private': fields.Boolean(
|
udata/core/dataset/rdf.py
CHANGED
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
This module centralize dataset helpers for RDF/DCAT serialization and parsing
|
|
3
3
|
'''
|
|
4
4
|
import calendar
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
|
|
7
8
|
from datetime import date
|
|
8
9
|
from html.parser import HTMLParser
|
|
9
10
|
from dateutil.parser import parse as parse_dt
|
|
10
11
|
from flask import current_app
|
|
12
|
+
from geomet import wkt
|
|
11
13
|
from rdflib import Graph, URIRef, Literal, BNode
|
|
12
14
|
from rdflib.resource import Resource as RdfResource
|
|
13
15
|
from rdflib.namespace import RDF
|
|
16
|
+
from mongoengine.errors import ValidationError
|
|
14
17
|
|
|
15
18
|
from udata import i18n, uris
|
|
19
|
+
from udata.core.spatial.models import SpatialCoverage
|
|
16
20
|
from udata.frontend.markdown import parse_html
|
|
17
21
|
from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
|
|
18
22
|
from udata.models import db, ContactPoint
|
|
@@ -334,6 +338,40 @@ def contact_point_from_rdf(rdf, dataset):
|
|
|
334
338
|
ContactPoint(name=name, email=email, owner=dataset.owner).save())
|
|
335
339
|
|
|
336
340
|
|
|
341
|
+
def spatial_from_rdf(graph):
|
|
342
|
+
for term in graph.objects(DCT.spatial):
|
|
343
|
+
for object in term.objects():
|
|
344
|
+
if isinstance(object, Literal):
|
|
345
|
+
if object.datatype.__str__() == 'https://www.iana.org/assignments/media-types/application/vnd.geo+json':
|
|
346
|
+
try:
|
|
347
|
+
geojson = json.loads(object.toPython())
|
|
348
|
+
except ValueError as e:
|
|
349
|
+
log.warning(f"Invalid JSON in spatial GeoJSON {object.toPython()} {e}")
|
|
350
|
+
continue
|
|
351
|
+
elif object.datatype.__str__() == 'http://www.opengis.net/rdf#wktLiteral':
|
|
352
|
+
try:
|
|
353
|
+
# .upper() si here because geomet doesn't support Polygon but only POLYGON
|
|
354
|
+
geojson = wkt.loads(object.toPython().strip().upper())
|
|
355
|
+
except ValueError as e:
|
|
356
|
+
log.warning(f"Invalid JSON in spatial WKT {object.toPython()} {e}")
|
|
357
|
+
continue
|
|
358
|
+
else:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
if geojson['type'] == 'Polygon':
|
|
362
|
+
geojson['type'] = 'MultiPolygon'
|
|
363
|
+
geojson['coordinates'] = [geojson['coordinates']]
|
|
364
|
+
|
|
365
|
+
spatial_coverage = SpatialCoverage(geom=geojson)
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
spatial_coverage.clean()
|
|
369
|
+
return spatial_coverage
|
|
370
|
+
except ValidationError:
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
return None
|
|
374
|
+
|
|
337
375
|
def frequency_from_rdf(term):
|
|
338
376
|
if isinstance(term, str):
|
|
339
377
|
try:
|
|
@@ -488,7 +526,7 @@ def resource_from_rdf(graph_or_distrib, dataset=None, is_additionnal=False):
|
|
|
488
526
|
return resource
|
|
489
527
|
|
|
490
528
|
|
|
491
|
-
def dataset_from_rdf(graph, dataset=None, node=None):
|
|
529
|
+
def dataset_from_rdf(graph: Graph, dataset=None, node=None):
|
|
492
530
|
'''
|
|
493
531
|
Create or update a dataset from a RDF/DCAT graph
|
|
494
532
|
'''
|
|
@@ -509,6 +547,10 @@ def dataset_from_rdf(graph, dataset=None, node=None):
|
|
|
509
547
|
if schema:
|
|
510
548
|
dataset.schema = schema
|
|
511
549
|
|
|
550
|
+
spatial_coverage = spatial_from_rdf(d)
|
|
551
|
+
if spatial_coverage:
|
|
552
|
+
dataset.spatial = spatial_coverage
|
|
553
|
+
|
|
512
554
|
acronym = rdf_value(d, SKOS.altLabel)
|
|
513
555
|
if acronym:
|
|
514
556
|
dataset.acronym = acronym
|
udata/core/organization/csv.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from udata.core.dataset.models import Dataset
|
|
1
2
|
from udata.frontend import csv
|
|
2
3
|
|
|
3
4
|
from .models import Organization
|
|
@@ -5,6 +6,8 @@ from .models import Organization
|
|
|
5
6
|
|
|
6
7
|
@csv.adapter(Organization)
|
|
7
8
|
class OrganizationCsvAdapter(csv.Adapter):
|
|
9
|
+
downloads_counts = None
|
|
10
|
+
|
|
8
11
|
fields = (
|
|
9
12
|
'id',
|
|
10
13
|
'name',
|
|
@@ -18,4 +21,27 @@ class OrganizationCsvAdapter(csv.Adapter):
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
def dynamic_fields(self):
|
|
21
|
-
return csv.metric_fields(Organization)
|
|
24
|
+
return csv.metric_fields(Organization) + self.get_dynamic_field_downloads()
|
|
25
|
+
|
|
26
|
+
def get_dynamic_field_downloads(self):
|
|
27
|
+
downloads_counts = self.get_downloads_counts()
|
|
28
|
+
return [('downloads', lambda o: downloads_counts.get(str(o.id), 0))]
|
|
29
|
+
|
|
30
|
+
def get_downloads_counts(self):
|
|
31
|
+
'''
|
|
32
|
+
Prefetch all the resources' downloads for all selected organization into memory
|
|
33
|
+
'''
|
|
34
|
+
if self.downloads_counts is not None:
|
|
35
|
+
return self.downloads_counts
|
|
36
|
+
|
|
37
|
+
self.downloads_counts = {}
|
|
38
|
+
|
|
39
|
+
ids = [o.id for o in self.queryset]
|
|
40
|
+
for dataset in Dataset.objects(organization__in=ids):
|
|
41
|
+
org_id = str(dataset.organization.id)
|
|
42
|
+
if self.downloads_counts.get(org_id) is None:
|
|
43
|
+
self.downloads_counts[org_id] = 0
|
|
44
|
+
|
|
45
|
+
self.downloads_counts[org_id] += sum(resource.metrics.get('views', 0) for resource in dataset.resources)
|
|
46
|
+
|
|
47
|
+
return self.downloads_counts
|
|
@@ -14,7 +14,8 @@ from udata.uris import endpoint_for
|
|
|
14
14
|
|
|
15
15
|
__all__ = (
|
|
16
16
|
'Organization', 'Team', 'Member', 'MembershipRequest',
|
|
17
|
-
'ORG_ROLES', 'MEMBERSHIP_STATUS', 'PUBLIC_SERVICE', 'CERTIFIED'
|
|
17
|
+
'ORG_ROLES', 'MEMBERSHIP_STATUS', 'PUBLIC_SERVICE', 'CERTIFIED',
|
|
18
|
+
'ASSOCIATION', 'COMPANY', 'LOCAL_AUTHORITY'
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
@@ -36,6 +37,9 @@ LOGO_SIZES = [100, 60, 25]
|
|
|
36
37
|
|
|
37
38
|
PUBLIC_SERVICE = 'public-service'
|
|
38
39
|
CERTIFIED = 'certified'
|
|
40
|
+
ASSOCIATION = 'Association'
|
|
41
|
+
COMPANY = 'Company'
|
|
42
|
+
LOCAL_AUTHORITY = 'Local authority'
|
|
39
43
|
|
|
40
44
|
TITLE_SIZE_LIMIT = 350
|
|
41
45
|
DESCRIPTION_SIZE_LIMIT = 100000
|
|
@@ -140,6 +144,9 @@ class Organization(WithMetrics, BadgeMixin, db.Datetimed, db.Document):
|
|
|
140
144
|
__badges__ = {
|
|
141
145
|
PUBLIC_SERVICE: _('Public Service'),
|
|
142
146
|
CERTIFIED: _('Certified'),
|
|
147
|
+
ASSOCIATION: _('Association'),
|
|
148
|
+
COMPANY: _('Company'),
|
|
149
|
+
LOCAL_AUTHORITY: _('Local authority'),
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
__metrics_keys__ = [
|
|
@@ -199,6 +206,18 @@ class Organization(WithMetrics, BadgeMixin, db.Datetimed, db.Document):
|
|
|
199
206
|
is_public_service = any(b.kind == PUBLIC_SERVICE for b in self.badges)
|
|
200
207
|
return self.certified and is_public_service
|
|
201
208
|
|
|
209
|
+
@property
|
|
210
|
+
def company(self):
|
|
211
|
+
return any(b.kind == COMPANY for b in self.badges)
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def association(self):
|
|
215
|
+
return any(b.kind == ASSOCIATION for b in self.badges)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def local_authority(self):
|
|
219
|
+
return any(b.kind == LOCAL_AUTHORITY for b in self.badges)
|
|
220
|
+
|
|
202
221
|
def member(self, user):
|
|
203
222
|
for member in self.members:
|
|
204
223
|
if member.user == user:
|
udata/core/organization/tasks.py
CHANGED
|
@@ -7,7 +7,7 @@ from udata.tasks import job, task, get_logger
|
|
|
7
7
|
|
|
8
8
|
from udata.core.badges.tasks import notify_new_badge
|
|
9
9
|
|
|
10
|
-
from .models import Organization, CERTIFIED, PUBLIC_SERVICE
|
|
10
|
+
from .models import Organization, CERTIFIED, PUBLIC_SERVICE, COMPANY, ASSOCIATION, LOCAL_AUTHORITY
|
|
11
11
|
|
|
12
12
|
log = get_logger(__name__)
|
|
13
13
|
|
|
@@ -114,3 +114,63 @@ def notify_badge_public_service(org_id):
|
|
|
114
114
|
organization=org,
|
|
115
115
|
badge=org.get_badge(PUBLIC_SERVICE)
|
|
116
116
|
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@notify_new_badge(Organization, COMPANY)
|
|
120
|
+
def notify_badge_company(org_id):
|
|
121
|
+
'''
|
|
122
|
+
Send an email when a `COMPANY` badge is added to an `Organization`
|
|
123
|
+
'''
|
|
124
|
+
org = Organization.objects.get(pk=org_id)
|
|
125
|
+
recipients = [member.user for member in org.members]
|
|
126
|
+
subject = _(
|
|
127
|
+
'Your organization "%(name)s" has been identified as a company',
|
|
128
|
+
name=org.name
|
|
129
|
+
)
|
|
130
|
+
mail.send(
|
|
131
|
+
subject,
|
|
132
|
+
recipients,
|
|
133
|
+
'badge_added_company',
|
|
134
|
+
organization=org,
|
|
135
|
+
badge=org.get_badge(COMPANY)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@notify_new_badge(Organization, ASSOCIATION)
|
|
140
|
+
def notify_badge_association(org_id):
|
|
141
|
+
'''
|
|
142
|
+
Send an email when a `ASSOCIATION` badge is added to an `Organization`
|
|
143
|
+
'''
|
|
144
|
+
org = Organization.objects.get(pk=org_id)
|
|
145
|
+
recipients = [member.user for member in org.members]
|
|
146
|
+
subject = _(
|
|
147
|
+
'Your organization "%(name)s" has been identified as an association',
|
|
148
|
+
name=org.name
|
|
149
|
+
)
|
|
150
|
+
mail.send(
|
|
151
|
+
subject,
|
|
152
|
+
recipients,
|
|
153
|
+
'badge_added_association',
|
|
154
|
+
organization=org,
|
|
155
|
+
badge=org.get_badge(ASSOCIATION)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@notify_new_badge(Organization, LOCAL_AUTHORITY)
|
|
160
|
+
def notify_badge_local_authority(org_id):
|
|
161
|
+
'''
|
|
162
|
+
Send an email when a `LOCAL_AUTHORITY` badge is added to an `Organization`
|
|
163
|
+
'''
|
|
164
|
+
org = Organization.objects.get(pk=org_id)
|
|
165
|
+
recipients = [member.user for member in org.members]
|
|
166
|
+
subject = _(
|
|
167
|
+
'Your organization "%(name)s" has been identified as a local authority',
|
|
168
|
+
name=org.name
|
|
169
|
+
)
|
|
170
|
+
mail.send(
|
|
171
|
+
subject,
|
|
172
|
+
recipients,
|
|
173
|
+
'badge_added_local_authority',
|
|
174
|
+
organization=org,
|
|
175
|
+
badge=org.get_badge(LOCAL_AUTHORITY)
|
|
176
|
+
)
|
udata/core/spatial/commands.py
CHANGED
|
@@ -44,7 +44,10 @@ def load_levels(col, json_levels):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def load_zones(col, json_geozones):
|
|
47
|
-
|
|
47
|
+
loaded_geozones = 0
|
|
48
|
+
for _, geozone in enumerate(json_geozones):
|
|
49
|
+
if geozone.get('is_deleted', False):
|
|
50
|
+
continue
|
|
48
51
|
params = {
|
|
49
52
|
'slug': slugify.slugify(geozone['nom'], separator='-'),
|
|
50
53
|
'level': str(geozone['level']),
|
|
@@ -56,11 +59,12 @@ def load_zones(col, json_geozones):
|
|
|
56
59
|
col.objects(id=geozone['_id']).modify(upsert=True, **{
|
|
57
60
|
'set__{0}'.format(k): v for k, v in params.items()
|
|
58
61
|
})
|
|
62
|
+
loaded_geozones += 1
|
|
59
63
|
except errors.ValidationError as e:
|
|
60
64
|
log.warning('Validation error (%s) for %s with %s',
|
|
61
65
|
e, geozone['nom'], params)
|
|
62
66
|
continue
|
|
63
|
-
return
|
|
67
|
+
return loaded_geozones
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
@contextmanager
|
|
@@ -137,6 +141,10 @@ def load(geozones_file, levels_file, drop=False):
|
|
|
137
141
|
total = load_zones(GeoZone, json_geozones)
|
|
138
142
|
log.info('Loaded {total} zones'.format(total=total))
|
|
139
143
|
|
|
144
|
+
log.info('Clean removed geozones in datasets')
|
|
145
|
+
count = fixup_removed_geozone()
|
|
146
|
+
log.info(f'{count} geozones removed from datasets')
|
|
147
|
+
|
|
140
148
|
|
|
141
149
|
@grp.command()
|
|
142
150
|
def migrate():
|
|
@@ -184,3 +192,19 @@ def migrate():
|
|
|
184
192
|
'''.format(level_summary, **counter)), level_summary])
|
|
185
193
|
log.info(summary)
|
|
186
194
|
log.info('Done')
|
|
195
|
+
|
|
196
|
+
def fixup_removed_geozone():
|
|
197
|
+
count = 0
|
|
198
|
+
all_datasets = Dataset.objects(spatial__zones__0__exists=True).timeout(False)
|
|
199
|
+
for dataset in all_datasets:
|
|
200
|
+
zones = dataset.spatial.zones
|
|
201
|
+
new_zones = [z for z in zones if getattr(z, 'name', None) is not None]
|
|
202
|
+
|
|
203
|
+
if len(new_zones) < len(zones):
|
|
204
|
+
log.debug(f"Removing deleted zones from dataset '{dataset.title}'")
|
|
205
|
+
count += len(zones) - len(new_zones)
|
|
206
|
+
dataset.spatial.zones = new_zones
|
|
207
|
+
dataset.save()
|
|
208
|
+
|
|
209
|
+
return count
|
|
210
|
+
|
udata/core/topic/api.py
CHANGED
|
@@ -3,6 +3,7 @@ from udata.core.dataset.api_fields import dataset_fields
|
|
|
3
3
|
from udata.core.discussions.models import Discussion
|
|
4
4
|
from udata.core.organization.api_fields import org_ref_fields
|
|
5
5
|
from udata.core.reuse.api_fields import reuse_fields
|
|
6
|
+
from udata.core.spatial.api_fields import spatial_coverage_fields
|
|
6
7
|
from udata.core.topic.permissions import TopicEditPermission
|
|
7
8
|
from udata.core.topic.parsers import TopicApiParser
|
|
8
9
|
from udata.core.user.api_fields import user_ref_fields
|
|
@@ -37,6 +38,11 @@ topic_fields = api.model('Topic', {
|
|
|
37
38
|
'private': fields.Boolean(description='Is the topic private'),
|
|
38
39
|
'created_at': fields.ISODateTime(
|
|
39
40
|
description='The topic creation date', readonly=True),
|
|
41
|
+
'spatial': fields.Nested(
|
|
42
|
+
spatial_coverage_fields, allow_null=True,
|
|
43
|
+
description='The spatial coverage'),
|
|
44
|
+
'last_modified': fields.ISODateTime(
|
|
45
|
+
description='The topic last modification date', readonly=True),
|
|
40
46
|
'organization': fields.Nested(
|
|
41
47
|
org_ref_fields, allow_null=True,
|
|
42
48
|
description='The publishing organization', readonly=True),
|
udata/core/topic/apiv2.py
CHANGED
|
@@ -13,6 +13,7 @@ from udata.core.organization.api_fields import org_ref_fields
|
|
|
13
13
|
from udata.core.reuse.api import ReuseApiParser
|
|
14
14
|
from udata.core.reuse.apiv2 import reuse_page_fields
|
|
15
15
|
from udata.core.reuse.models import Reuse
|
|
16
|
+
from udata.core.spatial.api_fields import spatial_coverage_fields
|
|
16
17
|
from udata.core.topic.models import Topic
|
|
17
18
|
from udata.core.topic.parsers import TopicApiParser
|
|
18
19
|
from udata.core.topic.permissions import TopicEditPermission
|
|
@@ -63,6 +64,11 @@ topic_fields = apiv2.model('Topic', {
|
|
|
63
64
|
'private': fields.Boolean(description='Is the topic private'),
|
|
64
65
|
'created_at': fields.ISODateTime(
|
|
65
66
|
description='The topic creation date', readonly=True),
|
|
67
|
+
'spatial': fields.Nested(
|
|
68
|
+
spatial_coverage_fields, allow_null=True,
|
|
69
|
+
description='The spatial coverage'),
|
|
70
|
+
'last_modified': fields.ISODateTime(
|
|
71
|
+
description='The topic last modification date', readonly=True),
|
|
66
72
|
'organization': fields.Nested(
|
|
67
73
|
org_ref_fields, allow_null=True,
|
|
68
74
|
description='The publishing organization', readonly=True),
|
udata/core/topic/forms.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from udata.forms import ModelForm, fields, validators
|
|
2
|
+
from udata.core.spatial.forms import SpatialCoverageField
|
|
2
3
|
from udata.i18n import lazy_gettext as _
|
|
3
4
|
|
|
4
5
|
from .models import Topic
|
|
@@ -20,6 +21,10 @@ class TopicForm(ModelForm):
|
|
|
20
21
|
datasets = fields.DatasetListField(_('Associated datasets'))
|
|
21
22
|
reuses = fields.ReuseListField(_('Associated reuses'))
|
|
22
23
|
|
|
24
|
+
spatial = SpatialCoverageField(
|
|
25
|
+
_('Spatial coverage'),
|
|
26
|
+
description=_('The geographical area covered by the data.'))
|
|
27
|
+
|
|
23
28
|
tags = fields.TagField(_('Tags'), [validators.DataRequired()])
|
|
24
29
|
private = fields.BooleanField(_('Private'))
|
|
25
30
|
featured = fields.BooleanField(_('Featured'))
|
udata/core/topic/models.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
1
|
from flask import url_for
|
|
3
|
-
from mongoengine.fields import DateTimeField
|
|
4
2
|
from mongoengine.signals import pre_save
|
|
5
|
-
from udata.models import db
|
|
3
|
+
from udata.models import db, SpatialCoverage
|
|
6
4
|
from udata.search import reindex
|
|
7
5
|
from udata.tasks import as_task_param
|
|
8
6
|
|
|
@@ -10,7 +8,7 @@ from udata.tasks import as_task_param
|
|
|
10
8
|
__all__ = ('Topic', )
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
class Topic(db.Document, db.Owned):
|
|
11
|
+
class Topic(db.Document, db.Owned, db.Datetimed):
|
|
14
12
|
name = db.StringField(required=True)
|
|
15
13
|
slug = db.SlugField(max_length=255, required=True, populate_from='name',
|
|
16
14
|
update=True, follow=True)
|
|
@@ -28,7 +26,7 @@ class Topic(db.Document, db.Owned):
|
|
|
28
26
|
private = db.BooleanField()
|
|
29
27
|
extras = db.ExtrasField()
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
spatial = db.EmbeddedDocumentField(SpatialCoverage)
|
|
32
30
|
|
|
33
31
|
meta = {
|
|
34
32
|
'indexes': [
|
udata/forms/fields.py
CHANGED
|
@@ -180,6 +180,16 @@ class BooleanField(FieldHelper, fields.BooleanField):
|
|
|
180
180
|
self.stacked = kwargs.pop('stacked', False)
|
|
181
181
|
super(BooleanField, self).__init__(*args, **kwargs)
|
|
182
182
|
|
|
183
|
+
def process_formdata(self, valuelist):
|
|
184
|
+
# We override this so that when no value is provided
|
|
185
|
+
# the form doesn't think the value is `False` instead
|
|
186
|
+
# the value is not present and the model can keep the
|
|
187
|
+
# existing value
|
|
188
|
+
if not valuelist:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
super().process_formdata(valuelist)
|
|
192
|
+
|
|
183
193
|
|
|
184
194
|
class RadioField(FieldHelper, fields.RadioField):
|
|
185
195
|
def __init__(self, *args, **kwargs):
|
udata/frontend/csv.py
CHANGED
|
@@ -58,8 +58,8 @@ class Adapter(object):
|
|
|
58
58
|
else:
|
|
59
59
|
field_tuple = (name, self.getter(*field))
|
|
60
60
|
except Exception as e: # Catch all errors intentionally.
|
|
61
|
-
log.
|
|
62
|
-
name=self.__class__.__name__, error=e))
|
|
61
|
+
log.exception('Error exporting CSV for {name}: {error_class} {error}'.format(
|
|
62
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e), stack_info=True)
|
|
63
63
|
self._fields.append(field_tuple)
|
|
64
64
|
return self._fields
|
|
65
65
|
|
|
@@ -89,8 +89,8 @@ class Adapter(object):
|
|
|
89
89
|
try:
|
|
90
90
|
content = safestr(getter(obj))
|
|
91
91
|
except Exception as e: # Catch all errors intentionally.
|
|
92
|
-
log.
|
|
93
|
-
name=self.__class__.__name__, error=e))
|
|
92
|
+
log.exception('Error exporting CSV for {name}: {error_class} {error}'.format(
|
|
93
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e), stack_info=True)
|
|
94
94
|
row.append(content)
|
|
95
95
|
return row
|
|
96
96
|
|
|
@@ -130,8 +130,8 @@ class NestedAdapter(Adapter):
|
|
|
130
130
|
else:
|
|
131
131
|
field_tuple = (name, self.getter(*field))
|
|
132
132
|
except Exception as e: # Catch all errors intentionally.
|
|
133
|
-
log.
|
|
134
|
-
name=self.__class__.__name__, error=e))
|
|
133
|
+
log.exception('Error exporting CSV for {name}: {error_class} {error}'.format(
|
|
134
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e), stack_info=True)
|
|
135
135
|
self._nested_fields.append(field_tuple)
|
|
136
136
|
return self._nested_fields
|
|
137
137
|
|
|
@@ -155,8 +155,8 @@ class NestedAdapter(Adapter):
|
|
|
155
155
|
try:
|
|
156
156
|
content = safestr(getter(nested))
|
|
157
157
|
except Exception as e: # Catch all errors intentionally.
|
|
158
|
-
log.
|
|
159
|
-
name=self.__class__.__name__, error=e))
|
|
158
|
+
log.exception('Error exporting CSV for {name}: {error_class} {error}'.format(
|
|
159
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e), stack_info=True)
|
|
160
160
|
row.append(content)
|
|
161
161
|
return row
|
|
162
162
|
|
udata/harvest/actions.py
CHANGED
|
@@ -10,6 +10,7 @@ from flask import current_app
|
|
|
10
10
|
from udata.auth import current_user
|
|
11
11
|
from udata.core.dataset.models import HarvestDatasetMetadata
|
|
12
12
|
from udata.models import User, Organization, PeriodicTask, Dataset
|
|
13
|
+
from udata.storage.s3 import delete_file
|
|
13
14
|
|
|
14
15
|
from . import backends, signals
|
|
15
16
|
from .models import (
|
|
@@ -162,6 +163,16 @@ def purge_jobs():
|
|
|
162
163
|
'''Delete jobs older than retention policy'''
|
|
163
164
|
retention = current_app.config['HARVEST_JOBS_RETENTION_DAYS']
|
|
164
165
|
expiration = datetime.utcnow() - timedelta(days=retention)
|
|
166
|
+
|
|
167
|
+
jobs_with_external_files = HarvestJob.objects(data__filename__exists=True, created__lt=expiration)
|
|
168
|
+
for job in jobs_with_external_files:
|
|
169
|
+
bucket = current_app.config.get('HARVEST_GRAPHS_S3_BUCKET')
|
|
170
|
+
if bucket is None:
|
|
171
|
+
log.error(f"Bucket isn't configured anymore, but jobs still exist with external filenames. Could not delete them.")
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
delete_file(bucket, job.data['filename'])
|
|
175
|
+
|
|
165
176
|
return HarvestJob.objects(created__lt=expiration).delete()
|
|
166
177
|
|
|
167
178
|
|