udata 10.4.2.dev35475__py2.py3-none-any.whl → 10.4.3__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/__init__.py +1 -1
- udata/api_fields.py +27 -2
- udata/commands/fixtures.py +11 -1
- udata/core/dataservices/api.py +12 -10
- udata/core/dataservices/apiv2.py +4 -1
- udata/core/dataservices/constants.py +19 -0
- udata/core/dataservices/models.py +54 -1
- udata/core/dataset/api.py +33 -27
- udata/core/dataset/api_fields.py +21 -0
- udata/core/dataset/apiv2.py +14 -11
- udata/core/dataset/models.py +60 -15
- udata/core/dataset/rdf.py +1 -1
- udata/core/dataset/tasks.py +3 -2
- udata/core/organization/api.py +11 -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/core/spatial/models.py +9 -0
- udata/core/user/models.py +11 -5
- udata/harvest/api.py +2 -1
- udata/harvest/tests/dcat/bnodes.xml +5 -0
- udata/harvest/tests/test_dcat_backend.py +1 -0
- udata/migrations/2025-06-18-clean-spatial-coverages.py +25 -0
- 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_dataservices_api.py +78 -0
- udata/tests/api/test_follow_api.py +20 -0
- udata/tests/api/test_organizations_api.py +25 -0
- udata/tests/test_api_fields.py +35 -0
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +98 -38
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +98 -38
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +98 -38
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +98 -38
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +98 -38
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +98 -38
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +98 -38
- udata/translations/udata.pot +98 -38
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/METADATA +16 -4
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/RECORD +62 -61
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/LICENSE +0 -0
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/WHEEL +0 -0
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/entry_points.txt +0 -0
- {udata-10.4.2.dev35475.dist-info → udata-10.4.3.dist-info}/top_level.txt +0 -0
udata/__init__.py
CHANGED
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):
|
|
@@ -505,7 +517,6 @@ def patch(obj, request) -> type:
|
|
|
505
517
|
field = obj.__write_fields__.get(key)
|
|
506
518
|
if field is not None and not field.readonly:
|
|
507
519
|
model_attribute = getattr(obj.__class__, key)
|
|
508
|
-
|
|
509
520
|
if hasattr(model_attribute, "from_input"):
|
|
510
521
|
value = model_attribute.from_input(value)
|
|
511
522
|
elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
@@ -531,6 +542,20 @@ def patch(obj, request) -> type:
|
|
|
531
542
|
value["id"],
|
|
532
543
|
document_type=db.resolve_model(value["class"]),
|
|
533
544
|
)
|
|
545
|
+
elif value and isinstance(
|
|
546
|
+
model_attribute,
|
|
547
|
+
mongoengine.fields.EmbeddedDocumentField,
|
|
548
|
+
):
|
|
549
|
+
embedded_field = model_attribute.document_type()
|
|
550
|
+
value = embedded_field._from_son(value)
|
|
551
|
+
elif value and isinstance(
|
|
552
|
+
model_attribute,
|
|
553
|
+
mongoengine.fields.EmbeddedDocumentListField,
|
|
554
|
+
):
|
|
555
|
+
embedded_field = model_attribute.field.document_type()
|
|
556
|
+
# MongoEngine BaseDocument has a `from_json` method for string and a private `_from_son`
|
|
557
|
+
# but there is no public `from_son` to use
|
|
558
|
+
value = [embedded_field._from_son(embedded_value) for embedded_value in value]
|
|
534
559
|
|
|
535
560
|
info = getattr(model_attribute, "__additional_field_info__", {})
|
|
536
561
|
|
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
|
@@ -9,7 +9,7 @@ from flask_login import current_user
|
|
|
9
9
|
|
|
10
10
|
from udata.api import API, api, fields
|
|
11
11
|
from udata.api_fields import patch
|
|
12
|
-
from udata.core.dataservices.
|
|
12
|
+
from udata.core.dataservices.constants import DATASERVICE_ACCESS_TYPE_RESTRICTED
|
|
13
13
|
from udata.core.dataset.models import Dataset
|
|
14
14
|
from udata.core.followers.api import FollowAPI
|
|
15
15
|
from udata.core.site.models import current_site
|
|
@@ -18,7 +18,6 @@ from udata.i18n import gettext as _
|
|
|
18
18
|
from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
|
|
19
19
|
|
|
20
20
|
from .models import Dataservice
|
|
21
|
-
from .permissions import DataserviceEditPermission
|
|
22
21
|
from .rdf import dataservice_to_rdf
|
|
23
22
|
|
|
24
23
|
ns = api.namespace("dataservices", "Dataservices related operations (beta)")
|
|
@@ -49,7 +48,8 @@ class DataservicesAPI(API):
|
|
|
49
48
|
dataservice = patch(Dataservice(), request)
|
|
50
49
|
if not dataservice.owner and not dataservice.organization:
|
|
51
50
|
dataservice.owner = current_user._get_current_object()
|
|
52
|
-
|
|
51
|
+
if dataservice.access_type != DATASERVICE_ACCESS_TYPE_RESTRICTED:
|
|
52
|
+
dataservice.access_audiences = []
|
|
53
53
|
dataservice.save()
|
|
54
54
|
return dataservice, 201
|
|
55
55
|
|
|
@@ -97,7 +97,7 @@ class DataserviceAPI(API):
|
|
|
97
97
|
@api.doc("get_dataservice")
|
|
98
98
|
@api.marshal_with(Dataservice.__read_fields__)
|
|
99
99
|
def get(self, dataservice):
|
|
100
|
-
if not
|
|
100
|
+
if not dataservice.permissions["edit"].can():
|
|
101
101
|
if dataservice.private:
|
|
102
102
|
api.abort(404)
|
|
103
103
|
elif dataservice.deleted_at:
|
|
@@ -115,10 +115,12 @@ class DataserviceAPI(API):
|
|
|
115
115
|
):
|
|
116
116
|
api.abort(410, "dataservice has been deleted")
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
dataservice.permissions["edit"].test()
|
|
119
119
|
|
|
120
120
|
patch(dataservice, request)
|
|
121
121
|
dataservice.metadata_modified_at = datetime.utcnow()
|
|
122
|
+
if dataservice.access_type != DATASERVICE_ACCESS_TYPE_RESTRICTED:
|
|
123
|
+
dataservice.access_audiences = []
|
|
122
124
|
|
|
123
125
|
dataservice.save()
|
|
124
126
|
return dataservice
|
|
@@ -130,7 +132,7 @@ class DataserviceAPI(API):
|
|
|
130
132
|
if dataservice.deleted_at:
|
|
131
133
|
api.abort(410, "dataservice has been deleted")
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
dataservice.permissions["delete"].test()
|
|
134
136
|
dataservice.deleted_at = datetime.utcnow()
|
|
135
137
|
dataservice.metadata_modified_at = datetime.utcnow()
|
|
136
138
|
dataservice.save()
|
|
@@ -163,7 +165,7 @@ class DataserviceDatasetsAPI(API):
|
|
|
163
165
|
if dataservice.deleted_at:
|
|
164
166
|
api.abort(410, "Dataservice has been deleted")
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
dataservice.permissions["edit"].test()
|
|
167
169
|
|
|
168
170
|
data = request.json
|
|
169
171
|
|
|
@@ -199,7 +201,7 @@ class DataserviceDatasetAPI(API):
|
|
|
199
201
|
if dataservice.deleted_at:
|
|
200
202
|
api.abort(410, "Dataservice has been deleted")
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
dataservice.permissions["edit"].test()
|
|
203
205
|
|
|
204
206
|
if dataset not in dataservice.datasets:
|
|
205
207
|
api.abort(404, "Dataset not found in dataservice")
|
|
@@ -228,8 +230,8 @@ class DataserviceRdfAPI(API):
|
|
|
228
230
|
@api.response(410, "Dataservice has been deleted")
|
|
229
231
|
class DataserviceRdfFormatAPI(API):
|
|
230
232
|
@api.doc("rdf_dataservice_format")
|
|
231
|
-
def get(self, dataservice, format):
|
|
232
|
-
if not
|
|
233
|
+
def get(self, dataservice: Dataservice, format):
|
|
234
|
+
if not dataservice.permissions["edit"].can():
|
|
233
235
|
if dataservice.private:
|
|
234
236
|
api.abort(404)
|
|
235
237
|
elif dataservice.deleted_at:
|
udata/core/dataservices/apiv2.py
CHANGED
|
@@ -2,14 +2,17 @@ from flask import request
|
|
|
2
2
|
|
|
3
3
|
from udata import search
|
|
4
4
|
from udata.api import API, apiv2
|
|
5
|
-
from udata.core.dataservices.models import Dataservice, HarvestMetadata
|
|
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__)
|
|
15
|
+
apiv2.inherit("AccessAudience (read)", AccessAudience.__read_fields__)
|
|
13
16
|
|
|
14
17
|
ns = apiv2.namespace("dataservices", "Dataservice related operations")
|
|
15
18
|
|
|
@@ -9,3 +9,22 @@ DATASERVICE_ACCESS_TYPES = [
|
|
|
9
9
|
DATASERVICE_ACCESS_TYPE_OPEN_WITH_ACCOUNT,
|
|
10
10
|
DATASERVICE_ACCESS_TYPE_RESTRICTED,
|
|
11
11
|
]
|
|
12
|
+
|
|
13
|
+
DATASERVICE_ACCESS_AUDIENCE_ADMINISTRATION = "local_authority_and_administration"
|
|
14
|
+
DATASERVICE_ACCESS_AUDIENCE_COMPANY = "company_and_association"
|
|
15
|
+
DATASERVICE_ACCESS_AUDIENCE_PRIVATE = "private"
|
|
16
|
+
|
|
17
|
+
DATASERVICE_ACCESS_AUDIENCE_TYPES = [
|
|
18
|
+
DATASERVICE_ACCESS_AUDIENCE_ADMINISTRATION,
|
|
19
|
+
DATASERVICE_ACCESS_AUDIENCE_COMPANY,
|
|
20
|
+
DATASERVICE_ACCESS_AUDIENCE_PRIVATE,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
DATASERVICE_ACCESS_AUDIENCE_YES = "yes"
|
|
24
|
+
DATASERVICE_ACCESS_AUDIENCE_NO = "no"
|
|
25
|
+
DATASERVICE_ACCESS_AUDIENCE_UNDER_CONDITIONS = "under_condition"
|
|
26
|
+
DATASERVICE_ACCESS_AUDIENCE_CONDITIONS = [
|
|
27
|
+
DATASERVICE_ACCESS_AUDIENCE_YES,
|
|
28
|
+
DATASERVICE_ACCESS_AUDIENCE_NO,
|
|
29
|
+
DATASERVICE_ACCESS_AUDIENCE_UNDER_CONDITIONS,
|
|
30
|
+
]
|
|
@@ -7,14 +7,22 @@ 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
|
-
from udata.core.dataservices.constants import
|
|
13
|
+
from udata.core.dataservices.constants import (
|
|
14
|
+
DATASERVICE_ACCESS_AUDIENCE_CONDITIONS,
|
|
15
|
+
DATASERVICE_ACCESS_AUDIENCE_TYPES,
|
|
16
|
+
DATASERVICE_ACCESS_TYPES,
|
|
17
|
+
DATASERVICE_FORMATS,
|
|
18
|
+
)
|
|
13
19
|
from udata.core.dataset.models import Dataset
|
|
20
|
+
from udata.core.metrics.helpers import get_stock_metrics
|
|
14
21
|
from udata.core.metrics.models import WithMetrics
|
|
15
22
|
from udata.core.owned import Owned, OwnedQuerySet
|
|
16
23
|
from udata.i18n import lazy_gettext as _
|
|
17
24
|
from udata.models import Discussion, Follow, db
|
|
25
|
+
from udata.mongo.errors import FieldValidationError
|
|
18
26
|
from udata.uris import endpoint_for
|
|
19
27
|
|
|
20
28
|
# "frequency"
|
|
@@ -27,6 +35,15 @@ from udata.uris import endpoint_for
|
|
|
27
35
|
# "temporal_coverage"
|
|
28
36
|
|
|
29
37
|
|
|
38
|
+
dataservice_permissions_fields = api.model(
|
|
39
|
+
"DataservicePermissions",
|
|
40
|
+
{
|
|
41
|
+
"delete": fields.Permission(),
|
|
42
|
+
"edit": fields.Permission(),
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
30
47
|
class DataserviceQuerySet(OwnedQuerySet):
|
|
31
48
|
def visible(self):
|
|
32
49
|
return self(archived_at=None, deleted_at=None, private=False)
|
|
@@ -98,6 +115,21 @@ class HarvestMetadata(db.EmbeddedDocument):
|
|
|
98
115
|
archived_reason = field(db.StringField())
|
|
99
116
|
|
|
100
117
|
|
|
118
|
+
@generate_fields()
|
|
119
|
+
class AccessAudience(db.EmbeddedDocument):
|
|
120
|
+
role = field(db.StringField(choices=DATASERVICE_ACCESS_AUDIENCE_TYPES), filterable={})
|
|
121
|
+
condition = field(db.StringField(choices=DATASERVICE_ACCESS_AUDIENCE_CONDITIONS), filterable={})
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_only_one_condition_per_role(access_audiences, **_kwargs):
|
|
125
|
+
roles = set(e["role"] for e in access_audiences)
|
|
126
|
+
if len(roles) != len(access_audiences):
|
|
127
|
+
raise FieldValidationError(
|
|
128
|
+
_("You can only set one condition for a given access audience role"),
|
|
129
|
+
field="access_audiences",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
101
133
|
@generate_fields(
|
|
102
134
|
searchable=True,
|
|
103
135
|
additional_filters={"organization_badge": "organization.badges"},
|
|
@@ -158,6 +190,11 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
158
190
|
availability_url = field(db.URLField())
|
|
159
191
|
|
|
160
192
|
access_type = field(db.StringField(choices=DATASERVICE_ACCESS_TYPES), filterable={})
|
|
193
|
+
access_audiences = field(
|
|
194
|
+
db.EmbeddedDocumentListField(AccessAudience),
|
|
195
|
+
checks=[check_only_one_condition_per_role],
|
|
196
|
+
)
|
|
197
|
+
|
|
161
198
|
authorization_request_url = field(db.URLField())
|
|
162
199
|
|
|
163
200
|
format = field(db.StringField(choices=DATASERVICE_FORMATS))
|
|
@@ -252,6 +289,7 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
252
289
|
__metrics_keys__ = [
|
|
253
290
|
"discussions",
|
|
254
291
|
"followers",
|
|
292
|
+
"followers_by_months",
|
|
255
293
|
"views",
|
|
256
294
|
]
|
|
257
295
|
|
|
@@ -259,12 +297,27 @@ class Dataservice(Auditable, WithMetrics, Owned, db.Document):
|
|
|
259
297
|
def is_hidden(self):
|
|
260
298
|
return self.private or self.deleted_at or self.archived_at
|
|
261
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
|
+
|
|
262
312
|
def count_discussions(self):
|
|
263
313
|
self.metrics["discussions"] = Discussion.objects(subject=self, closed=None).count()
|
|
264
314
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
265
315
|
|
|
266
316
|
def count_followers(self):
|
|
267
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
|
+
)
|
|
268
321
|
self.save(signal_kwargs={"ignores": ["post_save"]})
|
|
269
322
|
|
|
270
323
|
|
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
|
|
|
@@ -487,8 +503,6 @@ class ResourcesAPI(API):
|
|
|
487
503
|
api.abort(400, "This endpoint only supports remote resources")
|
|
488
504
|
form.populate_obj(resource)
|
|
489
505
|
dataset.add_resource(resource)
|
|
490
|
-
dataset.last_modified_internal = datetime.utcnow()
|
|
491
|
-
dataset.save()
|
|
492
506
|
return resource, 201
|
|
493
507
|
|
|
494
508
|
@api.secure
|
|
@@ -497,7 +511,7 @@ class ResourcesAPI(API):
|
|
|
497
511
|
@api.marshal_list_with(resource_fields)
|
|
498
512
|
def put(self, dataset):
|
|
499
513
|
"""Reorder resources"""
|
|
500
|
-
|
|
514
|
+
dataset.permissions["edit_resources"].test()
|
|
501
515
|
resources = request.json
|
|
502
516
|
if len(dataset.resources) != len(resources):
|
|
503
517
|
api.abort(
|
|
@@ -551,12 +565,10 @@ class UploadNewDatasetResource(UploadMixin, API):
|
|
|
551
565
|
@api.marshal_with(upload_fields, code=201)
|
|
552
566
|
def post(self, dataset):
|
|
553
567
|
"""Upload a file for a new dataset resource"""
|
|
554
|
-
|
|
568
|
+
dataset.permissions["edit_resources"].test()
|
|
555
569
|
infos = self.handle_upload(dataset)
|
|
556
570
|
resource = Resource(**infos)
|
|
557
571
|
dataset.add_resource(resource)
|
|
558
|
-
dataset.last_modified_internal = datetime.utcnow()
|
|
559
|
-
dataset.save()
|
|
560
572
|
return resource, 201
|
|
561
573
|
|
|
562
574
|
|
|
@@ -602,15 +614,13 @@ class UploadDatasetResource(ResourceMixin, UploadMixin, API):
|
|
|
602
614
|
@api.marshal_with(upload_fields)
|
|
603
615
|
def post(self, dataset, rid):
|
|
604
616
|
"""Upload a file related to a given resource on a given dataset"""
|
|
605
|
-
|
|
617
|
+
dataset.permissions["edit_resources"].test()
|
|
606
618
|
resource = self.get_resource_or_404(dataset, rid)
|
|
607
619
|
fs_filename_to_remove = resource.fs_filename
|
|
608
620
|
infos = self.handle_upload(dataset)
|
|
609
621
|
for k, v in infos.items():
|
|
610
622
|
resource[k] = v
|
|
611
623
|
dataset.update_resource(resource)
|
|
612
|
-
dataset.last_modified_internal = datetime.utcnow()
|
|
613
|
-
dataset.save()
|
|
614
624
|
if fs_filename_to_remove is not None:
|
|
615
625
|
storages.resources.delete(fs_filename_to_remove)
|
|
616
626
|
return resource
|
|
@@ -631,7 +641,7 @@ class ReuploadCommunityResource(ResourceMixin, UploadMixin, API):
|
|
|
631
641
|
@api.marshal_with(upload_community_fields)
|
|
632
642
|
def post(self, community):
|
|
633
643
|
"""Update the file related to a given community resource"""
|
|
634
|
-
|
|
644
|
+
community.permissions["edit"].test()
|
|
635
645
|
fs_filename_to_remove = community.fs_filename
|
|
636
646
|
infos = self.handle_upload(community.dataset)
|
|
637
647
|
community.update(**infos)
|
|
@@ -648,7 +658,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
648
658
|
@api.marshal_with(resource_fields)
|
|
649
659
|
def get(self, dataset, rid):
|
|
650
660
|
"""Get a resource given its identifier"""
|
|
651
|
-
if not
|
|
661
|
+
if not dataset.permissions["edit"].can():
|
|
652
662
|
if dataset.private:
|
|
653
663
|
api.abort(404)
|
|
654
664
|
elif dataset.deleted:
|
|
@@ -662,7 +672,7 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
662
672
|
@api.marshal_with(resource_fields)
|
|
663
673
|
def put(self, dataset, rid):
|
|
664
674
|
"""Update a given resource on a given dataset"""
|
|
665
|
-
|
|
675
|
+
dataset.permissions["edit_resources"].test()
|
|
666
676
|
resource = self.get_resource_or_404(dataset, rid)
|
|
667
677
|
form = api.validate(ResourceFormWithoutId, resource)
|
|
668
678
|
|
|
@@ -683,19 +693,15 @@ class ResourceAPI(ResourceMixin, API):
|
|
|
683
693
|
form.populate_obj(resource)
|
|
684
694
|
resource.last_modified_internal = datetime.utcnow()
|
|
685
695
|
dataset.update_resource(resource)
|
|
686
|
-
dataset.last_modified_internal = datetime.utcnow()
|
|
687
|
-
dataset.save()
|
|
688
696
|
return resource
|
|
689
697
|
|
|
690
698
|
@api.secure
|
|
691
699
|
@api.doc("delete_resource")
|
|
692
700
|
def delete(self, dataset, rid):
|
|
693
701
|
"""Delete a given resource on a given dataset"""
|
|
694
|
-
|
|
702
|
+
dataset.permissions["edit_resources"].test()
|
|
695
703
|
resource = self.get_resource_or_404(dataset, rid)
|
|
696
704
|
dataset.remove_resource(resource)
|
|
697
|
-
dataset.last_modified_internal = datetime.utcnow()
|
|
698
|
-
dataset.save()
|
|
699
705
|
return "", 204
|
|
700
706
|
|
|
701
707
|
|
|
@@ -751,7 +757,7 @@ class CommunityResourceAPI(API):
|
|
|
751
757
|
@api.marshal_with(community_resource_fields)
|
|
752
758
|
def put(self, community):
|
|
753
759
|
"""Update a given community resource"""
|
|
754
|
-
|
|
760
|
+
community.permissions["edit"].test()
|
|
755
761
|
form = api.validate(CommunityResourceForm, community)
|
|
756
762
|
if community.filetype == "file":
|
|
757
763
|
form._fields.get("url").data = community.url
|
|
@@ -766,7 +772,7 @@ class CommunityResourceAPI(API):
|
|
|
766
772
|
@api.doc("delete_community_resource")
|
|
767
773
|
def delete(self, community):
|
|
768
774
|
"""Delete a given community resource"""
|
|
769
|
-
|
|
775
|
+
community.permissions["delete"].test()
|
|
770
776
|
# Deletes community resource's file from file storage
|
|
771
777
|
if community.fs_filename is not None:
|
|
772
778
|
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
|
)
|