udata 10.4.3.dev35606__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.

Files changed (35) hide show
  1. udata/api_fields.py +13 -1
  2. udata/commands/fixtures.py +11 -1
  3. udata/core/dataservices/api.py +7 -9
  4. udata/core/dataservices/apiv2.py +2 -0
  5. udata/core/dataservices/models.py +22 -0
  6. udata/core/dataset/api.py +16 -17
  7. udata/core/dataset/api_fields.py +21 -0
  8. udata/core/dataset/apiv2.py +14 -11
  9. udata/core/dataset/models.py +19 -0
  10. udata/core/reuse/api.py +4 -5
  11. udata/core/reuse/api_fields.py +8 -0
  12. udata/core/reuse/apiv2.py +2 -0
  13. udata/core/reuse/models.py +13 -1
  14. udata/static/chunks/{11.51d706fb9521c16976bc.js → 11.b6f741fcc366abfad9c4.js} +3 -3
  15. udata/static/chunks/{11.51d706fb9521c16976bc.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
  16. udata/static/chunks/{13.39e106d56f794ebd06a0.js → 13.2d06442dd9a05d9777b5.js} +2 -2
  17. udata/static/chunks/{13.39e106d56f794ebd06a0.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
  18. udata/static/chunks/{17.70cbb4a91b002338007e.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
  19. udata/static/chunks/{17.70cbb4a91b002338007e.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
  20. udata/static/chunks/{19.a348a5fff8fe2801e52a.js → 19.f03a102365af4315f9db.js} +3 -3
  21. udata/static/chunks/{19.a348a5fff8fe2801e52a.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
  22. udata/static/chunks/{5.343ca020a2d38cec1a14.js → 5.0fa1408dae4e76b87b2e.js} +3 -3
  23. udata/static/chunks/{5.343ca020a2d38cec1a14.js.map → 5.0fa1408dae4e76b87b2e.js.map} +1 -1
  24. udata/static/chunks/{6.a3b07de9dd2ca2d24e85.js → 6.d663709d877baa44a71e.js} +3 -3
  25. udata/static/chunks/{6.a3b07de9dd2ca2d24e85.js.map → 6.d663709d877baa44a71e.js.map} +1 -1
  26. udata/static/chunks/{8.462bb3029de008497675.js → 8.778091d55cd8ea39af6b.js} +2 -2
  27. udata/static/chunks/{8.462bb3029de008497675.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
  28. udata/static/common.js +1 -1
  29. udata/static/common.js.map +1 -1
  30. {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35617.dist-info}/METADATA +2 -1
  31. {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35617.dist-info}/RECORD +35 -35
  32. {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35617.dist-info}/LICENSE +0 -0
  33. {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35617.dist-info}/WHEEL +0 -0
  34. {udata-10.4.3.dev35606.dist-info → udata-10.4.3.dev35617.dist-info}/entry_points.txt +0 -0
  35. {udata-10.4.3.dev35606.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
- read_fields[method_name] = restx_fields.String(
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):
@@ -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": ["datasets", "image_thumbnail", "page", "uri", "owner"],
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
 
@@ -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 OwnablePermission(dataservice).can():
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
- OwnablePermission(dataservice).test()
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
- OwnablePermission(dataservice).test()
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
- OwnablePermission(dataservice).test()
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
- OwnablePermission(dataservice).test()
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 DataserviceEditPermission(dataservice).can():
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:
@@ -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
@@ -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 DatasetEditPermission(dataset).can():
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
- DatasetEditPermission(dataset).test()
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
- DatasetEditPermission(dataset).test()
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 DatasetEditPermission(dataset).can():
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(community).test()
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 DatasetEditPermission(dataset).can():
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(community).test()
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
- ResourceEditPermission(community).test()
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)
@@ -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
  )
@@ -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 DatasetEditPermission(dataset).can():
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 DatasetEditPermission(dataset).can():
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
- DatasetEditPermission(dataset).test()
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
- DatasetEditPermission(dataset).test()
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 DatasetEditPermission(dataset).can():
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 DatasetEditPermission(dataset).can():
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 DatasetEditPermission(dataset).can():
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 DatasetEditPermission(dataset).can():
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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:
@@ -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 ReuseEditPermission(reuse).can():
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
- ReuseEditPermission(reuse).test()
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
- ReuseEditPermission(reuse).test()
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
- ReuseEditPermission(reuse).test()
337
+ reuse.permissions["edit"].test()
339
338
  parse_uploaded_image(reuse.image)
340
339
  reuse.save()
341
340
 
@@ -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
 
@@ -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