udata 10.4.3.dev35606__py2.py3-none-any.whl → 10.4.3.dev35659__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api_fields.py +13 -1
- udata/commands/fixtures.py +11 -1
- udata/core/dataservices/api.py +7 -9
- udata/core/dataservices/apiv2.py +2 -0
- udata/core/dataservices/models.py +27 -0
- udata/core/dataset/api.py +16 -17
- udata/core/dataset/api_fields.py +21 -0
- udata/core/dataset/apiv2.py +14 -11
- udata/core/dataset/models.py +26 -0
- udata/core/organization/models.py +29 -2
- udata/core/reuse/api.py +4 -5
- udata/core/reuse/api_fields.py +8 -0
- udata/core/reuse/apiv2.py +2 -0
- udata/core/reuse/models.py +18 -1
- udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.b6f741fcc366abfad9c4.js} +3 -3
- udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
- udata/static/chunks/{13.39e106d56f794ebd06a0.js → 13.2d06442dd9a05d9777b5.js} +2 -2
- udata/static/chunks/{13.39e106d56f794ebd06a0.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
- udata/static/chunks/{17.70cbb4a91b002338007e.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
- udata/static/chunks/{17.70cbb4a91b002338007e.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
- udata/static/chunks/{19.a348a5fff8fe2801e52a.js → 19.f03a102365af4315f9db.js} +3 -3
- udata/static/chunks/{19.a348a5fff8fe2801e52a.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
- udata/static/chunks/{5.343ca020a2d38cec1a14.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
- udata/static/chunks/{5.343ca020a2d38cec1a14.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
- udata/static/chunks/{6.a3b07de9dd2ca2d24e85.js → 6.d663709d877baa44a71e.js} +3 -3
- udata/static/chunks/{6.a3b07de9dd2ca2d24e85.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
- udata/static/chunks/{8.462bb3029de008497675.js → 8.778091d55cd8ea39af6b.js} +2 -2
- udata/static/chunks/{8.462bb3029de008497675.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/METADATA +3 -1
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/RECORD +36 -36
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/LICENSE +0 -0
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/WHEEL +0 -0
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/entry_points.txt +0 -0
- {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35659.dist-info}/top_level.txt +0 -0
udata/api_fields.py
CHANGED
|
@@ -341,6 +341,9 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
341
341
|
continue # Do not override if the attribute is also callable like for Extras
|
|
342
342
|
|
|
343
343
|
method = getattr(cls, method_name)
|
|
344
|
+
if isinstance(method, property):
|
|
345
|
+
method = method.fget
|
|
346
|
+
|
|
344
347
|
if not callable(method):
|
|
345
348
|
continue
|
|
346
349
|
|
|
@@ -356,7 +359,16 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
356
359
|
"""
|
|
357
360
|
return lambda o: method(o)
|
|
358
361
|
|
|
359
|
-
|
|
362
|
+
nested_fields: dict | None = additional_field_info.get("nested_fields")
|
|
363
|
+
if nested_fields is None:
|
|
364
|
+
# If there is no `nested_fields` convert the object to the string representation.
|
|
365
|
+
field_constructor = restx_fields.String
|
|
366
|
+
else:
|
|
367
|
+
|
|
368
|
+
def field_constructor(**kwargs):
|
|
369
|
+
return restx_fields.Nested(nested_fields, **kwargs)
|
|
370
|
+
|
|
371
|
+
read_fields[method_name] = field_constructor(
|
|
360
372
|
attribute=make_lambda(method), **{"readonly": True, **additional_field_info}
|
|
361
373
|
)
|
|
362
374
|
if additional_field_info.get("show_as_ref", False):
|
udata/commands/fixtures.py
CHANGED
|
@@ -54,16 +54,25 @@ UNWANTED_KEYS: dict[str, list[str]] = {
|
|
|
54
54
|
"badges",
|
|
55
55
|
"spatial",
|
|
56
56
|
"quality",
|
|
57
|
+
"permissions",
|
|
57
58
|
],
|
|
58
59
|
"resource": ["latest", "preview_url", "last_modified"],
|
|
59
60
|
"organization": ["class", "page", "uri", "logo_thumbnail"],
|
|
60
|
-
"reuse": [
|
|
61
|
+
"reuse": [
|
|
62
|
+
"datasets",
|
|
63
|
+
"image_thumbnail",
|
|
64
|
+
"page",
|
|
65
|
+
"uri",
|
|
66
|
+
"owner",
|
|
67
|
+
"permissions",
|
|
68
|
+
],
|
|
61
69
|
"community": [
|
|
62
70
|
"dataset",
|
|
63
71
|
"owner",
|
|
64
72
|
"latest",
|
|
65
73
|
"last_modified",
|
|
66
74
|
"preview_url",
|
|
75
|
+
"permissions",
|
|
67
76
|
],
|
|
68
77
|
"discussion": ["subject", "url", "class", "permissions"],
|
|
69
78
|
"discussion_message": ["permissions"],
|
|
@@ -75,6 +84,7 @@ UNWANTED_KEYS: dict[str, list[str]] = {
|
|
|
75
84
|
"owner",
|
|
76
85
|
"self_api_url",
|
|
77
86
|
"self_web_url",
|
|
87
|
+
"permissions",
|
|
78
88
|
],
|
|
79
89
|
}
|
|
80
90
|
|
udata/core/dataservices/api.py
CHANGED
|
@@ -10,7 +10,6 @@ from flask_login import current_user
|
|
|
10
10
|
from udata.api import API, api, fields
|
|
11
11
|
from udata.api_fields import patch
|
|
12
12
|
from udata.core.dataservices.constants import DATASERVICE_ACCESS_TYPE_RESTRICTED
|
|
13
|
-
from udata.core.dataservices.permissions import OwnablePermission
|
|
14
13
|
from udata.core.dataset.models import Dataset
|
|
15
14
|
from udata.core.followers.api import FollowAPI
|
|
16
15
|
from udata.core.site.models import current_site
|
|
@@ -19,7 +18,6 @@ from udata.i18n import gettext as _
|
|
|
19
18
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
20
19
|
|
|
21
20
|
from .models import Dataservice
|
|
22
|
-
from .permissions import DataserviceEditPermission
|
|
23
21
|
from .rdf import dataservice_to_rdf
|
|
24
22
|
|
|
25
23
|
ns = api.namespace("dataservices", "Dataservices related operations (beta)")
|
|
@@ -99,7 +97,7 @@ class DataserviceAPI(API):
|
|
|
99
97
|
@api.doc("get_dataservice")
|
|
100
98
|
@api.marshal_with(Dataservice.__read_fields__)
|
|
101
99
|
def get(self, dataservice):
|
|
102
|
-
if not
|
|
100
|
+
if not dataservice.permissions["edit"].can():
|
|
103
101
|
if dataservice.private:
|
|
104
102
|
api.abort(404)
|
|
105
103
|
elif dataservice.deleted_at:
|
|
@@ -117,7 +115,7 @@ class DataserviceAPI(API):
|
|
|
117
115
|
):
|
|
118
116
|
api.abort(410, "dataservice has been deleted")
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
dataservice.permissions["edit"].test()
|
|
121
119
|
|
|
122
120
|
patch(dataservice, request)
|
|
123
121
|
dataservice.metadata_modified_at = datetime.utcnow()
|
|
@@ -134,7 +132,7 @@ class DataserviceAPI(API):
|
|
|
134
132
|
if dataservice.deleted_at:
|
|
135
133
|
api.abort(410, "dataservice has been deleted")
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
dataservice.permissions["delete"].test()
|
|
138
136
|
dataservice.deleted_at = datetime.utcnow()
|
|
139
137
|
dataservice.metadata_modified_at = datetime.utcnow()
|
|
140
138
|
dataservice.save()
|
|
@@ -167,7 +165,7 @@ class DataserviceDatasetsAPI(API):
|
|
|
167
165
|
if dataservice.deleted_at:
|
|
168
166
|
api.abort(410, "Dataservice has been deleted")
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
dataservice.permissions["edit"].test()
|
|
171
169
|
|
|
172
170
|
data = request.json
|
|
173
171
|
|
|
@@ -203,7 +201,7 @@ class DataserviceDatasetAPI(API):
|
|
|
203
201
|
if dataservice.deleted_at:
|
|
204
202
|
api.abort(410, "Dataservice has been deleted")
|
|
205
203
|
|
|
206
|
-
|
|
204
|
+
dataservice.permissions["edit"].test()
|
|
207
205
|
|
|
208
206
|
if dataset not in dataservice.datasets:
|
|
209
207
|
api.abort(404, "Dataset not found in dataservice")
|
|
@@ -232,8 +230,8 @@ class DataserviceRdfAPI(API):
|
|
|
232
230
|
@api.response(410, "Dataservice has been deleted")
|
|
233
231
|
class DataserviceRdfFormatAPI(API):
|
|
234
232
|
@api.doc("rdf_dataservice_format")
|
|
235
|
-
def get(self, dataservice, format):
|
|
236
|
-
if not
|
|
233
|
+
def get(self, dataservice: Dataservice, format):
|
|
234
|
+
if not dataservice.permissions["edit"].can():
|
|
237
235
|
if dataservice.private:
|
|
238
236
|
api.abort(404)
|
|
239
237
|
elif dataservice.deleted_at:
|
udata/core/dataservices/apiv2.py
CHANGED
|
@@ -5,8 +5,10 @@ from udata.api import API, apiv2
|
|
|
5
5
|
from udata.core.dataservices.models import AccessAudience, Dataservice, HarvestMetadata
|
|
6
6
|
from udata.utils import multi_to_dict
|
|
7
7
|
|
|
8
|
+
from .models import dataservice_permissions_fields
|
|
8
9
|
from .search import DataserviceSearch
|
|
9
10
|
|
|
11
|
+
apiv2.inherit("DataservicePermissions", dataservice_permissions_fields)
|
|
10
12
|
apiv2.inherit("DataservicePage", Dataservice.__page_fields__)
|
|
11
13
|
apiv2.inherit("Dataservice (read)", Dataservice.__read_fields__)
|
|
12
14
|
apiv2.inherit("HarvestMetadata (read)", HarvestMetadata.__read_fields__)
|
|
@@ -7,6 +7,7 @@ from mongoengine.signals import post_save
|
|
|
7
7
|
|
|
8
8
|
import udata.core.contact_point.api_fields as contact_api_fields
|
|
9
9
|
import udata.core.dataset.api_fields as datasets_api_fields
|
|
10
|
+
from udata.api import api, fields
|
|
10
11
|
from udata.api_fields import field, function_field, generate_fields
|
|
11
12
|
from udata.core.activity.models import Auditable
|
|
12
13
|
from udata.core.dataservices.constants import (
|
|
@@ -16,6 +17,7 @@ from udata.core.dataservices.constants import (
|
|
|
16
17
|
DATASERVICE_FORMATS,
|
|
17
18
|
)
|
|
18
19
|
from udata.core.dataset.models import Dataset
|
|
20
|
+
from udata.core.metrics.helpers import get_stock_metrics
|
|
19
21
|
from udata.core.metrics.models import WithMetrics
|
|
20
22
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
21
23
|
from udata.i18n import lazy_gettext as _
|
|
@@ -33,6 +35,15 @@ from udata.uris import endpoint_for
|
|
|
33
35
|
# "temporal_coverage"
|
|
34
36
|
|
|
35
37
|
|
|
38
|
+
dataservice_permissions_fields = api.model(
|
|
39
|
+
"DataservicePermissions",
|
|
40
|
+
{
|
|
41
|
+
"delete": fields.Permission(),
|
|
42
|
+
"edit": fields.Permission(),
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
36
47
|
class DataserviceQuerySet(OwnedQuerySet):
|
|
37
48
|
def visible(self):
|
|
38
49
|
return self(archived_at=None, deleted_at=None, private=False)
|
|
@@ -278,6 +289,7 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
278
289
|
__metrics_keys__ = [
|
|
279
290
|
"discussions",
|
|
280
291
|
"followers",
|
|
292
|
+
"followers_by_months",
|
|
281
293
|
"views",
|
|
282
294
|
]
|
|
283
295
|
|
|
@@ -285,12 +297,27 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
285
297
|
def is_hidden(self):
|
|
286
298
|
return self.private or self.deleted_at or self.archived_at
|
|
287
299
|
|
|
300
|
+
@property
|
|
301
|
+
@function_field(
|
|
302
|
+
nested_fields=dataservice_permissions_fields,
|
|
303
|
+
)
|
|
304
|
+
def permissions(self):
|
|
305
|
+
from .permissions import DataserviceEditPermission
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"delete": DataserviceEditPermission(self),
|
|
309
|
+
"edit": DataserviceEditPermission(self),
|
|
310
|
+
}
|
|
311
|
+
|
|
288
312
|
def count_discussions(self):
|
|
289
313
|
self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
|
|
290
314
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
291
315
|
|
|
292
316
|
def count_followers(self):
|
|
293
317
|
self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
|
|
318
|
+
self.metrics["followers_by_months"] = get_stock_metrics(
|
|
319
|
+
Follow.objects(following=self), date_label="since"
|
|
320
|
+
)
|
|
294
321
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
295
322
|
|
|
296
323
|
|
udata/core/dataset/api.py
CHANGED
|
@@ -85,7 +85,6 @@ from .models import (
|
|
|
85
85
|
ResourceSchema,
|
|
86
86
|
get_resource,
|
|
87
87
|
)
|
|
88
|
-
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
89
88
|
from .rdf import dataset_to_rdf
|
|
90
89
|
|
|
91
90
|
DEFAULT_SORTING = "-created_at_internal"
|
|
@@ -359,9 +358,9 @@ class DatasetsAtomFeedAPI(API):
|
|
|
359
358
|
class DatasetAPI(API):
|
|
360
359
|
@api.doc("get_dataset")
|
|
361
360
|
@api.marshal_with(dataset_fields)
|
|
362
|
-
def get(self, dataset):
|
|
361
|
+
def get(self, dataset: Dataset):
|
|
363
362
|
"""Get a dataset given its identifier"""
|
|
364
|
-
if not
|
|
363
|
+
if not dataset.permissions["edit"].can():
|
|
365
364
|
if dataset.private:
|
|
366
365
|
api.abort(404)
|
|
367
366
|
elif dataset.deleted:
|
|
@@ -373,12 +372,12 @@ class DatasetAPI(API):
|
|
|
373
372
|
@api.expect(dataset_fields)
|
|
374
373
|
@api.marshal_with(dataset_fields)
|
|
375
374
|
@api.response(400, errors.VALIDATION_ERROR)
|
|
376
|
-
def put(self, dataset):
|
|
375
|
+
def put(self, dataset: Dataset):
|
|
377
376
|
"""Update a dataset given its identifier"""
|
|
378
377
|
request_deleted = request.json.get("deleted", True)
|
|
379
378
|
if dataset.deleted and request_deleted is not None:
|
|
380
379
|
api.abort(410, "Dataset has been deleted")
|
|
381
|
-
|
|
380
|
+
dataset.permissions["edit"].test()
|
|
382
381
|
dataset.last_modified_internal = datetime.utcnow()
|
|
383
382
|
form = api.validate(DatasetForm, dataset)
|
|
384
383
|
|
|
@@ -391,7 +390,7 @@ class DatasetAPI(API):
|
|
|
391
390
|
"""Delete a dataset given its identifier"""
|
|
392
391
|
if dataset.deleted:
|
|
393
392
|
api.abort(410, "Dataset has been deleted")
|
|
394
|
-
|
|
393
|
+
dataset.permissions["delete"].test()
|
|
395
394
|
dataset.deleted = datetime.utcnow()
|
|
396
395
|
dataset.last_modified_internal = datetime.utcnow()
|
|
397
396
|
dataset.save()
|
|
@@ -437,7 +436,7 @@ class DatasetRdfAPI(API):
|
|
|
437
436
|
class DatasetRdfFormatAPI(API):
|
|
438
437
|
@api.doc("rdf_dataset_format")
|
|
439
438
|
def get(self, dataset, format):
|
|
440
|
-
if not
|
|
439
|
+
if not dataset.permissions["edit"].can():
|
|
441
440
|
if dataset.private:
|
|
442
441
|
api.abort(404)
|
|
443
442
|
elif dataset.deleted:
|
|
@@ -496,7 +495,7 @@ class ResourcesAPI(API):
|
|
|
496
495
|
@api.marshal_with(resource_fields, code=201)
|
|
497
496
|
def post(self, dataset):
|
|
498
497
|
"""Create a new resource for a given dataset"""
|
|
499
|
-
|
|
498
|
+
dataset.permissions["edit_resources"].test()
|
|
500
499
|
form = api.validate(ResourceFormWithoutId)
|
|
501
500
|
resource = Resource()
|
|
502
501
|
|
|
@@ -514,7 +513,7 @@ class ResourcesAPI(API):
|
|
|
514
513
|
@api.marshal_list_with(resource_fields)
|
|
515
514
|
def put(self, dataset):
|
|
516
515
|
"""Reorder resources"""
|
|
517
|
-
|
|
516
|
+
dataset.permissions["edit_resources"].test()
|
|
518
517
|
resources = request.json
|
|
519
518
|
if len(dataset.resources) != len(resources):
|
|
520
519
|
api.abort(
|
|
@@ -568,7 +567,7 @@ class UploadNewDatasetResource(UploadMixin, API):
|
|
|
568
567
|
@api.marshal_with(upload_fields, code=201)
|
|
569
568
|
def post(self, dataset):
|
|
570
569
|
"""Upload a file for a new dataset resource"""
|
|
571
|
-
|
|
570
|
+
dataset.permissions["edit_resources"].test()
|
|
572
571
|
infos = self.handle_upload(dataset)
|
|
573
572
|
resource = Resource(**infos)
|
|
574
573
|
dataset.add_resource(resource)
|
|
@@ -619,7 +618,7 @@ class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
|
619
618
|
@api.marshal_with(upload_fields)
|
|
620
619
|
def post(self, dataset, rid):
|
|
621
620
|
"""Upload a file related to a given resource on a given dataset"""
|
|
622
|
-
|
|
621
|
+
dataset.permissions["edit_resources"].test()
|
|
623
622
|
resource = self.get_resource_or_404(dataset, rid)
|
|
624
623
|
fs_filename_to_remove = resource.fs_filename
|
|
625
624
|
infos = self.handle_upload(dataset)
|
|
@@ -648,7 +647,7 @@ class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
|
648
647
|
@api.marshal_with(upload_community_fields)
|
|
649
648
|
def post(self, community):
|
|
650
649
|
"""Update the file related to a given community resource"""
|
|
651
|
-
|
|
650
|
+
community.permissions["edit"].test()
|
|
652
651
|
fs_filename_to_remove = community.fs_filename
|
|
653
652
|
infos = self.handle_upload(community.dataset)
|
|
654
653
|
community.update(**infos)
|
|
@@ -665,7 +664,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
665
664
|
@api.marshal_with(resource_fields)
|
|
666
665
|
def get(self, dataset, rid):
|
|
667
666
|
"""Get a resource given its identifier"""
|
|
668
|
-
if not
|
|
667
|
+
if not dataset.permissions["edit"].can():
|
|
669
668
|
if dataset.private:
|
|
670
669
|
api.abort(404)
|
|
671
670
|
elif dataset.deleted:
|
|
@@ -679,7 +678,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
679
678
|
@api.marshal_with(resource_fields)
|
|
680
679
|
def put(self, dataset, rid):
|
|
681
680
|
"""Update a given resource on a given dataset"""
|
|
682
|
-
|
|
681
|
+
dataset.permissions["edit_resources"].test()
|
|
683
682
|
resource = self.get_resource_or_404(dataset, rid)
|
|
684
683
|
form = api.validate(ResourceFormWithoutId, resource)
|
|
685
684
|
|
|
@@ -708,7 +707,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
708
707
|
@api.doc("delete_resource")
|
|
709
708
|
def delete(self, dataset, rid):
|
|
710
709
|
"""Delete a given resource on a given dataset"""
|
|
711
|
-
|
|
710
|
+
dataset.permissions["edit_resources"].test()
|
|
712
711
|
resource = self.get_resource_or_404(dataset, rid)
|
|
713
712
|
dataset.remove_resource(resource)
|
|
714
713
|
dataset.last_modified_internal = datetime.utcnow()
|
|
@@ -768,7 +767,7 @@ class CommunityResourceAPI(API):
|
|
|
768
767
|
@api.marshal_with(community_resource_fields)
|
|
769
768
|
def put(self, community):
|
|
770
769
|
"""Update a given community resource"""
|
|
771
|
-
|
|
770
|
+
community.permissions["edit"].test()
|
|
772
771
|
form = api.validate(CommunityResourceForm, community)
|
|
773
772
|
if community.filetype == "file":
|
|
774
773
|
form._fields.get("url").data = community.url
|
|
@@ -783,7 +782,7 @@ class CommunityResourceAPI(API):
|
|
|
783
782
|
@api.doc("delete_community_resource")
|
|
784
783
|
def delete(self, community):
|
|
785
784
|
"""Delete a given community resource"""
|
|
786
|
-
|
|
785
|
+
community.permissions["delete"].test()
|
|
787
786
|
# Deletes community resource's file from file storage
|
|
788
787
|
if community.fs_filename is not None:
|
|
789
788
|
storages.resources.delete(community.fs_filename)
|
udata/core/dataset/api_fields.py
CHANGED
|
@@ -220,6 +220,15 @@ dataset_ref_fields = api.inherit(
|
|
|
220
220
|
},
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
+
|
|
224
|
+
community_resource_permissions_fields = api.model(
|
|
225
|
+
"DatasetPermissions",
|
|
226
|
+
{
|
|
227
|
+
"delete": fields.Permission(),
|
|
228
|
+
"edit": fields.Permission(),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
|
|
223
232
|
community_resource_fields = api.inherit(
|
|
224
233
|
"CommunityResource",
|
|
225
234
|
resource_fields,
|
|
@@ -233,6 +242,7 @@ community_resource_fields = api.inherit(
|
|
|
233
242
|
"owner": fields.Nested(
|
|
234
243
|
user_ref_fields, allow_null=True, description="The user information"
|
|
235
244
|
),
|
|
245
|
+
"permissions": fields.Nested(community_resource_permissions_fields),
|
|
236
246
|
},
|
|
237
247
|
)
|
|
238
248
|
|
|
@@ -285,6 +295,7 @@ DEFAULT_MASK = ",".join(
|
|
|
285
295
|
"internal",
|
|
286
296
|
"contact_points",
|
|
287
297
|
"featured",
|
|
298
|
+
"permissions",
|
|
288
299
|
)
|
|
289
300
|
)
|
|
290
301
|
|
|
@@ -300,6 +311,15 @@ dataset_internal_fields = api.model(
|
|
|
300
311
|
},
|
|
301
312
|
)
|
|
302
313
|
|
|
314
|
+
dataset_permissions_fields = api.model(
|
|
315
|
+
"DatasetPermissions",
|
|
316
|
+
{
|
|
317
|
+
"delete": fields.Permission(),
|
|
318
|
+
"edit": fields.Permission(),
|
|
319
|
+
"edit_resources": fields.Permission(),
|
|
320
|
+
},
|
|
321
|
+
)
|
|
322
|
+
|
|
303
323
|
dataset_fields = api.model(
|
|
304
324
|
"Dataset",
|
|
305
325
|
{
|
|
@@ -401,6 +421,7 @@ dataset_fields = api.model(
|
|
|
401
421
|
"contact_points": fields.List(
|
|
402
422
|
fields.Nested(contact_point_fields, description="The dataset contact points"),
|
|
403
423
|
),
|
|
424
|
+
"permissions": fields.Nested(dataset_permissions_fields),
|
|
404
425
|
},
|
|
405
426
|
mask=DEFAULT_MASK,
|
|
406
427
|
)
|
udata/core/dataset/apiv2.py
CHANGED
|
@@ -20,6 +20,7 @@ from .api_fields import (
|
|
|
20
20
|
checksum_fields,
|
|
21
21
|
dataset_harvest_fields,
|
|
22
22
|
dataset_internal_fields,
|
|
23
|
+
dataset_permissions_fields,
|
|
23
24
|
org_ref_fields,
|
|
24
25
|
resource_fields,
|
|
25
26
|
resource_harvest_fields,
|
|
@@ -31,7 +32,6 @@ from .api_fields import (
|
|
|
31
32
|
)
|
|
32
33
|
from .constants import DEFAULT_FREQUENCY, DEFAULT_LICENSE, FULL_OBJECTS_HEADER, UPDATE_FREQUENCIES
|
|
33
34
|
from .models import CommunityResource, Dataset
|
|
34
|
-
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
35
35
|
from .search import DatasetSearch
|
|
36
36
|
|
|
37
37
|
DEFAULT_PAGE_SIZE = 50
|
|
@@ -70,6 +70,7 @@ DEFAULT_MASK_APIV2 = ",".join(
|
|
|
70
70
|
"internal",
|
|
71
71
|
"contact_points",
|
|
72
72
|
"featured",
|
|
73
|
+
"permissions",
|
|
73
74
|
)
|
|
74
75
|
)
|
|
75
76
|
|
|
@@ -224,6 +225,7 @@ dataset_fields = apiv2.model(
|
|
|
224
225
|
required=False,
|
|
225
226
|
description="The dataset contact points",
|
|
226
227
|
),
|
|
228
|
+
"permissions": fields.Nested(dataset_permissions_fields),
|
|
227
229
|
},
|
|
228
230
|
mask=DEFAULT_MASK_APIV2,
|
|
229
231
|
)
|
|
@@ -269,6 +271,7 @@ apiv2.inherit("ResourceInternals", resource_internal_fields)
|
|
|
269
271
|
apiv2.inherit("ContactPoint", contact_point_fields)
|
|
270
272
|
apiv2.inherit("Schema", schema_fields)
|
|
271
273
|
apiv2.inherit("CatalogSchema", catalog_schema_fields)
|
|
274
|
+
apiv2.inherit("DatasetPermissions", dataset_permissions_fields)
|
|
272
275
|
|
|
273
276
|
|
|
274
277
|
@ns.route("/search/", endpoint="dataset_search")
|
|
@@ -318,7 +321,7 @@ class DatasetAPI(API):
|
|
|
318
321
|
@apiv2.marshal_with(dataset_fields)
|
|
319
322
|
def get(self, dataset):
|
|
320
323
|
"""Get a dataset given its identifier"""
|
|
321
|
-
if not
|
|
324
|
+
if not dataset.permissions["edit"].can():
|
|
322
325
|
if dataset.private:
|
|
323
326
|
apiv2.abort(404)
|
|
324
327
|
elif dataset.deleted:
|
|
@@ -335,7 +338,7 @@ class DatasetExtrasAPI(API):
|
|
|
335
338
|
@apiv2.doc("get_dataset_extras")
|
|
336
339
|
def get(self, dataset):
|
|
337
340
|
"""Get a dataset extras given its identifier"""
|
|
338
|
-
if not
|
|
341
|
+
if not dataset.permissions["edit"].can():
|
|
339
342
|
if dataset.private:
|
|
340
343
|
apiv2.abort(404)
|
|
341
344
|
elif dataset.deleted:
|
|
@@ -351,7 +354,7 @@ class DatasetExtrasAPI(API):
|
|
|
351
354
|
apiv2.abort(400, "Wrong payload format, dict expected")
|
|
352
355
|
if dataset.deleted:
|
|
353
356
|
apiv2.abort(410, "Dataset has been deleted")
|
|
354
|
-
|
|
357
|
+
dataset.permissions["edit"].test()
|
|
355
358
|
# first remove extras key associated to a None value in payload
|
|
356
359
|
for key in [k for k in data if data[k] is None]:
|
|
357
360
|
dataset.extras.pop(key, None)
|
|
@@ -370,7 +373,7 @@ class DatasetExtrasAPI(API):
|
|
|
370
373
|
apiv2.abort(400, "Wrong payload format, list expected")
|
|
371
374
|
if dataset.deleted:
|
|
372
375
|
apiv2.abort(410, "Dataset has been deleted")
|
|
373
|
-
|
|
376
|
+
dataset.permissions["delete"].test()
|
|
374
377
|
for key in data:
|
|
375
378
|
try:
|
|
376
379
|
del dataset.extras[key]
|
|
@@ -387,7 +390,7 @@ class ResourcesAPI(API):
|
|
|
387
390
|
@apiv2.marshal_with(resource_page_fields)
|
|
388
391
|
def get(self, dataset):
|
|
389
392
|
"""Get the given dataset resources, paginated."""
|
|
390
|
-
if not
|
|
393
|
+
if not dataset.permissions["edit"].can():
|
|
391
394
|
if dataset.private:
|
|
392
395
|
apiv2.abort(404)
|
|
393
396
|
elif dataset.deleted:
|
|
@@ -434,7 +437,7 @@ class DatasetSchemasAPI(API):
|
|
|
434
437
|
@apiv2.marshal_with(schema_fields)
|
|
435
438
|
def get(self, dataset):
|
|
436
439
|
"""Get a dataset schemas given its identifier"""
|
|
437
|
-
if not
|
|
440
|
+
if not dataset.permissions["edit"].can():
|
|
438
441
|
if dataset.private:
|
|
439
442
|
apiv2.abort(404)
|
|
440
443
|
elif dataset.deleted:
|
|
@@ -477,7 +480,7 @@ class ResourceAPI(API):
|
|
|
477
480
|
def get(self, rid):
|
|
478
481
|
dataset = Dataset.objects(resources__id=rid).first()
|
|
479
482
|
if dataset:
|
|
480
|
-
if not
|
|
483
|
+
if not dataset.permissions["edit"].can():
|
|
481
484
|
if dataset.private:
|
|
482
485
|
apiv2.abort(404)
|
|
483
486
|
elif dataset.deleted:
|
|
@@ -508,7 +511,7 @@ class ResourceExtrasAPI(ResourceMixin, API):
|
|
|
508
511
|
@apiv2.doc("get_resource_extras")
|
|
509
512
|
def get(self, dataset, rid):
|
|
510
513
|
"""Get a resource extras given its identifier"""
|
|
511
|
-
if not
|
|
514
|
+
if not dataset.permissions["edit"].can():
|
|
512
515
|
if dataset.private:
|
|
513
516
|
apiv2.abort(404)
|
|
514
517
|
elif dataset.deleted:
|
|
@@ -525,7 +528,7 @@ class ResourceExtrasAPI(ResourceMixin, API):
|
|
|
525
528
|
apiv2.abort(400, "Wrong payload format, dict expected")
|
|
526
529
|
if dataset.deleted:
|
|
527
530
|
apiv2.abort(410, "Dataset has been deleted")
|
|
528
|
-
|
|
531
|
+
dataset.permissions["edit_resources"].test()
|
|
529
532
|
resource = self.get_resource_or_404(dataset, rid)
|
|
530
533
|
# first remove extras key associated to a None value in payload
|
|
531
534
|
for key in [k for k in data if data[k] is None]:
|
|
@@ -545,7 +548,7 @@ class ResourceExtrasAPI(ResourceMixin, API):
|
|
|
545
548
|
apiv2.abort(400, "Wrong payload format, list expected")
|
|
546
549
|
if dataset.deleted:
|
|
547
550
|
apiv2.abort(410, "Dataset has been deleted")
|
|
548
|
-
|
|
551
|
+
dataset.permissions["edit_resources"].test()
|
|
549
552
|
resource = self.get_resource_or_404(dataset, rid)
|
|
550
553
|
try:
|
|
551
554
|
for key in data:
|
udata/core/dataset/models.py
CHANGED
|
@@ -20,6 +20,7 @@ from udata.api_fields import field
|
|
|
20
20
|
from udata.app import cache
|
|
21
21
|
from udata.core import storages
|
|
22
22
|
from udata.core.activity.models import Auditable
|
|
23
|
+
from udata.core.metrics.helpers import get_stock_metrics
|
|
23
24
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
24
25
|
from udata.frontend.markdown import mdstrip
|
|
25
26
|
from udata.i18n import lazy_gettext as _
|
|
@@ -599,7 +600,9 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
599
600
|
__metrics_keys__ = [
|
|
600
601
|
"discussions",
|
|
601
602
|
"reuses",
|
|
603
|
+
"reuses_by_months",
|
|
602
604
|
"followers",
|
|
605
|
+
"followers_by_months",
|
|
603
606
|
"views",
|
|
604
607
|
"resources_downloads",
|
|
605
608
|
]
|
|
@@ -706,6 +709,16 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
706
709
|
"Dataset's organization did not define the requested custom metadata."
|
|
707
710
|
)
|
|
708
711
|
|
|
712
|
+
@property
|
|
713
|
+
def permissions(self):
|
|
714
|
+
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
"delete": DatasetEditPermission(self),
|
|
718
|
+
"edit": DatasetEditPermission(self),
|
|
719
|
+
"edit_resources": ResourceEditPermission(self),
|
|
720
|
+
}
|
|
721
|
+
|
|
709
722
|
def url_for(self, *args, **kwargs):
|
|
710
723
|
return endpoint_for("datasets.show", "api.dataset", dataset=self, *args, **kwargs)
|
|
711
724
|
|
|
@@ -1053,12 +1066,16 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
1053
1066
|
from udata.models import Reuse
|
|
1054
1067
|
|
|
1055
1068
|
self.metrics["reuses"] = Reuse.objects(datasets=self).visible().count()
|
|
1069
|
+
self.metrics["reuses_by_months"] = get_stock_metrics(Reuse.objects(datasets=self).visible())
|
|
1056
1070
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
1057
1071
|
|
|
1058
1072
|
def count_followers(self):
|
|
1059
1073
|
from udata.models import Follow
|
|
1060
1074
|
|
|
1061
1075
|
self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
|
|
1076
|
+
self.metrics["followers_by_months"] = get_stock_metrics(
|
|
1077
|
+
Follow.objects(following=self), date_label="since"
|
|
1078
|
+
)
|
|
1062
1079
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
1063
1080
|
|
|
1064
1081
|
|
|
@@ -1088,6 +1105,15 @@ class CommunityResource(ResourceMixin, WithMetrics, Owned, db.Document):
|
|
|
1088
1105
|
def from_community(self):
|
|
1089
1106
|
return True
|
|
1090
1107
|
|
|
1108
|
+
@property
|
|
1109
|
+
def permissions(self):
|
|
1110
|
+
from .permissions import ResourceEditPermission
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
"delete": ResourceEditPermission(self),
|
|
1114
|
+
"edit": ResourceEditPermission(self),
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1091
1117
|
|
|
1092
1118
|
class ResourceSchema(object):
|
|
1093
1119
|
@staticmethod
|
|
@@ -8,6 +8,7 @@ from werkzeug.utils import cached_property
|
|
|
8
8
|
from udata.api_fields import field
|
|
9
9
|
from udata.core.activity.models import Auditable
|
|
10
10
|
from udata.core.badges.models import Badge, BadgeMixin, BadgesList
|
|
11
|
+
from udata.core.metrics.helpers import get_stock_metrics
|
|
11
12
|
from udata.core.metrics.models import WithMetrics
|
|
12
13
|
from udata.core.storages import avatars, default_image_basename
|
|
13
14
|
from udata.frontend.markdown import mdstrip
|
|
@@ -161,9 +162,16 @@ class Organization(Auditable, WithMetrics, OrganizationBadgeMixin, db.Datetimed,
|
|
|
161
162
|
return self.name or ""
|
|
162
163
|
|
|
163
164
|
__metrics_keys__ = [
|
|
165
|
+
"dataservices",
|
|
166
|
+
"dataservices_by_months",
|
|
164
167
|
"datasets",
|
|
168
|
+
"datasets_by_months",
|
|
169
|
+
"datasets_followers_by_months",
|
|
170
|
+
"datasets_reuses_by_months",
|
|
165
171
|
"members",
|
|
166
172
|
"reuses",
|
|
173
|
+
"reuses_by_months",
|
|
174
|
+
"reuses_followers_by_months",
|
|
167
175
|
"dataservices",
|
|
168
176
|
"followers",
|
|
169
177
|
"views",
|
|
@@ -299,21 +307,40 @@ class Organization(Auditable, WithMetrics, OrganizationBadgeMixin, db.Datetimed,
|
|
|
299
307
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
300
308
|
|
|
301
309
|
def count_datasets(self):
|
|
302
|
-
from udata.models import Dataset
|
|
310
|
+
from udata.models import Dataset, Follow, Reuse
|
|
303
311
|
|
|
304
312
|
self.metrics["datasets"] = Dataset.objects(organization=self).visible().count()
|
|
313
|
+
self.metrics["datasets_by_months"] = get_stock_metrics(
|
|
314
|
+
Dataset.objects(organization=self).visible(), date_label="created_at_internal"
|
|
315
|
+
)
|
|
316
|
+
self.metrics["datasets_followers_by_months"] = get_stock_metrics(
|
|
317
|
+
Follow.objects(following__in=Dataset.objects(organization=self)), date_label="since"
|
|
318
|
+
)
|
|
319
|
+
self.metrics["datasets_reuses_by_months"] = get_stock_metrics(
|
|
320
|
+
Reuse.objects(datasets__in=Dataset.objects(organization=self)).visible()
|
|
321
|
+
)
|
|
322
|
+
|
|
305
323
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
306
324
|
|
|
307
325
|
def count_reuses(self):
|
|
308
|
-
from udata.models import Reuse
|
|
326
|
+
from udata.models import Follow, Reuse
|
|
309
327
|
|
|
310
328
|
self.metrics["reuses"] = Reuse.objects(organization=self).visible().count()
|
|
329
|
+
self.metrics["reuses_by_months"] = get_stock_metrics(
|
|
330
|
+
Reuse.objects(organization=self).visible()
|
|
331
|
+
)
|
|
332
|
+
self.metrics["reuses_followers_by_months"] = get_stock_metrics(
|
|
333
|
+
Follow.objects(following__in=Reuse.objects(organization=self)), date_label="since"
|
|
334
|
+
)
|
|
311
335
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
312
336
|
|
|
313
337
|
def count_dataservices(self):
|
|
314
338
|
from udata.models import Dataservice
|
|
315
339
|
|
|
316
340
|
self.metrics["dataservices"] = Dataservice.objects(organization=self).visible().count()
|
|
341
|
+
self.metrics["dataservices_by_months"] = get_stock_metrics(
|
|
342
|
+
Dataservice.objects(organization=self).visible(), date_label="created_at"
|
|
343
|
+
)
|
|
317
344
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
318
345
|
|
|
319
346
|
def count_followers(self):
|