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.

Files changed (32) 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 +33 -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.0f04e49a40a0a381bcce.js → 11.b6f741fcc366abfad9c4.js} +3 -3
  15. udata/static/chunks/{11.0f04e49a40a0a381bcce.js.map → 11.b6f741fcc366abfad9c4.js.map} +1 -1
  16. udata/static/chunks/{13.d9c1735d14038b94c17e.js → 13.2d06442dd9a05d9777b5.js} +2 -2
  17. udata/static/chunks/{13.d9c1735d14038b94c17e.js.map → 13.2d06442dd9a05d9777b5.js.map} +1 -1
  18. udata/static/chunks/{17.81c57c0dedf812e43013.js → 17.e8e4caaad5cb0cc0bacc.js} +2 -2
  19. udata/static/chunks/{17.81c57c0dedf812e43013.js.map → 17.e8e4caaad5cb0cc0bacc.js.map} +1 -1
  20. udata/static/chunks/{19.8da42e8359d72afc2618.js → 19.f03a102365af4315f9db.js} +3 -3
  21. udata/static/chunks/{19.8da42e8359d72afc2618.js.map → 19.f03a102365af4315f9db.js.map} +1 -1
  22. udata/static/chunks/{8.494b003a94383b142c18.js → 8.778091d55cd8ea39af6b.js} +2 -2
  23. udata/static/chunks/{8.494b003a94383b142c18.js.map → 8.778091d55cd8ea39af6b.js.map} +1 -1
  24. udata/static/common.js +1 -1
  25. udata/static/common.js.map +1 -1
  26. udata/tests/api/test_follow_api.py +20 -0
  27. {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/METADATA +3 -1
  28. {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/RECORD +32 -32
  29. {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/LICENSE +0 -0
  30. {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/WHEEL +0 -0
  31. {udata-10.4.3.dev35551.dist-info → udata-10.4.3.dev35617.dist-info}/entry_points.txt +0 -0
  32. {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
- 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
@@ -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 DatasetEditPermission(dataset).can():
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
- DatasetEditPermission(dataset).test()
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
- DatasetEditPermission(dataset).test()
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 DatasetEditPermission(dataset).can():
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(community).test()
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 DatasetEditPermission(dataset).can():
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(dataset).test()
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
- ResourceEditPermission(community).test()
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
- ResourceEditPermission(community).test()
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)
@@ -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