udata 10.4.3.dev35551__py2.py3-none-any.whl → 10.4.3.dev35617__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 +22 -0
- udata/core/dataset/api.py +33 -17
- udata/core/dataset/api_fields.py +21 -0
- udata/core/dataset/apiv2.py +14 -11
- udata/core/dataset/models.py +19 -0
- 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 +13 -1
- udata/static/chunks/{11.0f04e49a40a0a381bcce.js → 11.b6f741fcc366abfad9c4.js} +3 -3
- udata/static/chunks/{11.0f04e49a40a0a381bcce.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
- udata/static/chunks/{13.d9c1735d14038b94c17e.js → 13.2d06442dd9a05d9777b5.js} +2 -2
- udata/static/chunks/{13.d9c1735d14038b94c17e.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
- udata/static/chunks/{17.81c57c0dedf812e43013.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
- udata/static/chunks/{17.81c57c0dedf812e43013.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
- udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
- udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
- udata/static/chunks/{8.494b003a94383b142c18.js → 8.778091d55cd8ea39af6b.js} +2 -2
- udata/static/chunks/{8.494b003a94383b142c18.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_follow_api.py +20 -0
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/METADATA +3 -1
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/RECORD +32 -32
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/LICENSE +0 -0
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/WHEEL +0 -0
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/entry_points.txt +0 -0
- {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.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 (
|
|
@@ -33,6 +34,15 @@ from udata.uris import endpoint_for
|
|
|
33
34
|
# "temporal_coverage"
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
dataservice_permissions_fields = api.model(
|
|
38
|
+
"DataservicePermissions",
|
|
39
|
+
{
|
|
40
|
+
"delete": fields.Permission(),
|
|
41
|
+
"edit": fields.Permission(),
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
36
46
|
class DataserviceQuerySet(OwnedQuerySet):
|
|
37
47
|
def visible(self):
|
|
38
48
|
return self(archived_at=None, deleted_at=None, private=False)
|
|
@@ -285,6 +295,18 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
285
295
|
def is_hidden(self):
|
|
286
296
|
return self.private or self.deleted_at or self.archived_at
|
|
287
297
|
|
|
298
|
+
@property
|
|
299
|
+
@function_field(
|
|
300
|
+
nested_fields=dataservice_permissions_fields,
|
|
301
|
+
)
|
|
302
|
+
def permissions(self):
|
|
303
|
+
from .permissions import DataserviceEditPermission
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"delete": DataserviceEditPermission(self),
|
|
307
|
+
"edit": DataserviceEditPermission(self),
|
|
308
|
+
}
|
|
309
|
+
|
|
288
310
|
def count_discussions(self):
|
|
289
311
|
self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
|
|
290
312
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
udata/core/dataset/api.py
CHANGED
|
@@ -39,6 +39,7 @@ from udata.core.badges.fields import badge_fields
|
|
|
39
39
|
from udata.core.dataservices.models import Dataservice
|
|
40
40
|
from udata.core.dataset.models import CHECKSUM_TYPES
|
|
41
41
|
from udata.core.followers.api import FollowAPI
|
|
42
|
+
from udata.core.followers.models import Follow
|
|
42
43
|
from udata.core.organization.models import Organization
|
|
43
44
|
from udata.core.reuse.models import Reuse
|
|
44
45
|
from udata.core.site.models import current_site
|
|
@@ -84,7 +85,6 @@ from .models import (
|
|
|
84
85
|
ResourceSchema,
|
|
85
86
|
get_resource,
|
|
86
87
|
)
|
|
87
|
-
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
88
88
|
from .rdf import dataset_to_rdf
|
|
89
89
|
|
|
90
90
|
DEFAULT_SORTING = "-created_at_internal"
|
|
@@ -122,6 +122,12 @@ class DatasetApiParser(ModelApiParser):
|
|
|
122
122
|
location="args",
|
|
123
123
|
)
|
|
124
124
|
self.parser.add_argument("owner", type=str, location="args")
|
|
125
|
+
self.parser.add_argument(
|
|
126
|
+
"followed_by",
|
|
127
|
+
type=str,
|
|
128
|
+
location="args",
|
|
129
|
+
help="(beta, subject to change/be removed)",
|
|
130
|
+
)
|
|
125
131
|
self.parser.add_argument("format", type=str, location="args")
|
|
126
132
|
self.parser.add_argument("schema", type=str, location="args")
|
|
127
133
|
self.parser.add_argument("schema_version", type=str, location="args")
|
|
@@ -183,6 +189,16 @@ class DatasetApiParser(ModelApiParser):
|
|
|
183
189
|
if not ObjectId.is_valid(args["owner"]):
|
|
184
190
|
api.abort(400, "Owner arg must be an identifier")
|
|
185
191
|
datasets = datasets.filter(owner=args["owner"])
|
|
192
|
+
if args.get("followed_by"):
|
|
193
|
+
if not ObjectId.is_valid(args["followed_by"]):
|
|
194
|
+
api.abort(400, "`followed_by` arg must be an identifier")
|
|
195
|
+
ids = [
|
|
196
|
+
f.following.id
|
|
197
|
+
for f in Follow.objects(follower=args["followed_by"]).filter(
|
|
198
|
+
__raw__={"following._cls": Dataset._class_name}
|
|
199
|
+
)
|
|
200
|
+
]
|
|
201
|
+
datasets = datasets.filter(id__in=ids)
|
|
186
202
|
if args.get("format"):
|
|
187
203
|
datasets = datasets.filter(resources__format=args["format"])
|
|
188
204
|
if args.get("schema"):
|
|
@@ -342,9 +358,9 @@ class DatasetsAtomFeedAPI(API):
|
|
|
342
358
|
class DatasetAPI(API):
|
|
343
359
|
@api.doc("get_dataset")
|
|
344
360
|
@api.marshal_with(dataset_fields)
|
|
345
|
-
def get(self, dataset):
|
|
361
|
+
def get(self, dataset: Dataset):
|
|
346
362
|
"""Get a dataset given its identifier"""
|
|
347
|
-
if not
|
|
363
|
+
if not dataset.permissions["edit"].can():
|
|
348
364
|
if dataset.private:
|
|
349
365
|
api.abort(404)
|
|
350
366
|
elif dataset.deleted:
|
|
@@ -356,12 +372,12 @@ class DatasetAPI(API):
|
|
|
356
372
|
@api.expect(dataset_fields)
|
|
357
373
|
@api.marshal_with(dataset_fields)
|
|
358
374
|
@api.response(400, errors.VALIDATION_ERROR)
|
|
359
|
-
def put(self, dataset):
|
|
375
|
+
def put(self, dataset: Dataset):
|
|
360
376
|
"""Update a dataset given its identifier"""
|
|
361
377
|
request_deleted = request.json.get("deleted", True)
|
|
362
378
|
if dataset.deleted and request_deleted is not None:
|
|
363
379
|
api.abort(410, "Dataset has been deleted")
|
|
364
|
-
|
|
380
|
+
dataset.permissions["edit"].test()
|
|
365
381
|
dataset.last_modified_internal = datetime.utcnow()
|
|
366
382
|
form = api.validate(DatasetForm, dataset)
|
|
367
383
|
|
|
@@ -374,7 +390,7 @@ class DatasetAPI(API):
|
|
|
374
390
|
"""Delete a dataset given its identifier"""
|
|
375
391
|
if dataset.deleted:
|
|
376
392
|
api.abort(410, "Dataset has been deleted")
|
|
377
|
-
|
|
393
|
+
dataset.permissions["delete"].test()
|
|
378
394
|
dataset.deleted = datetime.utcnow()
|
|
379
395
|
dataset.last_modified_internal = datetime.utcnow()
|
|
380
396
|
dataset.save()
|
|
@@ -420,7 +436,7 @@ class DatasetRdfAPI(API):
|
|
|
420
436
|
class DatasetRdfFormatAPI(API):
|
|
421
437
|
@api.doc("rdf_dataset_format")
|
|
422
438
|
def get(self, dataset, format):
|
|
423
|
-
if not
|
|
439
|
+
if not dataset.permissions["edit"].can():
|
|
424
440
|
if dataset.private:
|
|
425
441
|
api.abort(404)
|
|
426
442
|
elif dataset.deleted:
|
|
@@ -479,7 +495,7 @@ class ResourcesAPI(API):
|
|
|
479
495
|
@api.marshal_with(resource_fields, code=201)
|
|
480
496
|
def post(self, dataset):
|
|
481
497
|
"""Create a new resource for a given dataset"""
|
|
482
|
-
|
|
498
|
+
dataset.permissions["edit_resources"].test()
|
|
483
499
|
form = api.validate(ResourceFormWithoutId)
|
|
484
500
|
resource = Resource()
|
|
485
501
|
|
|
@@ -497,7 +513,7 @@ class ResourcesAPI(API):
|
|
|
497
513
|
@api.marshal_list_with(resource_fields)
|
|
498
514
|
def put(self, dataset):
|
|
499
515
|
"""Reorder resources"""
|
|
500
|
-
|
|
516
|
+
dataset.permissions["edit_resources"].test()
|
|
501
517
|
resources = request.json
|
|
502
518
|
if len(dataset.resources) != len(resources):
|
|
503
519
|
api.abort(
|
|
@@ -551,7 +567,7 @@ class UploadNewDatasetResource(UploadMixin, API):
|
|
|
551
567
|
@api.marshal_with(upload_fields, code=201)
|
|
552
568
|
def post(self, dataset):
|
|
553
569
|
"""Upload a file for a new dataset resource"""
|
|
554
|
-
|
|
570
|
+
dataset.permissions["edit_resources"].test()
|
|
555
571
|
infos = self.handle_upload(dataset)
|
|
556
572
|
resource = Resource(**infos)
|
|
557
573
|
dataset.add_resource(resource)
|
|
@@ -602,7 +618,7 @@ class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
|
602
618
|
@api.marshal_with(upload_fields)
|
|
603
619
|
def post(self, dataset, rid):
|
|
604
620
|
"""Upload a file related to a given resource on a given dataset"""
|
|
605
|
-
|
|
621
|
+
dataset.permissions["edit_resources"].test()
|
|
606
622
|
resource = self.get_resource_or_404(dataset, rid)
|
|
607
623
|
fs_filename_to_remove = resource.fs_filename
|
|
608
624
|
infos = self.handle_upload(dataset)
|
|
@@ -631,7 +647,7 @@ class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
|
631
647
|
@api.marshal_with(upload_community_fields)
|
|
632
648
|
def post(self, community):
|
|
633
649
|
"""Update the file related to a given community resource"""
|
|
634
|
-
|
|
650
|
+
community.permissions["edit"].test()
|
|
635
651
|
fs_filename_to_remove = community.fs_filename
|
|
636
652
|
infos = self.handle_upload(community.dataset)
|
|
637
653
|
community.update(**infos)
|
|
@@ -648,7 +664,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
648
664
|
@api.marshal_with(resource_fields)
|
|
649
665
|
def get(self, dataset, rid):
|
|
650
666
|
"""Get a resource given its identifier"""
|
|
651
|
-
if not
|
|
667
|
+
if not dataset.permissions["edit"].can():
|
|
652
668
|
if dataset.private:
|
|
653
669
|
api.abort(404)
|
|
654
670
|
elif dataset.deleted:
|
|
@@ -662,7 +678,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
662
678
|
@api.marshal_with(resource_fields)
|
|
663
679
|
def put(self, dataset, rid):
|
|
664
680
|
"""Update a given resource on a given dataset"""
|
|
665
|
-
|
|
681
|
+
dataset.permissions["edit_resources"].test()
|
|
666
682
|
resource = self.get_resource_or_404(dataset, rid)
|
|
667
683
|
form = api.validate(ResourceFormWithoutId, resource)
|
|
668
684
|
|
|
@@ -691,7 +707,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
691
707
|
@api.doc("delete_resource")
|
|
692
708
|
def delete(self, dataset, rid):
|
|
693
709
|
"""Delete a given resource on a given dataset"""
|
|
694
|
-
|
|
710
|
+
dataset.permissions["edit_resources"].test()
|
|
695
711
|
resource = self.get_resource_or_404(dataset, rid)
|
|
696
712
|
dataset.remove_resource(resource)
|
|
697
713
|
dataset.last_modified_internal = datetime.utcnow()
|
|
@@ -751,7 +767,7 @@ class CommunityResourceAPI(API):
|
|
|
751
767
|
@api.marshal_with(community_resource_fields)
|
|
752
768
|
def put(self, community):
|
|
753
769
|
"""Update a given community resource"""
|
|
754
|
-
|
|
770
|
+
community.permissions["edit"].test()
|
|
755
771
|
form = api.validate(CommunityResourceForm, community)
|
|
756
772
|
if community.filetype == "file":
|
|
757
773
|
form._fields.get("url").data = community.url
|
|
@@ -766,7 +782,7 @@ class CommunityResourceAPI(API):
|
|
|
766
782
|
@api.doc("delete_community_resource")
|
|
767
783
|
def delete(self, community):
|
|
768
784
|
"""Delete a given community resource"""
|
|
769
|
-
|
|
785
|
+
community.permissions["delete"].test()
|
|
770
786
|
# Deletes community resource's file from file storage
|
|
771
787
|
if community.fs_filename is not None:
|
|
772
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
|
@@ -706,6 +706,16 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, db.Document):
|
|
|
706
706
|
"Dataset's organization did not define the requested custom metadata."
|
|
707
707
|
)
|
|
708
708
|
|
|
709
|
+
@property
|
|
710
|
+
def permissions(self):
|
|
711
|
+
from .permissions import DatasetEditPermission, ResourceEditPermission
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
"delete": DatasetEditPermission(self),
|
|
715
|
+
"edit": DatasetEditPermission(self),
|
|
716
|
+
"edit_resources": ResourceEditPermission(self),
|
|
717
|
+
}
|
|
718
|
+
|
|
709
719
|
def url_for(self, *args, **kwargs):
|
|
710
720
|
return endpoint_for("datasets.show", "api.dataset", dataset=self, *args, **kwargs)
|
|
711
721
|
|
|
@@ -1088,6 +1098,15 @@ class CommunityResource(ResourceMixin, WithMetrics, Owned, db.Document):
|
|
|
1088
1098
|
def from_community(self):
|
|
1089
1099
|
return True
|
|
1090
1100
|
|
|
1101
|
+
@property
|
|
1102
|
+
def permissions(self):
|
|
1103
|
+
from .permissions import ResourceEditPermission
|
|
1104
|
+
|
|
1105
|
+
return {
|
|
1106
|
+
"delete": ResourceEditPermission(self),
|
|
1107
|
+
"edit": ResourceEditPermission(self),
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1091
1110
|
|
|
1092
1111
|
class ResourceSchema(object):
|
|
1093
1112
|
@staticmethod
|
udata/core/reuse/api.py
CHANGED
|
@@ -33,7 +33,6 @@ from .api_fields import (
|
|
|
33
33
|
reuse_type_fields,
|
|
34
34
|
)
|
|
35
35
|
from .models import Reuse
|
|
36
|
-
from .permissions import ReuseEditPermission
|
|
37
36
|
|
|
38
37
|
DEFAULT_SORTING = "-created_at"
|
|
39
38
|
SUGGEST_SORTING = "-metrics.followers"
|
|
@@ -179,7 +178,7 @@ class ReuseAPI(API):
|
|
|
179
178
|
@api.marshal_with(Reuse.__read_fields__)
|
|
180
179
|
def get(self, reuse):
|
|
181
180
|
"""Fetch a given reuse"""
|
|
182
|
-
if not
|
|
181
|
+
if not reuse.permissions["edit"].can():
|
|
183
182
|
if reuse.private:
|
|
184
183
|
api.abort(404)
|
|
185
184
|
elif reuse.deleted:
|
|
@@ -196,7 +195,7 @@ class ReuseAPI(API):
|
|
|
196
195
|
request_deleted = request.json.get("deleted", True)
|
|
197
196
|
if reuse.deleted and request_deleted is not None:
|
|
198
197
|
api.abort(410, "This reuse has been deleted")
|
|
199
|
-
|
|
198
|
+
reuse.permissions["edit"].test()
|
|
200
199
|
|
|
201
200
|
# This is a patch but old API acted like PATCH on PUT requests.
|
|
202
201
|
return patch_and_save(reuse, request)
|
|
@@ -208,7 +207,7 @@ class ReuseAPI(API):
|
|
|
208
207
|
"""Delete a given reuse"""
|
|
209
208
|
if reuse.deleted:
|
|
210
209
|
api.abort(410, "This reuse has been deleted")
|
|
211
|
-
|
|
210
|
+
reuse.permissions["delete"].test()
|
|
212
211
|
reuse.deleted = datetime.utcnow()
|
|
213
212
|
reuse.save()
|
|
214
213
|
return "", 204
|
|
@@ -335,7 +334,7 @@ class ReuseImageAPI(API):
|
|
|
335
334
|
@api.marshal_with(uploaded_image_fields)
|
|
336
335
|
def post(self, reuse):
|
|
337
336
|
"""Upload a new reuse image"""
|
|
338
|
-
|
|
337
|
+
reuse.permissions["edit"].test()
|
|
339
338
|
parse_uploaded_image(reuse.image)
|
|
340
339
|
reuse.save()
|
|
341
340
|
|
udata/core/reuse/api_fields.py
CHANGED
|
@@ -4,6 +4,14 @@ from .constants import IMAGE_SIZES
|
|
|
4
4
|
|
|
5
5
|
BIGGEST_IMAGE_SIZE = IMAGE_SIZES[0]
|
|
6
6
|
|
|
7
|
+
reuse_permissions_fields = api.model(
|
|
8
|
+
"ReusePermissions",
|
|
9
|
+
{
|
|
10
|
+
"delete": fields.Permission(),
|
|
11
|
+
"edit": fields.Permission(),
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
|
|
7
15
|
reuse_type_fields = api.model(
|
|
8
16
|
"ReuseType",
|
|
9
17
|
{
|
udata/core/reuse/apiv2.py
CHANGED
|
@@ -5,8 +5,10 @@ from udata.api import API, apiv2
|
|
|
5
5
|
from udata.core.reuse.models import Reuse
|
|
6
6
|
from udata.utils import multi_to_dict
|
|
7
7
|
|
|
8
|
+
from .api_fields import reuse_permissions_fields
|
|
8
9
|
from .search import ReuseSearch
|
|
9
10
|
|
|
11
|
+
apiv2.inherit("ReusePermissions", reuse_permissions_fields)
|
|
10
12
|
apiv2.inherit("ReusePage", Reuse.__page_fields__)
|
|
11
13
|
apiv2.inherit("Reuse (read)", Reuse.__read_fields__)
|
|
12
14
|
|
udata/core/reuse/models.py
CHANGED
|
@@ -6,7 +6,7 @@ from udata.api_fields import field, function_field, generate_fields
|
|
|
6
6
|
from udata.core.activity.models import Auditable
|
|
7
7
|
from udata.core.dataset.api_fields import dataset_fields
|
|
8
8
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
9
|
-
from udata.core.reuse.api_fields import BIGGEST_IMAGE_SIZE
|
|
9
|
+
from udata.core.reuse.api_fields import BIGGEST_IMAGE_SIZE, reuse_permissions_fields
|
|
10
10
|
from udata.core.storages import default_image_basename, images
|
|
11
11
|
from udata.frontend.markdown import mdstrip
|
|
12
12
|
from udata.i18n import lazy_gettext as _
|
|
@@ -200,6 +200,18 @@ class Reuse(db.Datetimed, Auditable, WithMetrics, ReuseBadgeMixin, Owned, db.Doc
|
|
|
200
200
|
"reuses.show", reuse=self, _external=True, fallback_endpoint="api.reuse"
|
|
201
201
|
)
|
|
202
202
|
|
|
203
|
+
@property
|
|
204
|
+
@function_field(
|
|
205
|
+
nested_fields=reuse_permissions_fields,
|
|
206
|
+
)
|
|
207
|
+
def permissions(self):
|
|
208
|
+
from .permissions import ReuseEditPermission
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"delete": ReuseEditPermission(self),
|
|
212
|
+
"edit": ReuseEditPermission(self),
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
@property
|
|
204
216
|
def is_visible(self):
|
|
205
217
|
return not self.is_hidden
|