udata 9.1.4.dev31123__py2.py3-none-any.whl → 9.1.4.dev31168__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api_fields.py +119 -26
- udata/core/activity/api.py +14 -1
- udata/core/badges/models.py +7 -1
- udata/core/metrics/models.py +4 -1
- udata/core/organization/api.py +1 -2
- udata/core/owned.py +4 -2
- udata/core/post/api.py +2 -2
- udata/core/reuse/api.py +32 -25
- udata/core/reuse/api_fields.py +2 -101
- udata/core/reuse/apiv2.py +4 -4
- udata/core/reuse/models.py +98 -16
- udata/core/site/api.py +2 -3
- udata/core/topic/api.py +2 -2
- udata/core/topic/apiv2.py +1 -2
- udata/core/user/api.py +2 -3
- udata/features/transfer/api.py +1 -2
- udata/mongo/datetime_fields.py +11 -4
- udata/mongo/document.py +2 -0
- udata/mongo/taglist_field.py +26 -0
- udata/static/admin.js +36 -36
- udata/static/admin.js.map +1 -1
- udata/static/chunks/{12.5b900cac4417e10ef3a0.js → 12.576e63b7a990f8eab784.js} +2 -2
- udata/static/chunks/12.576e63b7a990f8eab784.js.map +1 -0
- udata/static/chunks/{28.1759a7f57d526e6db574.js → 28.1ef31a46255dc2bf56d1.js} +2 -2
- udata/static/chunks/28.1ef31a46255dc2bf56d1.js.map +1 -0
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/api/test_activities_api.py +69 -0
- udata/tests/api/test_reuses_api.py +49 -0
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/METADATA +3 -1
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/RECORD +35 -35
- udata/core/reuse/forms.py +0 -45
- udata/static/chunks/12.5b900cac4417e10ef3a0.js.map +0 -1
- udata/static/chunks/28.1759a7f57d526e6db574.js.map +0 -1
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/LICENSE +0 -0
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/WHEEL +0 -0
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/entry_points.txt +0 -0
- {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31168.dist-info}/top_level.txt +0 -0
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
|
|
5
6
|
|
|
6
7
|
import udata.api.fields as custom_restx_fields
|
|
7
|
-
from udata.api import api
|
|
8
|
-
from udata.mongo.engine import db
|
|
8
|
+
from udata.api import api, base_reference
|
|
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,8 +28,6 @@ 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
|
-
|
|
33
31
|
params = {}
|
|
34
32
|
params["required"] = field.required
|
|
35
33
|
|
|
@@ -45,7 +43,9 @@ def convert_db_to_field(key, field, info={}):
|
|
|
45
43
|
# is always good enough.
|
|
46
44
|
return info.get("convert_to"), info.get("convert_to")
|
|
47
45
|
elif isinstance(field, mongo_fields.StringField):
|
|
48
|
-
constructor =
|
|
46
|
+
constructor = (
|
|
47
|
+
custom_restx_fields.Markdown if info.get("markdown", False) else restx_fields.String
|
|
48
|
+
)
|
|
49
49
|
params["min_length"] = field.min_length
|
|
50
50
|
params["max_length"] = field.max_length
|
|
51
51
|
params["enum"] = field.choices
|
|
@@ -61,11 +61,29 @@ 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
|
+
|
|
64
77
|
elif isinstance(field, mongo_fields.ListField):
|
|
65
78
|
# For lists, we convert the inner value from Mongo to RestX then we create
|
|
66
79
|
# 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__", {})
|
|
67
85
|
field_read, field_write = convert_db_to_field(
|
|
68
|
-
f"{key}.inner", field.field, info.get("inner_field_info", {})
|
|
86
|
+
f"{key}.inner", field.field, {**info, **inner_info, **info.get("inner_field_info", {})}
|
|
69
87
|
)
|
|
70
88
|
|
|
71
89
|
def constructor_read(**kwargs):
|
|
@@ -114,13 +132,13 @@ def convert_db_to_field(key, field, info={}):
|
|
|
114
132
|
)
|
|
115
133
|
|
|
116
134
|
else:
|
|
117
|
-
raise ValueError(f"Unsupported MongoEngine field type {field.__class__
|
|
135
|
+
raise ValueError(f"Unsupported MongoEngine field type {field.__class__}")
|
|
118
136
|
|
|
119
137
|
read_params = {**params, **read_params, **info}
|
|
120
138
|
write_params = {**params, **write_params, **info}
|
|
121
139
|
|
|
122
140
|
read = constructor_read(**read_params) if constructor_read else constructor(**read_params)
|
|
123
|
-
if write_params.get("readonly", False):
|
|
141
|
+
if write_params.get("readonly", False) or (constructor_write is None and constructor is None):
|
|
124
142
|
write = None
|
|
125
143
|
else:
|
|
126
144
|
write = (
|
|
@@ -129,6 +147,26 @@ def convert_db_to_field(key, field, info={}):
|
|
|
129
147
|
return read, write
|
|
130
148
|
|
|
131
149
|
|
|
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
|
+
|
|
132
170
|
def generate_fields(**kwargs):
|
|
133
171
|
"""
|
|
134
172
|
This decorator will create two auto-generated attributes on the class `__read_fields__` and `__write_fields__`
|
|
@@ -138,18 +176,18 @@ def generate_fields(**kwargs):
|
|
|
138
176
|
def wrapper(cls):
|
|
139
177
|
read_fields = {}
|
|
140
178
|
write_fields = {}
|
|
141
|
-
|
|
179
|
+
ref_fields = {}
|
|
180
|
+
sortables = kwargs.get("additionalSorts", [])
|
|
142
181
|
filterables = []
|
|
143
182
|
|
|
144
|
-
read_fields["id"] = restx_fields.String(required=True)
|
|
145
|
-
|
|
146
|
-
for key, field in cls._fields.items():
|
|
147
|
-
info = getattr(field, "__additional_field_info__", None)
|
|
148
|
-
if info is None:
|
|
149
|
-
continue
|
|
183
|
+
read_fields["id"] = restx_fields.String(required=True, readonly=True)
|
|
150
184
|
|
|
151
|
-
|
|
152
|
-
|
|
185
|
+
for key, field, info in get_fields(cls):
|
|
186
|
+
sortable_key = info.get("sortable", False)
|
|
187
|
+
if sortable_key:
|
|
188
|
+
sortables.append(
|
|
189
|
+
{"key": sortable_key if isinstance(sortable_key, str) else key, "value": key}
|
|
190
|
+
)
|
|
153
191
|
|
|
154
192
|
filterable = info.get("filterable", None)
|
|
155
193
|
if filterable is not None:
|
|
@@ -166,18 +204,26 @@ def generate_fields(**kwargs):
|
|
|
166
204
|
):
|
|
167
205
|
filterable["constraints"].append("objectid")
|
|
168
206
|
|
|
207
|
+
if "type" not in filterable:
|
|
208
|
+
filterable["type"] = str
|
|
209
|
+
if isinstance(field, mongo_fields.BooleanField):
|
|
210
|
+
filterable["type"] = bool
|
|
211
|
+
|
|
169
212
|
# We may add more information later here:
|
|
170
213
|
# - type of mongo query to execute (right now only simple =)
|
|
171
214
|
|
|
172
215
|
filterables.append(filterable)
|
|
173
216
|
|
|
174
|
-
read, write = convert_db_to_field(key, field)
|
|
217
|
+
read, write = convert_db_to_field(key, field, info)
|
|
175
218
|
|
|
176
219
|
if read:
|
|
177
220
|
read_fields[key] = read
|
|
178
221
|
if write:
|
|
179
222
|
write_fields[key] = write
|
|
180
223
|
|
|
224
|
+
if read and info.get("show_as_ref", False):
|
|
225
|
+
ref_fields[key] = read
|
|
226
|
+
|
|
181
227
|
# The goal of this loop is to fetch all functions (getters) of the class
|
|
182
228
|
# If a function has an `__additional_field_info__` attribute it means
|
|
183
229
|
# it has been decorated with `@function_field()` and should be included
|
|
@@ -209,9 +255,12 @@ def generate_fields(**kwargs):
|
|
|
209
255
|
read_fields[method_name] = restx_fields.String(
|
|
210
256
|
attribute=make_lambda(method), **{"readonly": True, **info}
|
|
211
257
|
)
|
|
258
|
+
if info.get("show_as_ref", False):
|
|
259
|
+
ref_fields[key] = read_fields[method_name]
|
|
212
260
|
|
|
213
261
|
cls.__read_fields__ = api.model(f"{cls.__name__} (read)", read_fields, **kwargs)
|
|
214
262
|
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)
|
|
215
264
|
|
|
216
265
|
mask = kwargs.pop("mask", None)
|
|
217
266
|
if mask is not None:
|
|
@@ -236,7 +285,9 @@ def generate_fields(**kwargs):
|
|
|
236
285
|
)
|
|
237
286
|
|
|
238
287
|
if sortables:
|
|
239
|
-
choices =
|
|
288
|
+
choices = [sortable["key"] for sortable in sortables] + [
|
|
289
|
+
"-" + sortable["key"] for sortable in sortables
|
|
290
|
+
]
|
|
240
291
|
parser.add_argument(
|
|
241
292
|
"sort",
|
|
242
293
|
type=str,
|
|
@@ -245,8 +296,12 @@ def generate_fields(**kwargs):
|
|
|
245
296
|
help="The field (and direction) on which sorting apply",
|
|
246
297
|
)
|
|
247
298
|
|
|
299
|
+
searchable = kwargs.pop("searchable", False)
|
|
300
|
+
if searchable:
|
|
301
|
+
parser.add_argument("q", type=str, location="args")
|
|
302
|
+
|
|
248
303
|
for filterable in filterables:
|
|
249
|
-
parser.add_argument(filterable["key"], type=
|
|
304
|
+
parser.add_argument(filterable["key"], type=filterable["type"], location="args")
|
|
250
305
|
|
|
251
306
|
cls.__index_parser__ = parser
|
|
252
307
|
|
|
@@ -254,7 +309,23 @@ def generate_fields(**kwargs):
|
|
|
254
309
|
args = cls.__index_parser__.parse_args()
|
|
255
310
|
|
|
256
311
|
if sortables and args["sort"]:
|
|
257
|
-
|
|
312
|
+
negate = args["sort"].startswith("-")
|
|
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)
|
|
258
329
|
|
|
259
330
|
for filterable in filterables:
|
|
260
331
|
if args.get(filterable["key"]):
|
|
@@ -303,11 +374,16 @@ def patch(obj, request):
|
|
|
303
374
|
Patch the object with the data from the request.
|
|
304
375
|
Only fields decorated with the `field()` decorator will be read (and not readonly).
|
|
305
376
|
"""
|
|
377
|
+
from udata.mongo.engine import db
|
|
378
|
+
|
|
306
379
|
for key, value in request.json.items():
|
|
307
380
|
field = obj.__write_fields__.get(key)
|
|
308
381
|
if field is not None and not field.readonly:
|
|
309
382
|
model_attribute = getattr(obj.__class__, key)
|
|
310
|
-
|
|
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(
|
|
311
387
|
model_attribute.field, mongoengine.fields.ReferenceField
|
|
312
388
|
):
|
|
313
389
|
# TODO `wrap_primary_key` do Mongo request, do a first pass to fetch all documents before calling it (to avoid multiple queries).
|
|
@@ -333,7 +409,7 @@ def patch(obj, request):
|
|
|
333
409
|
# `check` field attribute allows to do validation from the request before setting
|
|
334
410
|
# the attribute
|
|
335
411
|
check = info.get("check", None)
|
|
336
|
-
if check is not None:
|
|
412
|
+
if check is not None and value != getattr(obj, key):
|
|
337
413
|
check(**{key: value}) # TODO add other model attributes in function parameters
|
|
338
414
|
|
|
339
415
|
setattr(obj, key, value)
|
|
@@ -341,10 +417,21 @@ def patch(obj, request):
|
|
|
341
417
|
return obj
|
|
342
418
|
|
|
343
419
|
|
|
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
|
+
|
|
344
431
|
def wrap_primary_key(
|
|
345
432
|
field_name: str,
|
|
346
433
|
foreign_field: mongoengine.fields.ReferenceField | mongoengine.fields.GenericReferenceField,
|
|
347
|
-
value: str,
|
|
434
|
+
value: str | None,
|
|
348
435
|
document_type=None,
|
|
349
436
|
):
|
|
350
437
|
"""
|
|
@@ -353,6 +440,12 @@ def wrap_primary_key(
|
|
|
353
440
|
|
|
354
441
|
TODO: we only check the document reference if the ID is a `String` field (not in the case of a classic `ObjectId`).
|
|
355
442
|
"""
|
|
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
|
+
|
|
356
449
|
document_type = document_type or foreign_field.document_type().__class__
|
|
357
450
|
id_field_name = document_type._meta["id_field"]
|
|
358
451
|
|
|
@@ -375,7 +468,7 @@ def wrap_primary_key(
|
|
|
375
468
|
return foreign_document
|
|
376
469
|
|
|
377
470
|
if isinstance(id_field, mongoengine.fields.ObjectIdField):
|
|
378
|
-
return
|
|
471
|
+
return foreign_document.to_dbref()
|
|
379
472
|
elif isinstance(id_field, mongoengine.fields.StringField):
|
|
380
473
|
# Right now I didn't find a simpler way to make mongoengine happy.
|
|
381
474
|
# For references, it expects `ObjectId`, `DBRef`, `LazyReference` or `document` but since
|
udata/core/activity/api.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
+
from bson import ObjectId
|
|
3
4
|
from mongoengine.errors import DoesNotExist
|
|
4
5
|
|
|
5
6
|
from udata.api import API, api, fields
|
|
@@ -61,13 +62,19 @@ activity_parser.add_argument(
|
|
|
61
62
|
help="Filter activities for that particular organization",
|
|
62
63
|
location="args",
|
|
63
64
|
)
|
|
65
|
+
activity_parser.add_argument(
|
|
66
|
+
"related_to",
|
|
67
|
+
type=str,
|
|
68
|
+
help="Filter activities for that particular object id (ex : reuse, dataset, etc.)",
|
|
69
|
+
location="args",
|
|
70
|
+
)
|
|
64
71
|
|
|
65
72
|
|
|
66
73
|
@api.route("/activity", endpoint="activity")
|
|
67
74
|
class SiteActivityAPI(API):
|
|
68
75
|
@api.doc("activity")
|
|
69
76
|
@api.expect(activity_parser)
|
|
70
|
-
@api.
|
|
77
|
+
@api.marshal_with(activity_page_fields)
|
|
71
78
|
def get(self):
|
|
72
79
|
"""Fetch site activity, optionally filtered by user of org."""
|
|
73
80
|
args = activity_parser.parse_args()
|
|
@@ -79,6 +86,12 @@ class SiteActivityAPI(API):
|
|
|
79
86
|
if args["user"]:
|
|
80
87
|
qs = qs(actor=args["user"])
|
|
81
88
|
|
|
89
|
+
if args["related_to"]:
|
|
90
|
+
if not ObjectId.is_valid(args["related_to"]):
|
|
91
|
+
api.abort(400, "`related_to` arg must be an identifier")
|
|
92
|
+
|
|
93
|
+
qs = qs(related_to=args["related_to"])
|
|
94
|
+
|
|
82
95
|
qs = qs.order_by("-created_at")
|
|
83
96
|
qs = qs.paginate(args["page"], args["page_size"])
|
|
84
97
|
|
udata/core/badges/models.py
CHANGED
|
@@ -3,7 +3,9 @@ from datetime import datetime
|
|
|
3
3
|
|
|
4
4
|
from mongoengine.signals import post_save
|
|
5
5
|
|
|
6
|
+
from udata.api_fields import field
|
|
6
7
|
from udata.auth import current_user
|
|
8
|
+
from udata.core.badges.fields import badge_fields
|
|
7
9
|
from udata.mongo import db
|
|
8
10
|
|
|
9
11
|
from .signals import on_badge_added, on_badge_removed
|
|
@@ -40,7 +42,11 @@ class BadgesList(db.EmbeddedDocumentListField):
|
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class BadgeMixin(object):
|
|
43
|
-
badges =
|
|
45
|
+
badges = field(
|
|
46
|
+
BadgesList(),
|
|
47
|
+
readonly=True,
|
|
48
|
+
inner_field_info={"nested_fields": badge_fields},
|
|
49
|
+
)
|
|
44
50
|
|
|
45
51
|
def get_badge(self, kind):
|
|
46
52
|
"""Get a badge given its kind if present"""
|
udata/core/metrics/models.py
CHANGED
udata/core/organization/api.py
CHANGED
|
@@ -16,7 +16,6 @@ from udata.core.dataset.models import Dataset
|
|
|
16
16
|
from udata.core.discussions.api import discussion_fields
|
|
17
17
|
from udata.core.discussions.models import Discussion
|
|
18
18
|
from udata.core.followers.api import FollowAPI
|
|
19
|
-
from udata.core.reuse.api_fields import reuse_fields
|
|
20
19
|
from udata.core.reuse.models import Reuse
|
|
21
20
|
from udata.core.storages.api import (
|
|
22
21
|
image_parser,
|
|
@@ -462,7 +461,7 @@ class OrgDatasetsAPI(API):
|
|
|
462
461
|
@ns.route("/<org:org>/reuses/", endpoint="org_reuses")
|
|
463
462
|
class OrgReusesAPI(API):
|
|
464
463
|
@api.doc("list_organization_reuses")
|
|
465
|
-
@api.marshal_list_with(
|
|
464
|
+
@api.marshal_list_with(Reuse.__read_fields__)
|
|
466
465
|
def get(self, org):
|
|
467
466
|
"""List organization reuses (including private ones when member)"""
|
|
468
467
|
qs = Reuse.objects.owned_by(org)
|
udata/core/owned.py
CHANGED
|
@@ -32,7 +32,7 @@ def check_owner_is_current_user(owner):
|
|
|
32
32
|
current_user.is_authenticated
|
|
33
33
|
and owner
|
|
34
34
|
and not admin_permission
|
|
35
|
-
and current_user.id != owner
|
|
35
|
+
and current_user.id != owner.id
|
|
36
36
|
):
|
|
37
37
|
raise FieldValidationError(_("You can only set yourself as owner"), field="owner")
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ def check_organization_is_valid_for_current_user(organization):
|
|
|
41
41
|
from udata.auth import current_user
|
|
42
42
|
from udata.models import Organization
|
|
43
43
|
|
|
44
|
-
org = Organization.objects(id=organization).first()
|
|
44
|
+
org = Organization.objects(id=organization.id).first()
|
|
45
45
|
if org is None:
|
|
46
46
|
raise FieldValidationError(_("Unknown organization"), field="organization")
|
|
47
47
|
|
|
@@ -62,6 +62,7 @@ class Owned(object):
|
|
|
62
62
|
description="Only present if organization is not set. Can only be set to the current authenticated user.",
|
|
63
63
|
check=check_owner_is_current_user,
|
|
64
64
|
allow_null=True,
|
|
65
|
+
filterable={},
|
|
65
66
|
)
|
|
66
67
|
organization = field(
|
|
67
68
|
ReferenceField(Organization, reverse_delete_rule=NULLIFY),
|
|
@@ -69,6 +70,7 @@ class Owned(object):
|
|
|
69
70
|
description="Only present if owner is not set. Can only be set to an organization of the current authenticated user.",
|
|
70
71
|
check=check_organization_is_valid_for_current_user,
|
|
71
72
|
allow_null=True,
|
|
73
|
+
filterable={},
|
|
72
74
|
)
|
|
73
75
|
|
|
74
76
|
on_owner_change = signal("Owned.on_owner_change")
|
udata/core/post/api.py
CHANGED
|
@@ -3,7 +3,7 @@ from datetime import datetime
|
|
|
3
3
|
from udata.api import API, api, fields
|
|
4
4
|
from udata.auth import admin_permission
|
|
5
5
|
from udata.core.dataset.api_fields import dataset_fields
|
|
6
|
-
from udata.core.reuse.
|
|
6
|
+
from udata.core.reuse.models import Reuse
|
|
7
7
|
from udata.core.storages.api import (
|
|
8
8
|
image_parser,
|
|
9
9
|
parse_uploaded_image,
|
|
@@ -29,7 +29,7 @@ post_fields = api.model(
|
|
|
29
29
|
"credit_url": fields.String(description="An optional link associated to the credits"),
|
|
30
30
|
"tags": fields.List(fields.String, description="Some keywords to help in search"),
|
|
31
31
|
"datasets": fields.List(fields.Nested(dataset_fields), description="The post datasets"),
|
|
32
|
-
"reuses": fields.List(fields.Nested(
|
|
32
|
+
"reuses": fields.List(fields.Nested(Reuse.__read_fields__), description="The post reuses"),
|
|
33
33
|
"owner": fields.Nested(
|
|
34
34
|
user_ref_fields, description="The owner user", readonly=True, allow_null=True
|
|
35
35
|
),
|
udata/core/reuse/api.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
|
+
import mongoengine
|
|
3
4
|
from bson.objectid import ObjectId
|
|
4
5
|
from flask import request
|
|
6
|
+
from flask_login import current_user
|
|
5
7
|
|
|
6
8
|
from udata.api import API, api, errors
|
|
7
9
|
from udata.api.parsers import ModelApiParser
|
|
10
|
+
from udata.api_fields import patch, patch_and_save
|
|
8
11
|
from udata.auth import admin_permission
|
|
9
12
|
from udata.core.badges import api as badges_api
|
|
10
13
|
from udata.core.badges.fields import badge_fields
|
|
11
14
|
from udata.core.dataset.api_fields import dataset_ref_fields
|
|
12
15
|
from udata.core.followers.api import FollowAPI
|
|
16
|
+
from udata.core.reuse.constants import REUSE_TOPICS, REUSE_TYPES
|
|
13
17
|
from udata.core.storages.api import (
|
|
14
18
|
image_parser,
|
|
15
19
|
parse_uploaded_image,
|
|
@@ -19,14 +23,10 @@ from udata.models import Dataset
|
|
|
19
23
|
from udata.utils import id_or_404
|
|
20
24
|
|
|
21
25
|
from .api_fields import (
|
|
22
|
-
reuse_fields,
|
|
23
|
-
reuse_page_fields,
|
|
24
26
|
reuse_suggestion_fields,
|
|
25
27
|
reuse_topic_fields,
|
|
26
28
|
reuse_type_fields,
|
|
27
29
|
)
|
|
28
|
-
from .constants import REUSE_TOPICS, REUSE_TYPES
|
|
29
|
-
from .forms import ReuseForm
|
|
30
30
|
from .models import Reuse
|
|
31
31
|
from .permissions import ReuseEditPermission
|
|
32
32
|
|
|
@@ -96,24 +96,30 @@ reuse_parser = ReuseApiParser()
|
|
|
96
96
|
@ns.route("/", endpoint="reuses")
|
|
97
97
|
class ReuseListAPI(API):
|
|
98
98
|
@api.doc("list_reuses")
|
|
99
|
-
@api.expect(
|
|
100
|
-
@api.marshal_with(
|
|
99
|
+
@api.expect(Reuse.__index_parser__)
|
|
100
|
+
@api.marshal_with(Reuse.__page_fields__)
|
|
101
101
|
def get(self):
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
sort = args["sort"] or ("$text_score" if args["q"] else None) or DEFAULT_SORTING
|
|
106
|
-
return reuses.order_by(sort).paginate(args["page"], args["page_size"])
|
|
102
|
+
query = Reuse.objects(deleted=None, private__ne=True)
|
|
103
|
+
|
|
104
|
+
return Reuse.apply_sort_filters_and_pagination(query)
|
|
107
105
|
|
|
108
106
|
@api.secure
|
|
109
107
|
@api.doc("create_reuse")
|
|
110
|
-
@api.expect(
|
|
108
|
+
@api.expect(Reuse.__write_fields__)
|
|
111
109
|
@api.response(400, "Validation error")
|
|
112
|
-
@api.marshal_with(
|
|
110
|
+
@api.marshal_with(Reuse.__read_fields__, code=201)
|
|
113
111
|
def post(self):
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
reuse = patch(Reuse(), request)
|
|
113
|
+
|
|
114
|
+
if not reuse.owner and not reuse.organization:
|
|
115
|
+
reuse.owner = current_user._get_current_object()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
reuse.save()
|
|
119
|
+
except mongoengine.errors.ValidationError as e:
|
|
120
|
+
api.abort(400, e.message)
|
|
121
|
+
|
|
122
|
+
return patch_and_save(reuse, request), 201
|
|
117
123
|
|
|
118
124
|
|
|
119
125
|
@ns.route("/<reuse:reuse>/", endpoint="reuse", doc=common_doc)
|
|
@@ -121,7 +127,7 @@ class ReuseListAPI(API):
|
|
|
121
127
|
@api.response(410, "Reuse has been deleted")
|
|
122
128
|
class ReuseAPI(API):
|
|
123
129
|
@api.doc("get_reuse")
|
|
124
|
-
@api.marshal_with(
|
|
130
|
+
@api.marshal_with(Reuse.__read_fields__)
|
|
125
131
|
def get(self, reuse):
|
|
126
132
|
"""Fetch a given reuse"""
|
|
127
133
|
if reuse.deleted and not ReuseEditPermission(reuse).can():
|
|
@@ -130,8 +136,8 @@ class ReuseAPI(API):
|
|
|
130
136
|
|
|
131
137
|
@api.secure
|
|
132
138
|
@api.doc("update_reuse")
|
|
133
|
-
@api.expect(
|
|
134
|
-
@api.marshal_with(
|
|
139
|
+
@api.expect(Reuse.__write_fields__)
|
|
140
|
+
@api.marshal_with(Reuse.__read_fields__)
|
|
135
141
|
@api.response(400, errors.VALIDATION_ERROR)
|
|
136
142
|
def put(self, reuse):
|
|
137
143
|
"""Update a given reuse"""
|
|
@@ -139,8 +145,9 @@ class ReuseAPI(API):
|
|
|
139
145
|
if reuse.deleted and request_deleted is not None:
|
|
140
146
|
api.abort(410, "This reuse has been deleted")
|
|
141
147
|
ReuseEditPermission(reuse).test()
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
|
|
149
|
+
# This is a patch but old API acted like PATCH on PUT requests.
|
|
150
|
+
return patch_and_save(reuse, request)
|
|
144
151
|
|
|
145
152
|
@api.secure
|
|
146
153
|
@api.doc("delete_reuse")
|
|
@@ -160,8 +167,8 @@ class ReuseDatasetsAPI(API):
|
|
|
160
167
|
@api.secure
|
|
161
168
|
@api.doc("reuse_add_dataset", **common_doc)
|
|
162
169
|
@api.expect(dataset_ref_fields)
|
|
163
|
-
@api.response(200, "The dataset is already present",
|
|
164
|
-
@api.marshal_with(
|
|
170
|
+
@api.response(200, "The dataset is already present", Reuse.__read_fields__)
|
|
171
|
+
@api.marshal_with(Reuse.__read_fields__, code=201)
|
|
165
172
|
def post(self, reuse):
|
|
166
173
|
"""Add a dataset to a given reuse"""
|
|
167
174
|
if "id" not in request.json:
|
|
@@ -211,7 +218,7 @@ class ReuseBadgeAPI(API):
|
|
|
211
218
|
class ReuseFeaturedAPI(API):
|
|
212
219
|
@api.doc("feature_reuse")
|
|
213
220
|
@api.secure(admin_permission)
|
|
214
|
-
@api.marshal_with(
|
|
221
|
+
@api.marshal_with(Reuse.__read_fields__)
|
|
215
222
|
def post(self, reuse):
|
|
216
223
|
"""Mark a reuse as featured"""
|
|
217
224
|
reuse.featured = True
|
|
@@ -220,7 +227,7 @@ class ReuseFeaturedAPI(API):
|
|
|
220
227
|
|
|
221
228
|
@api.doc("unfeature_reuse")
|
|
222
229
|
@api.secure(admin_permission)
|
|
223
|
-
@api.marshal_with(
|
|
230
|
+
@api.marshal_with(Reuse.__read_fields__)
|
|
224
231
|
def delete(self, reuse):
|
|
225
232
|
"""Unmark a reuse as featured"""
|
|
226
233
|
reuse.featured = False
|