udata 9.1.4__py2.py3-none-any.whl → 9.1.4.dev30973__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.
- tasks/__init__.py +2 -2
- udata/__init__.py +1 -1
- udata/api/__init__.py +3 -2
- udata/api/commands.py +1 -0
- udata/api/fields.py +1 -22
- udata/api_fields.py +37 -140
- udata/app.py +1 -1
- udata/auth/__init__.py +12 -8
- udata/commands/db.py +3 -3
- udata/commands/dcat.py +1 -1
- udata/commands/fixtures.py +40 -60
- udata/commands/purge.py +1 -2
- udata/commands/tests/test_fixtures.py +11 -44
- udata/core/activity/api.py +1 -14
- udata/core/activity/tasks.py +1 -1
- udata/core/badges/models.py +2 -6
- udata/core/contact_point/api.py +3 -1
- udata/core/dataservices/api.py +1 -37
- udata/core/dataservices/models.py +0 -38
- udata/core/dataservices/tasks.py +1 -1
- udata/core/dataset/events.py +1 -4
- udata/core/dataset/forms.py +2 -0
- udata/core/dataset/models.py +10 -12
- udata/core/dataset/rdf.py +1 -1
- udata/core/discussions/api.py +1 -1
- udata/core/discussions/models.py +2 -2
- udata/core/discussions/tasks.py +1 -1
- udata/core/metrics/models.py +1 -4
- udata/core/organization/api.py +7 -11
- udata/core/organization/api_fields.py +4 -10
- udata/core/organization/apiv2.py +1 -1
- udata/core/organization/csv.py +0 -1
- udata/core/organization/rdf.py +1 -4
- udata/core/owned.py +2 -4
- udata/core/post/api.py +2 -2
- udata/core/reuse/api.py +25 -32
- udata/core/reuse/api_fields.py +101 -2
- udata/core/reuse/apiv2.py +4 -4
- udata/core/reuse/forms.py +45 -0
- udata/core/reuse/models.py +16 -98
- udata/core/site/api.py +29 -3
- udata/core/spatial/commands.py +3 -3
- udata/core/spatial/factories.py +1 -1
- udata/core/spatial/forms.py +1 -1
- udata/core/spatial/models.py +2 -2
- udata/core/spatial/tests/test_models.py +1 -1
- udata/core/spatial/translations.py +1 -3
- udata/core/topic/api.py +2 -2
- udata/core/topic/apiv2.py +2 -1
- udata/core/user/api.py +8 -28
- udata/core/user/metrics.py +1 -1
- udata/cors.py +4 -4
- udata/features/transfer/api.py +2 -1
- udata/harvest/actions.py +1 -1
- udata/harvest/backends/__init__.py +1 -1
- udata/harvest/tasks.py +1 -0
- udata/harvest/tests/factories.py +2 -0
- udata/harvest/tests/test_base_backend.py +1 -0
- udata/harvest/tests/test_dcat_backend.py +17 -16
- udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +1 -1
- udata/migrations/2021-07-05-remove-unused-badges.py +1 -0
- udata/migrations/2023-02-08-rename-internal-dates.py +2 -0
- udata/migrations/2024-06-11-fix-reuse-datasets-references.py +1 -0
- udata/mongo/datetime_fields.py +4 -11
- udata/mongo/document.py +0 -2
- udata/mongo/taglist_field.py +0 -26
- udata/search/commands.py +1 -1
- udata/search/query.py +1 -1
- udata/settings.py +0 -1
- udata/static/admin.js +36 -36
- udata/static/admin.js.map +1 -1
- udata/static/chunks/{12.576e63b7a990f8eab784.js → 12.5b900cac4417e10ef3a0.js} +2 -2
- udata/static/chunks/12.5b900cac4417e10ef3a0.js.map +1 -0
- udata/static/chunks/{28.1ef31a46255dc2bf56d1.js → 28.1759a7f57d526e6db574.js} +2 -2
- udata/static/chunks/28.1759a7f57d526e6db574.js.map +1 -0
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_base_api.py +1 -1
- udata/tests/api/test_contact_points.py +4 -4
- udata/tests/api/test_dataservices_api.py +0 -59
- udata/tests/api/test_datasets_api.py +10 -10
- udata/tests/api/test_organizations_api.py +39 -39
- udata/tests/api/test_reuses_api.py +0 -49
- udata/tests/api/test_tags_api.py +4 -4
- udata/tests/api/test_transfer_api.py +1 -1
- udata/tests/api/test_user_api.py +0 -11
- udata/tests/apiv2/test_datasets.py +4 -4
- udata/tests/dataset/test_dataset_events.py +0 -28
- udata/tests/dataset/test_dataset_model.py +3 -3
- udata/tests/frontend/__init__.py +2 -0
- udata/tests/frontend/test_auth.py +1 -0
- udata/tests/organization/test_csv_adapter.py +2 -0
- udata/tests/organization/test_notifications.py +3 -3
- udata/tests/organization/test_organization_rdf.py +6 -31
- udata/tests/reuse/test_reuse_model.py +1 -0
- udata/tests/site/test_site_rdf.py +3 -1
- udata/tests/test_cors.py +3 -0
- udata/tests/test_owned.py +4 -4
- udata/tests/test_routing.py +1 -1
- udata/tests/test_tags.py +1 -1
- udata/tests/test_transfer.py +2 -1
- udata/tests/workers/test_jobs_commands.py +1 -1
- udata/utils.py +0 -12
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/METADATA +4 -16
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/RECORD +109 -109
- udata/static/chunks/12.576e63b7a990f8eab784.js.map +0 -1
- udata/static/chunks/28.1ef31a46255dc2bf56d1.js.map +0 -1
- udata/tests/api/test_activities_api.py +0 -69
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/LICENSE +0 -0
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/WHEEL +0 -0
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/entry_points.txt +0 -0
- {udata-9.1.4.dist-info → udata-9.1.4.dev30973.dist-info}/top_level.txt +0 -0
tasks/__init__.py
CHANGED
|
@@ -194,10 +194,10 @@ def qa(ctx):
|
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
@task
|
|
197
|
-
def serve(ctx, host="localhost"
|
|
197
|
+
def serve(ctx, host="localhost"):
|
|
198
198
|
"""Run a development server"""
|
|
199
199
|
with ctx.cd(ROOT):
|
|
200
|
-
ctx.run(
|
|
200
|
+
ctx.run("python manage.py serve -d -r -h %s" % host)
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
@task
|
udata/__init__.py
CHANGED
udata/api/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import itertools
|
|
2
3
|
import logging
|
|
3
4
|
import urllib.parse
|
|
4
5
|
from functools import wraps
|
|
@@ -17,7 +18,7 @@ from flask import (
|
|
|
17
18
|
from flask_restx import Api, Resource
|
|
18
19
|
from flask_storage import UnauthorizedFileType
|
|
19
20
|
|
|
20
|
-
from udata import entrypoints, tracking
|
|
21
|
+
from udata import cors, entrypoints, tracking
|
|
21
22
|
from udata.app import csrf
|
|
22
23
|
from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
|
|
23
24
|
from udata.i18n import get_locale
|
|
@@ -324,7 +325,7 @@ def init_app(app):
|
|
|
324
325
|
import udata.harvest.api # noqa
|
|
325
326
|
|
|
326
327
|
for module in entrypoints.get_enabled("udata.apis", app).values():
|
|
327
|
-
module if inspect.ismodule(module) else import_module(module)
|
|
328
|
+
api_module = module if inspect.ismodule(module) else import_module(module)
|
|
328
329
|
|
|
329
330
|
# api.init_app(app)
|
|
330
331
|
app.register_blueprint(apiv1_blueprint)
|
udata/api/commands.py
CHANGED
udata/api/fields.py
CHANGED
|
@@ -4,28 +4,7 @@ import logging
|
|
|
4
4
|
import pytz
|
|
5
5
|
from dateutil.parser import parse
|
|
6
6
|
from flask import request, url_for
|
|
7
|
-
|
|
8
|
-
# Explicitly import all of flask_restx fields so they're available throughout the codebase as api.fields
|
|
9
|
-
from flask_restx.fields import Arbitrary as Arbitrary
|
|
10
|
-
from flask_restx.fields import Boolean as Boolean
|
|
11
|
-
from flask_restx.fields import ClassName as ClassName
|
|
12
|
-
from flask_restx.fields import Date as Date
|
|
13
|
-
from flask_restx.fields import DateTime as DateTime
|
|
14
|
-
from flask_restx.fields import Fixed as Fixed
|
|
15
|
-
from flask_restx.fields import Float as Float
|
|
16
|
-
from flask_restx.fields import FormattedString as FormattedString
|
|
17
|
-
from flask_restx.fields import Integer as Integer
|
|
18
|
-
from flask_restx.fields import List as List
|
|
19
|
-
from flask_restx.fields import MarshallingError as MarshallingError
|
|
20
|
-
from flask_restx.fields import MinMaxMixin as MinMaxMixin
|
|
21
|
-
from flask_restx.fields import Nested as Nested
|
|
22
|
-
from flask_restx.fields import NumberMixin as NumberMixin
|
|
23
|
-
from flask_restx.fields import Polymorph as Polymorph
|
|
24
|
-
from flask_restx.fields import Raw as Raw
|
|
25
|
-
from flask_restx.fields import String as String
|
|
26
|
-
from flask_restx.fields import StringMixin as StringMixin
|
|
27
|
-
from flask_restx.fields import Url as Url
|
|
28
|
-
from flask_restx.fields import Wildcard as Wildcard
|
|
7
|
+
from flask_restx.fields import * # noqa
|
|
29
8
|
|
|
30
9
|
from udata.uris import endpoint_for
|
|
31
10
|
from udata.utils import multi_to_dict
|
udata/api_fields.py
CHANGED
|
@@ -2,10 +2,10 @@ import flask_restx.fields as restx_fields
|
|
|
2
2
|
import mongoengine
|
|
3
3
|
import mongoengine.fields as mongo_fields
|
|
4
4
|
from bson import ObjectId
|
|
5
|
-
from flask_storage.mongo import ImageField as FlaskStorageImageField
|
|
6
5
|
|
|
7
6
|
import udata.api.fields as custom_restx_fields
|
|
8
|
-
from udata.api import api
|
|
7
|
+
from udata.api import api
|
|
8
|
+
from udata.mongo.engine import db
|
|
9
9
|
from udata.mongo.errors import FieldValidationError
|
|
10
10
|
|
|
11
11
|
lazy_reference = api.model(
|
|
@@ -17,7 +17,7 @@ lazy_reference = api.model(
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def convert_db_to_field(key, field, info):
|
|
20
|
+
def convert_db_to_field(key, field, info={}):
|
|
21
21
|
"""
|
|
22
22
|
This function maps a Mongo field to a Flask RestX field.
|
|
23
23
|
Most of the types are a simple 1-to-1 mapping except lists and references that requires
|
|
@@ -28,6 +28,8 @@ def convert_db_to_field(key, field, info):
|
|
|
28
28
|
params. Since merging the params involve a litte bit of work (merging default params with read/write params and then with
|
|
29
29
|
user-supplied overrides, setting the readonly flag…), it's easier to have do this one time at the end of the function.
|
|
30
30
|
"""
|
|
31
|
+
info = {**getattr(field, "__additional_field_info__", {}), **info}
|
|
32
|
+
|
|
31
33
|
params = {}
|
|
32
34
|
params["required"] = field.required
|
|
33
35
|
|
|
@@ -43,9 +45,7 @@ def convert_db_to_field(key, field, info):
|
|
|
43
45
|
# is always good enough.
|
|
44
46
|
return info.get("convert_to"), info.get("convert_to")
|
|
45
47
|
elif isinstance(field, mongo_fields.StringField):
|
|
46
|
-
constructor =
|
|
47
|
-
custom_restx_fields.Markdown if info.get("markdown", False) else restx_fields.String
|
|
48
|
-
)
|
|
48
|
+
constructor = restx_fields.String
|
|
49
49
|
params["min_length"] = field.min_length
|
|
50
50
|
params["max_length"] = field.max_length
|
|
51
51
|
params["enum"] = field.choices
|
|
@@ -61,42 +61,18 @@ def convert_db_to_field(key, field, info):
|
|
|
61
61
|
constructor = custom_restx_fields.ISODateTime
|
|
62
62
|
elif isinstance(field, mongo_fields.DictField):
|
|
63
63
|
constructor = restx_fields.Raw
|
|
64
|
-
elif isinstance(field, mongo_fields.ImageField) or isinstance(field, FlaskStorageImageField):
|
|
65
|
-
size = info.get("size", None)
|
|
66
|
-
if size:
|
|
67
|
-
params["description"] = f"URL of the cropped and squared image ({size}x{size})"
|
|
68
|
-
else:
|
|
69
|
-
params["description"] = "URL of the image"
|
|
70
|
-
|
|
71
|
-
if info.get("is_thumbnail", False):
|
|
72
|
-
constructor_read = custom_restx_fields.ImageField
|
|
73
|
-
write_params["read_only"] = True
|
|
74
|
-
else:
|
|
75
|
-
constructor = custom_restx_fields.ImageField
|
|
76
|
-
|
|
77
64
|
elif isinstance(field, mongo_fields.ListField):
|
|
78
65
|
# For lists, we convert the inner value from Mongo to RestX then we create
|
|
79
66
|
# the `List` RestX type with this converted inner value.
|
|
80
|
-
# There is three level of information, from most important to least
|
|
81
|
-
# 1. `inner_field_info` inside `__additional_field_info__` on the parent
|
|
82
|
-
# 2. `__additional_field_info__` of the inner field
|
|
83
|
-
# 3. `__additional_field_info__` of the parent
|
|
84
|
-
inner_info = getattr(field.field, "__additional_field_info__", {})
|
|
85
67
|
field_read, field_write = convert_db_to_field(
|
|
86
|
-
f"{key}.inner", field.field,
|
|
68
|
+
f"{key}.inner", field.field, info.get("inner_field_info", {})
|
|
87
69
|
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return restx_fields.List(field_read, **kwargs)
|
|
91
|
-
|
|
92
|
-
def constructor_write(**kwargs):
|
|
93
|
-
return restx_fields.List(field_write, **kwargs)
|
|
70
|
+
constructor_read = lambda **kwargs: restx_fields.List(field_read, **kwargs)
|
|
71
|
+
constructor_write = lambda **kwargs: restx_fields.List(field_write, **kwargs)
|
|
94
72
|
elif isinstance(
|
|
95
73
|
field, (mongo_fields.GenericReferenceField, mongoengine.fields.GenericLazyReferenceField)
|
|
96
74
|
):
|
|
97
|
-
|
|
98
|
-
def constructor(**kwargs):
|
|
99
|
-
return restx_fields.Nested(lazy_reference, **kwargs)
|
|
75
|
+
constructor = lambda **kwargs: restx_fields.Nested(lazy_reference, **kwargs)
|
|
100
76
|
elif isinstance(field, mongo_fields.ReferenceField):
|
|
101
77
|
# For reference we accept while writing a String representing the ID of the referenced model.
|
|
102
78
|
# For reading, if the user supplied a `nested_fields` (RestX model), we use it to convert
|
|
@@ -107,38 +83,34 @@ def convert_db_to_field(key, field, info):
|
|
|
107
83
|
# If there is no `nested_fields` convert the object to the string representation.
|
|
108
84
|
constructor_read = restx_fields.String
|
|
109
85
|
else:
|
|
110
|
-
|
|
111
|
-
def constructor_read(**kwargs):
|
|
112
|
-
return restx_fields.Nested(nested_fields, **kwargs)
|
|
86
|
+
constructor_read = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
113
87
|
|
|
114
88
|
write_params["description"] = "ID of the reference"
|
|
115
89
|
constructor_write = restx_fields.String
|
|
116
90
|
elif isinstance(field, mongo_fields.EmbeddedDocumentField):
|
|
117
91
|
nested_fields = info.get("nested_fields")
|
|
118
92
|
if nested_fields is not None:
|
|
119
|
-
|
|
120
|
-
def constructor(**kwargs):
|
|
121
|
-
return restx_fields.Nested(nested_fields, **kwargs)
|
|
93
|
+
constructor = lambda **kwargs: restx_fields.Nested(nested_fields, **kwargs)
|
|
122
94
|
elif hasattr(field.document_type_obj, "__read_fields__"):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
95
|
+
constructor_read = lambda **kwargs: restx_fields.Nested(
|
|
96
|
+
field.document_type_obj.__read_fields__, **kwargs
|
|
97
|
+
)
|
|
98
|
+
constructor_write = lambda **kwargs: restx_fields.Nested(
|
|
99
|
+
field.document_type_obj.__write_fields__, **kwargs
|
|
100
|
+
)
|
|
129
101
|
else:
|
|
130
102
|
raise ValueError(
|
|
131
103
|
f"EmbeddedDocumentField `{key}` requires a `nested_fields` param to serialize/deserialize or a `@generate_fields()` definition."
|
|
132
104
|
)
|
|
133
105
|
|
|
134
106
|
else:
|
|
135
|
-
raise ValueError(f"Unsupported MongoEngine field type {field.__class__}")
|
|
107
|
+
raise ValueError(f"Unsupported MongoEngine field type {field.__class__.__name__}")
|
|
136
108
|
|
|
137
109
|
read_params = {**params, **read_params, **info}
|
|
138
110
|
write_params = {**params, **write_params, **info}
|
|
139
111
|
|
|
140
112
|
read = constructor_read(**read_params) if constructor_read else constructor(**read_params)
|
|
141
|
-
if write_params.get("readonly", False)
|
|
113
|
+
if write_params.get("readonly", False):
|
|
142
114
|
write = None
|
|
143
115
|
else:
|
|
144
116
|
write = (
|
|
@@ -147,26 +119,6 @@ def convert_db_to_field(key, field, info):
|
|
|
147
119
|
return read, write
|
|
148
120
|
|
|
149
121
|
|
|
150
|
-
def get_fields(cls):
|
|
151
|
-
"""
|
|
152
|
-
Returns all the exposed fields of the class (fields decorated with `field()`)
|
|
153
|
-
It also expends image fields to add thumbnail fields.
|
|
154
|
-
"""
|
|
155
|
-
for key, field in cls._fields.items():
|
|
156
|
-
info: dict | None = getattr(field, "__additional_field_info__", None)
|
|
157
|
-
if info is None:
|
|
158
|
-
continue
|
|
159
|
-
|
|
160
|
-
yield key, field, info
|
|
161
|
-
|
|
162
|
-
if isinstance(field, mongo_fields.ImageField) or isinstance(field, FlaskStorageImageField):
|
|
163
|
-
yield (
|
|
164
|
-
f"{key}_thumbnail",
|
|
165
|
-
field,
|
|
166
|
-
{**info, **info.get("thumbnail_info", {}), "is_thumbnail": True, "attribute": key},
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
122
|
def generate_fields(**kwargs):
|
|
171
123
|
"""
|
|
172
124
|
This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
|
|
@@ -176,18 +128,18 @@ def generate_fields(**kwargs):
|
|
|
176
128
|
def wrapper(cls):
|
|
177
129
|
read_fields = {}
|
|
178
130
|
write_fields = {}
|
|
179
|
-
|
|
180
|
-
sortables = kwargs.get("additionalSorts", [])
|
|
131
|
+
sortables = []
|
|
181
132
|
filterables = []
|
|
182
133
|
|
|
183
|
-
read_fields["id"] = restx_fields.String(required=True
|
|
134
|
+
read_fields["id"] = restx_fields.String(required=True)
|
|
184
135
|
|
|
185
|
-
for key, field
|
|
186
|
-
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
136
|
+
for key, field in cls._fields.items():
|
|
137
|
+
info = getattr(field, "__additional_field_info__", None)
|
|
138
|
+
if info is None:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if info.get("sortable", False):
|
|
142
|
+
sortables.append(key)
|
|
191
143
|
|
|
192
144
|
filterable = info.get("filterable", None)
|
|
193
145
|
if filterable is not None:
|
|
@@ -204,26 +156,18 @@ def generate_fields(**kwargs):
|
|
|
204
156
|
):
|
|
205
157
|
filterable["constraints"].append("objectid")
|
|
206
158
|
|
|
207
|
-
if "type" not in filterable:
|
|
208
|
-
filterable["type"] = str
|
|
209
|
-
if isinstance(field, mongo_fields.BooleanField):
|
|
210
|
-
filterable["type"] = bool
|
|
211
|
-
|
|
212
159
|
# We may add more information later here:
|
|
213
160
|
# - type of mongo query to execute (right now only simple =)
|
|
214
161
|
|
|
215
162
|
filterables.append(filterable)
|
|
216
163
|
|
|
217
|
-
read, write = convert_db_to_field(key, field
|
|
164
|
+
read, write = convert_db_to_field(key, field)
|
|
218
165
|
|
|
219
166
|
if read:
|
|
220
167
|
read_fields[key] = read
|
|
221
168
|
if write:
|
|
222
169
|
write_fields[key] = write
|
|
223
170
|
|
|
224
|
-
if read and info.get("show_as_ref", False):
|
|
225
|
-
ref_fields[key] = read
|
|
226
|
-
|
|
227
171
|
# The goal of this loop is to fetch all functions (getters) of the class
|
|
228
172
|
# If a function has an `__additional_field_info__` attribute it means
|
|
229
173
|
# it has been decorated with `@function_field()` and should be included
|
|
@@ -255,12 +199,9 @@ def generate_fields(**kwargs):
|
|
|
255
199
|
read_fields[method_name] = restx_fields.String(
|
|
256
200
|
attribute=make_lambda(method), **{"readonly": True, **info}
|
|
257
201
|
)
|
|
258
|
-
if info.get("show_as_ref", False):
|
|
259
|
-
ref_fields[key] = read_fields[method_name]
|
|
260
202
|
|
|
261
203
|
cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
|
|
262
204
|
cls.__write_fields__ = api.model(f"{cls.__name__} (write)", write_fields, **kwargs)
|
|
263
|
-
cls.__ref_fields__ = api.inherit(f"{cls.__name__}Reference", base_reference, ref_fields)
|
|
264
205
|
|
|
265
206
|
mask = kwargs.pop("mask", None)
|
|
266
207
|
if mask is not None:
|
|
@@ -285,9 +226,7 @@ def generate_fields(**kwargs):
|
|
|
285
226
|
)
|
|
286
227
|
|
|
287
228
|
if sortables:
|
|
288
|
-
choices = [
|
|
289
|
-
"-" + sortable["key"] for sortable in sortables
|
|
290
|
-
]
|
|
229
|
+
choices = sortables + ["-" + k for k in sortables]
|
|
291
230
|
parser.add_argument(
|
|
292
231
|
"sort",
|
|
293
232
|
type=str,
|
|
@@ -296,12 +235,8 @@ def generate_fields(**kwargs):
|
|
|
296
235
|
help="The field (and direction) on which sorting apply",
|
|
297
236
|
)
|
|
298
237
|
|
|
299
|
-
searchable = kwargs.pop("searchable", False)
|
|
300
|
-
if searchable:
|
|
301
|
-
parser.add_argument("q", type=str, location="args")
|
|
302
|
-
|
|
303
238
|
for filterable in filterables:
|
|
304
|
-
parser.add_argument(filterable["key"], type=
|
|
239
|
+
parser.add_argument(filterable["key"], type=str, location="args")
|
|
305
240
|
|
|
306
241
|
cls.__index_parser__ = parser
|
|
307
242
|
|
|
@@ -309,23 +244,7 @@ def generate_fields(**kwargs):
|
|
|
309
244
|
args = cls.__index_parser__.parse_args()
|
|
310
245
|
|
|
311
246
|
if sortables and args["sort"]:
|
|
312
|
-
|
|
313
|
-
sort_key = args["sort"][1:] if negate else args["sort"]
|
|
314
|
-
|
|
315
|
-
sort_by = next(
|
|
316
|
-
(sortable["value"] for sortable in sortables if sortable["key"] == sort_key),
|
|
317
|
-
None,
|
|
318
|
-
)
|
|
319
|
-
|
|
320
|
-
if sort_by:
|
|
321
|
-
if negate:
|
|
322
|
-
sort_by = "-" + sort_by
|
|
323
|
-
|
|
324
|
-
base_query = base_query.order_by(sort_by)
|
|
325
|
-
|
|
326
|
-
if searchable and args.get("q"):
|
|
327
|
-
phrase_query = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
328
|
-
base_query = base_query.search_text(phrase_query)
|
|
247
|
+
base_query = base_query.order_by(args["sort"])
|
|
329
248
|
|
|
330
249
|
for filterable in filterables:
|
|
331
250
|
if args.get(filterable["key"]):
|
|
@@ -374,16 +293,11 @@ def patch(obj, request):
|
|
|
374
293
|
Patch the object with the data from the request.
|
|
375
294
|
Only fields decorated with the `field()` decorator will be read (and not readonly).
|
|
376
295
|
"""
|
|
377
|
-
from udata.mongo.engine import db
|
|
378
|
-
|
|
379
296
|
for key, value in request.json.items():
|
|
380
297
|
field = obj.__write_fields__.get(key)
|
|
381
298
|
if field is not None and not field.readonly:
|
|
382
299
|
model_attribute = getattr(obj.__class__, key)
|
|
383
|
-
|
|
384
|
-
if hasattr(model_attribute, "from_input"):
|
|
385
|
-
value = model_attribute.from_input(value)
|
|
386
|
-
elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
300
|
+
if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
387
301
|
model_attribute.field, mongoengine.fields.ReferenceField
|
|
388
302
|
):
|
|
389
303
|
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
@@ -409,7 +323,7 @@ def patch(obj, request):
|
|
|
409
323
|
# `check` field attribute allows to do validation from the request before setting
|
|
410
324
|
# the attribute
|
|
411
325
|
check = info.get("check", None)
|
|
412
|
-
if check is not None
|
|
326
|
+
if check is not None:
|
|
413
327
|
check(**{key: value}) # TODO add other model attributes in function parameters
|
|
414
328
|
|
|
415
329
|
setattr(obj, key, value)
|
|
@@ -417,21 +331,10 @@ def patch(obj, request):
|
|
|
417
331
|
return obj
|
|
418
332
|
|
|
419
333
|
|
|
420
|
-
def patch_and_save(obj, request):
|
|
421
|
-
obj = patch(obj, request)
|
|
422
|
-
|
|
423
|
-
try:
|
|
424
|
-
obj.save()
|
|
425
|
-
except mongoengine.errors.ValidationError as e:
|
|
426
|
-
api.abort(400, e.message)
|
|
427
|
-
|
|
428
|
-
return obj
|
|
429
|
-
|
|
430
|
-
|
|
431
334
|
def wrap_primary_key(
|
|
432
335
|
field_name: str,
|
|
433
336
|
foreign_field: mongoengine.fields.ReferenceField | mongoengine.fields.GenericReferenceField,
|
|
434
|
-
value: str
|
|
337
|
+
value: str,
|
|
435
338
|
document_type=None,
|
|
436
339
|
):
|
|
437
340
|
"""
|
|
@@ -440,12 +343,6 @@ def wrap_primary_key(
|
|
|
440
343
|
|
|
441
344
|
TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
|
|
442
345
|
"""
|
|
443
|
-
if value is None:
|
|
444
|
-
return value
|
|
445
|
-
|
|
446
|
-
if isinstance(value, dict) and "id" in value:
|
|
447
|
-
return wrap_primary_key(field_name, foreign_field, value["id"], document_type)
|
|
448
|
-
|
|
449
346
|
document_type = document_type or foreign_field.document_type().__class__
|
|
450
347
|
id_field_name = document_type._meta["id_field"]
|
|
451
348
|
|
|
@@ -468,7 +365,7 @@ def wrap_primary_key(
|
|
|
468
365
|
return foreign_document
|
|
469
366
|
|
|
470
367
|
if isinstance(id_field, mongoengine.fields.ObjectIdField):
|
|
471
|
-
return
|
|
368
|
+
return ObjectId(value)
|
|
472
369
|
elif isinstance(id_field, mongoengine.fields.StringField):
|
|
473
370
|
# Right now I didn't find a simpler way to make mongoengine happy.
|
|
474
371
|
# For references, it expects `ObjectId`, `DBRef`, `LazyReference` or `document` but since
|
udata/app.py
CHANGED
udata/auth/__init__.py
CHANGED
|
@@ -2,14 +2,18 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
from flask import current_app, render_template
|
|
4
4
|
from flask_principal import Permission as BasePermission
|
|
5
|
-
from flask_principal import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from flask_security import
|
|
12
|
-
|
|
5
|
+
from flask_principal import (
|
|
6
|
+
PermissionDenied, # noqa: facade pattern
|
|
7
|
+
RoleNeed,
|
|
8
|
+
UserNeed, # noqa: facade pattern
|
|
9
|
+
identity_loaded, # noqa: facade pattern
|
|
10
|
+
)
|
|
11
|
+
from flask_security import ( # noqa
|
|
12
|
+
Security, # noqa
|
|
13
|
+
current_user,
|
|
14
|
+
login_required,
|
|
15
|
+
login_user,
|
|
16
|
+
)
|
|
13
17
|
from werkzeug.utils import import_string
|
|
14
18
|
|
|
15
19
|
log = logging.getLogger(__name__)
|
udata/commands/db.py
CHANGED
|
@@ -159,10 +159,10 @@ def check_references(models_to_check):
|
|
|
159
159
|
references = []
|
|
160
160
|
for model in set(_models):
|
|
161
161
|
if model.__name__ == "Activity":
|
|
162
|
-
print("Skipping Activity model, scheduled for deprecation")
|
|
162
|
+
print(f"Skipping Activity model, scheduled for deprecation")
|
|
163
163
|
continue
|
|
164
164
|
if model.__name__ == "GeoLevel":
|
|
165
|
-
print("Skipping GeoLevel model, scheduled for deprecation")
|
|
165
|
+
print(f"Skipping GeoLevel model, scheduled for deprecation")
|
|
166
166
|
continue
|
|
167
167
|
|
|
168
168
|
if models_to_check and model.__name__ not in models_to_check:
|
|
@@ -367,7 +367,7 @@ def check_references(models_to_check):
|
|
|
367
367
|
)
|
|
368
368
|
else:
|
|
369
369
|
print_and_save(f'Unknown ref type {reference["type"]}')
|
|
370
|
-
except mongoengine.errors.FieldDoesNotExist:
|
|
370
|
+
except mongoengine.errors.FieldDoesNotExist as e:
|
|
371
371
|
print_and_save(
|
|
372
372
|
f"[ERROR for {model.__name__} {obj.id}] {traceback.format_exc()}"
|
|
373
373
|
)
|
udata/commands/dcat.py
CHANGED
|
@@ -33,7 +33,7 @@ def parse_url(url, csw, iso, quiet=False, rid=""):
|
|
|
33
33
|
"""Parse the datasets in a DCAT format located at URL (debug)"""
|
|
34
34
|
if quiet:
|
|
35
35
|
verbose_loggers = ["rdflib", "udata.core.dataset"]
|
|
36
|
-
[logging.getLogger(
|
|
36
|
+
[logging.getLogger(l).setLevel(logging.ERROR) for l in verbose_loggers]
|
|
37
37
|
|
|
38
38
|
class MockSource:
|
|
39
39
|
url = ""
|
udata/commands/fixtures.py
CHANGED
|
@@ -16,6 +16,7 @@ from udata.commands import cli
|
|
|
16
16
|
from udata.core.contact_point.factories import ContactPointFactory
|
|
17
17
|
from udata.core.contact_point.models import ContactPoint
|
|
18
18
|
from udata.core.dataservices.factories import DataserviceFactory
|
|
19
|
+
from udata.core.dataservices.models import Dataservice
|
|
19
20
|
from udata.core.dataset.factories import (
|
|
20
21
|
CommunityResourceFactory,
|
|
21
22
|
DatasetFactory,
|
|
@@ -26,7 +27,6 @@ from udata.core.organization.factories import OrganizationFactory
|
|
|
26
27
|
from udata.core.organization.models import Member, Organization
|
|
27
28
|
from udata.core.reuse.factories import ReuseFactory
|
|
28
29
|
from udata.core.user.factories import UserFactory
|
|
29
|
-
from udata.core.user.models import User
|
|
30
30
|
|
|
31
31
|
log = logging.getLogger(__name__)
|
|
32
32
|
|
|
@@ -56,21 +56,22 @@ UNWANTED_KEYS: dict[str, list[str]] = {
|
|
|
56
56
|
"quality",
|
|
57
57
|
],
|
|
58
58
|
"resource": ["latest", "preview_url", "last_modified"],
|
|
59
|
-
"organization": ["
|
|
60
|
-
"reuse": ["datasets", "image_thumbnail", "page", "uri", "owner"],
|
|
59
|
+
"organization": ["members", "page", "uri", "logo_thumbnail"],
|
|
60
|
+
"reuse": ["datasets", "image_thumbnail", "page", "uri", "organization", "owner"],
|
|
61
61
|
"community": [
|
|
62
62
|
"dataset",
|
|
63
|
+
"organization",
|
|
63
64
|
"owner",
|
|
64
65
|
"latest",
|
|
65
66
|
"last_modified",
|
|
66
67
|
"preview_url",
|
|
67
68
|
],
|
|
68
|
-
"discussion": ["subject", "url", "class"],
|
|
69
|
-
"
|
|
70
|
-
"posted_by": ["uri", "page", "class", "avatar_thumbnail", "email"],
|
|
69
|
+
"discussion": ["subject", "user", "url", "class"],
|
|
70
|
+
"message": ["posted_by"],
|
|
71
71
|
"dataservice": [
|
|
72
72
|
"datasets",
|
|
73
73
|
"license",
|
|
74
|
+
"organization",
|
|
74
75
|
"owner",
|
|
75
76
|
"self_api_url",
|
|
76
77
|
"self_web_url",
|
|
@@ -80,8 +81,6 @@ UNWANTED_KEYS: dict[str, list[str]] = {
|
|
|
80
81
|
|
|
81
82
|
def remove_unwanted_keys(obj: dict, filter_type: str) -> dict:
|
|
82
83
|
"""Remove UNWANTED_KEYS from a dict."""
|
|
83
|
-
if filter_type not in UNWANTED_KEYS:
|
|
84
|
-
return obj
|
|
85
84
|
for unwanted_key in UNWANTED_KEYS[filter_type]:
|
|
86
85
|
if unwanted_key in obj:
|
|
87
86
|
del obj[unwanted_key]
|
|
@@ -152,29 +151,6 @@ def generate_fixtures_file(data_source: str, results_filename: str) -> None:
|
|
|
152
151
|
print(f"Fixtures saved to file {results_filename}")
|
|
153
152
|
|
|
154
153
|
|
|
155
|
-
def get_or_create(data, key, model, factory):
|
|
156
|
-
"""Try getting the object. If it doesn't exist yet, create it with the provided factory."""
|
|
157
|
-
if key not in data or data[key] is None:
|
|
158
|
-
return
|
|
159
|
-
data[key] = remove_unwanted_keys(data[key], key)
|
|
160
|
-
obj = model.objects(id=data[key]["id"]).first()
|
|
161
|
-
if not obj:
|
|
162
|
-
obj = factory(**data[key])
|
|
163
|
-
return obj
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def get_or_create_organization(data):
|
|
167
|
-
return get_or_create(data, "organization", Organization, OrganizationFactory)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def get_or_create_owner(data):
|
|
171
|
-
return get_or_create(data, "owner", User, UserFactory)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def get_or_create_user(data):
|
|
175
|
-
return get_or_create(data, "user", User, UserFactory)
|
|
176
|
-
|
|
177
|
-
|
|
178
154
|
@cli.command()
|
|
179
155
|
@click.argument("source", default=DEFAULT_FIXTURE_FILE)
|
|
180
156
|
def import_fixtures(source):
|
|
@@ -190,45 +166,49 @@ def import_fixtures(source):
|
|
|
190
166
|
user = UserFactory()
|
|
191
167
|
dataset = fixture["dataset"]
|
|
192
168
|
dataset = remove_unwanted_keys(dataset, "dataset")
|
|
193
|
-
if fixture["organization"]:
|
|
194
|
-
organization = fixture["organization"]
|
|
195
|
-
organization["members"] = [
|
|
196
|
-
Member(user=get_or_create_user(member), role=member["role"])
|
|
197
|
-
for member in organization["members"]
|
|
198
|
-
]
|
|
199
|
-
fixture["organization"] = organization
|
|
200
|
-
org = get_or_create_organization(fixture)
|
|
201
|
-
dataset = DatasetFactory(**dataset, organization=org)
|
|
202
|
-
else:
|
|
169
|
+
if not fixture["organization"]:
|
|
203
170
|
dataset = DatasetFactory(**dataset, owner=user)
|
|
171
|
+
else:
|
|
172
|
+
org = Organization.objects(id=fixture["organization"]["id"]).first()
|
|
173
|
+
if not org:
|
|
174
|
+
organization = fixture["organization"]
|
|
175
|
+
organization = remove_unwanted_keys(organization, "organization")
|
|
176
|
+
org = OrganizationFactory(**organization, members=[Member(user=user)])
|
|
177
|
+
dataset = DatasetFactory(**dataset, organization=org)
|
|
204
178
|
for resource in fixture["resources"]:
|
|
205
179
|
resource = remove_unwanted_keys(resource, "resource")
|
|
206
180
|
res = ResourceFactory(**resource)
|
|
207
181
|
dataset.add_resource(res)
|
|
208
182
|
for reuse in fixture["reuses"]:
|
|
209
183
|
reuse = remove_unwanted_keys(reuse, "reuse")
|
|
210
|
-
reuse[
|
|
211
|
-
reuse["organization"] = get_or_create_organization(reuse)
|
|
212
|
-
ReuseFactory(**reuse, datasets=[dataset])
|
|
184
|
+
ReuseFactory(**reuse, datasets=[dataset], owner=user)
|
|
213
185
|
for community in fixture["community_resources"]:
|
|
214
186
|
community = remove_unwanted_keys(community, "community")
|
|
215
|
-
community
|
|
216
|
-
community["organization"] = get_or_create_organization(community)
|
|
217
|
-
CommunityResourceFactory(**community, dataset=dataset)
|
|
187
|
+
CommunityResourceFactory(**community, dataset=dataset, owner=user)
|
|
218
188
|
for discussion in fixture["discussions"]:
|
|
219
189
|
discussion = remove_unwanted_keys(discussion, "discussion")
|
|
220
|
-
|
|
221
|
-
for message in
|
|
222
|
-
message
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
190
|
+
messages = discussion.pop("discussion")
|
|
191
|
+
for message in messages:
|
|
192
|
+
message = remove_unwanted_keys(message, "message")
|
|
193
|
+
DiscussionFactory(
|
|
194
|
+
**discussion,
|
|
195
|
+
subject=dataset,
|
|
196
|
+
user=user,
|
|
197
|
+
discussion=[
|
|
198
|
+
MessageDiscussionFactory(**message, posted_by=user) for message in messages
|
|
199
|
+
],
|
|
200
|
+
)
|
|
228
201
|
for dataservice in fixture["dataservices"]:
|
|
229
202
|
dataservice = remove_unwanted_keys(dataservice, "dataservice")
|
|
230
|
-
dataservice["contact_point"]
|
|
231
|
-
dataservice,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
203
|
+
if not dataservice["contact_point"]:
|
|
204
|
+
DataserviceFactory(**dataservice, datasets=[dataset])
|
|
205
|
+
else:
|
|
206
|
+
contact_point = ContactPoint.objects(
|
|
207
|
+
id=dataservice["contact_point"]["id"]
|
|
208
|
+
).first()
|
|
209
|
+
if not contact_point:
|
|
210
|
+
contact_point = ContactPointFactory(**dataservice["contact_point"])
|
|
211
|
+
dataservice.pop("contact_point")
|
|
212
|
+
DataserviceFactory(
|
|
213
|
+
**dataservice, datasets=[dataset], contact_point=contact_point
|
|
214
|
+
)
|