udata 9.1.4.dev31123__py2.py3-none-any.whl → 9.1.4.dev31143__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 (36) hide show
  1. udata/api_fields.py +119 -26
  2. udata/core/badges/models.py +7 -1
  3. udata/core/metrics/models.py +4 -1
  4. udata/core/organization/api.py +1 -2
  5. udata/core/owned.py +4 -2
  6. udata/core/post/api.py +2 -2
  7. udata/core/reuse/api.py +32 -25
  8. udata/core/reuse/api_fields.py +2 -101
  9. udata/core/reuse/apiv2.py +4 -4
  10. udata/core/reuse/models.py +98 -16
  11. udata/core/site/api.py +2 -3
  12. udata/core/topic/api.py +2 -2
  13. udata/core/topic/apiv2.py +1 -2
  14. udata/core/user/api.py +2 -3
  15. udata/features/transfer/api.py +1 -2
  16. udata/mongo/datetime_fields.py +11 -4
  17. udata/mongo/document.py +2 -0
  18. udata/mongo/taglist_field.py +26 -0
  19. udata/static/admin.js +36 -36
  20. udata/static/admin.js.map +1 -1
  21. udata/static/chunks/{12.5b900cac4417e10ef3a0.js → 12.576e63b7a990f8eab784.js} +2 -2
  22. udata/static/chunks/12.576e63b7a990f8eab784.js.map +1 -0
  23. udata/static/chunks/{28.1759a7f57d526e6db574.js → 28.1ef31a46255dc2bf56d1.js} +2 -2
  24. udata/static/chunks/28.1ef31a46255dc2bf56d1.js.map +1 -0
  25. udata/static/common.js +1 -1
  26. udata/static/common.js.map +1 -1
  27. udata/tests/api/test_reuses_api.py +49 -0
  28. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/METADATA +2 -1
  29. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/RECORD +33 -34
  30. udata/core/reuse/forms.py +0 -45
  31. udata/static/chunks/12.5b900cac4417e10ef3a0.js.map +0 -1
  32. udata/static/chunks/28.1759a7f57d526e6db574.js.map +0 -1
  33. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/LICENSE +0 -0
  34. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/WHEEL +0 -0
  35. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.dist-info}/entry_points.txt +0 -0
  36. {udata-9.1.4.dev31123.dist-info → udata-9.1.4.dev31143.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 = restx_fields.String
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__.__name__}")
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
- sortables = []
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
- if info.get("sortable", False):
152
- sortables.append(key)
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 = sortables + ["-" + k for k in sortables]
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=str, location="args")
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
- base_query = base_query.order_by(args["sort"])
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
- if isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
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 ObjectId(value)
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
@@ -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 = BadgesList()
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"""
@@ -5,7 +5,10 @@ __all__ = ("WithMetrics",)
5
5
 
6
6
 
7
7
  class WithMetrics(object):
8
- metrics = field(db.DictField())
8
+ metrics = field(
9
+ db.DictField(),
10
+ readonly=True,
11
+ )
9
12
 
10
13
  __metrics_keys__ = []
11
14
 
@@ -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(reuse_fields)
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.api_fields import reuse_fields
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(reuse_fields), description="The post reuses"),
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(reuse_parser.parser)
100
- @api.marshal_with(reuse_page_fields)
99
+ @api.expect(Reuse.__index_parser__)
100
+ @api.marshal_with(Reuse.__page_fields__)
101
101
  def get(self):
102
- args = reuse_parser.parse()
103
- reuses = Reuse.objects(deleted=None, private__ne=True)
104
- reuses = reuse_parser.parse_filters(reuses, args)
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(reuse_fields)
108
+ @api.expect(Reuse.__write_fields__)
111
109
  @api.response(400, "Validation error")
112
- @api.marshal_with(reuse_fields)
110
+ @api.marshal_with(Reuse.__read_fields__, code=201)
113
111
  def post(self):
114
- """Create a new object"""
115
- form = api.validate(ReuseForm)
116
- return form.save(), 201
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(reuse_fields)
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(reuse_fields)
134
- @api.marshal_with(reuse_fields)
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
- form = api.validate(ReuseForm, reuse)
143
- return form.save()
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", reuse_fields)
164
- @api.marshal_with(reuse_fields, code=201)
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(reuse_fields)
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(reuse_fields)
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
@@ -1,108 +1,9 @@
1
- from udata.api import api, base_reference, fields
2
- from udata.core.badges.fields import badge_fields
3
- from udata.core.dataset.api_fields import dataset_fields
4
- from udata.core.organization.api_fields import org_ref_fields
5
- from udata.core.user.api_fields import user_ref_fields
1
+ from udata.api import api, fields
6
2
 
7
- from .constants import IMAGE_SIZES, REUSE_TOPICS, REUSE_TYPES
3
+ from .constants import IMAGE_SIZES
8
4
 
9
5
  BIGGEST_IMAGE_SIZE = IMAGE_SIZES[0]
10
6
 
11
-
12
- reuse_fields = api.model(
13
- "Reuse",
14
- {
15
- "id": fields.String(description="The reuse identifier", readonly=True),
16
- "title": fields.String(description="The reuse title", required=True),
17
- "slug": fields.String(description="The reuse permalink string", readonly=True),
18
- "type": fields.String(description="The reuse type", required=True, enum=list(REUSE_TYPES)),
19
- "url": fields.String(description="The reuse remote URL (website)", required=True),
20
- "description": fields.Markdown(
21
- description="The reuse description in Markdown", required=True
22
- ),
23
- "tags": fields.List(fields.String, description="Some keywords to help in search"),
24
- "badges": fields.List(
25
- fields.Nested(badge_fields), description="The reuse badges", readonly=True
26
- ),
27
- "topic": fields.String(
28
- description="The reuse topic", required=True, enum=list(REUSE_TOPICS)
29
- ),
30
- "featured": fields.Boolean(description="Is the reuse featured", readonly=True),
31
- "private": fields.Boolean(
32
- description="Is the reuse private to the owner or the organization"
33
- ),
34
- "image": fields.ImageField(description="The reuse thumbnail thumbnail (cropped) URL"),
35
- "image_thumbnail": fields.ImageField(
36
- attribute="image",
37
- size=BIGGEST_IMAGE_SIZE,
38
- description="The reuse thumbnail thumbnail URL. This is the square "
39
- "({0}x{0}) and cropped version.".format(BIGGEST_IMAGE_SIZE),
40
- ),
41
- "created_at": fields.ISODateTime(description="The reuse creation date", readonly=True),
42
- "last_modified": fields.ISODateTime(
43
- description="The reuse last modification date", readonly=True
44
- ),
45
- "deleted": fields.ISODateTime(description="The deletion date if deleted", readonly=True),
46
- "archived": fields.ISODateTime(
47
- description="The archivation date if archived", allow_null=True
48
- ),
49
- "datasets": fields.List(fields.Nested(dataset_fields), description="The reused datasets"),
50
- "organization": fields.Nested(
51
- org_ref_fields,
52
- allow_null=True,
53
- description="The publishing organization",
54
- readonly=True,
55
- ),
56
- "owner": fields.Nested(
57
- user_ref_fields, description="The owner user", readonly=True, allow_null=True
58
- ),
59
- "metrics": fields.Raw(
60
- attribute=lambda o: o.get_metrics(), description="The reuse metrics", readonly=True
61
- ),
62
- "uri": fields.UrlFor(
63
- "api.reuse", lambda o: {"reuse": o}, description="The reuse API URI", readonly=True
64
- ),
65
- "page": fields.UrlFor(
66
- "reuses.show",
67
- lambda o: {"reuse": o},
68
- description="The reuse page URL",
69
- readonly=True,
70
- fallback_endpoint="api.reuse",
71
- ),
72
- "extras": fields.Raw(description="Extras attributes as key-value pairs"),
73
- },
74
- mask="*,datasets{title,uri,page}",
75
- )
76
-
77
- reuse_page_fields = api.model("ReusePage", fields.pager(reuse_fields))
78
-
79
-
80
- reuse_ref_fields = api.inherit(
81
- "ReuseReference",
82
- base_reference,
83
- {
84
- "title": fields.String(description="The reuse title", readonly=True),
85
- "image": fields.ImageField(description="The reuse thumbnail thumbnail (cropped) URL"),
86
- "image_thumbnail": fields.ImageField(
87
- attribute="image",
88
- size=BIGGEST_IMAGE_SIZE,
89
- description="The reuse thumbnail thumbnail URL. This is the square "
90
- "({0}x{0}) and cropped version.".format(BIGGEST_IMAGE_SIZE),
91
- ),
92
- "uri": fields.UrlFor(
93
- "api.reuse", lambda o: {"reuse": o}, description="The reuse API URI", readonly=True
94
- ),
95
- "page": fields.UrlFor(
96
- "reuses.show",
97
- lambda o: {"reuse": o},
98
- description="The reuse page URL",
99
- readonly=True,
100
- fallback_endpoint="api.reuse",
101
- ),
102
- },
103
- )
104
-
105
-
106
7
  reuse_type_fields = api.model(
107
8
  "ReuseType",
108
9
  {
udata/core/reuse/apiv2.py CHANGED
@@ -2,13 +2,13 @@ from flask import request
2
2
 
3
3
  from udata import search
4
4
  from udata.api import API, apiv2
5
+ from udata.core.reuse.models import Reuse
5
6
  from udata.utils import multi_to_dict
6
7
 
7
- from .api_fields import reuse_fields, reuse_page_fields
8
8
  from .search import ReuseSearch
9
9
 
10
- apiv2.inherit("ReusePage", reuse_page_fields)
11
- apiv2.inherit("Reuse", reuse_fields)
10
+ apiv2.inherit("ReusePage", Reuse.__page_fields__)
11
+ apiv2.inherit("Reuse (read)", Reuse.__read_fields__)
12
12
 
13
13
  ns = apiv2.namespace("reuses", "Reuse related operations")
14
14
 
@@ -23,7 +23,7 @@ class ReuseSearchAPI(API):
23
23
 
24
24
  @apiv2.doc("search_reuses")
25
25
  @apiv2.expect(search_parser)
26
- @apiv2.marshal_with(reuse_page_fields)
26
+ @apiv2.marshal_with(Reuse.__page_fields__)
27
27
  def get(self):
28
28
  """Search all reuses"""
29
29
  search_parser.parse_args()