udata 10.8.4.dev37423__py2.py3-none-any.whl → 10.8.4.dev37447__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 (37) hide show
  1. udata/api/__init__.py +5 -1
  2. udata/api_fields.py +94 -11
  3. udata/core/dataservices/models.py +2 -2
  4. udata/core/dataset/search.py +9 -0
  5. udata/core/pages/api.py +54 -0
  6. udata/core/pages/factories.py +10 -0
  7. udata/core/pages/models.py +103 -0
  8. udata/core/pages/permissions.py +7 -0
  9. udata/core/pages/tests/test_api.py +75 -0
  10. udata/core/site/api.py +11 -58
  11. udata/core/site/models.py +9 -4
  12. udata/models/__init__.py +1 -0
  13. udata/routing.py +5 -0
  14. udata/static/chunks/{10.8ca60413647062717b1e.js → 10.471164b2a9fe15614797.js} +3 -3
  15. udata/static/chunks/{10.8ca60413647062717b1e.js.map → 10.471164b2a9fe15614797.js.map} +1 -1
  16. udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.51d706fb9521c16976bc.js} +3 -3
  17. udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.51d706fb9521c16976bc.js.map} +1 -1
  18. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.f29411b06be1883356a3.js} +2 -2
  19. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.f29411b06be1883356a3.js.map} +1 -1
  20. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.3bd0340930d4a314ce9c.js} +2 -2
  21. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.3bd0340930d4a314ce9c.js.map} +1 -1
  22. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.8da42e8359d72afc2618.js} +3 -3
  23. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.8da42e8359d72afc2618.js.map} +1 -1
  24. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
  25. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
  26. udata/static/chunks/{9.033d7e190ca9e226a5d0.js → 9.07515e5187f475bce828.js} +3 -3
  27. udata/static/chunks/{9.033d7e190ca9e226a5d0.js.map → 9.07515e5187f475bce828.js.map} +1 -1
  28. udata/static/common.js +1 -1
  29. udata/static/common.js.map +1 -1
  30. udata/tests/site/test_site_api.py +20 -42
  31. udata/tests/test_api_fields.py +12 -14
  32. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/METADATA +2 -1
  33. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/RECORD +37 -32
  34. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/LICENSE +0 -0
  35. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/WHEEL +0 -0
  36. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/entry_points.txt +0 -0
  37. {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37447.dist-info}/top_level.txt +0 -0
udata/api/__init__.py CHANGED
@@ -268,6 +268,7 @@ validation_error_fields_v2 = apiv2.inherit("ValidationError", validation_error_f
268
268
 
269
269
  def convert_object_of_exceptions_to_object_of_strings(exceptions: dict):
270
270
  errors = {}
271
+
271
272
  for key, exception in exceptions.items():
272
273
  if isinstance(exception, Exception):
273
274
  errors[key] = str(exception)
@@ -290,7 +291,9 @@ def handle_validation_error(error: mongoengine.errors.ValidationError):
290
291
  """Error returned when validation failed."""
291
292
  return (
292
293
  {
293
- "errors": convert_object_of_exceptions_to_object_of_strings(error.errors),
294
+ "errors": convert_object_of_exceptions_to_object_of_strings(error.errors)
295
+ if error.errors
296
+ else {},
294
297
  "message": str(error),
295
298
  },
296
299
  400,
@@ -343,6 +346,7 @@ def init_app(app):
343
346
  import udata.core.reuse.apiv2 # noqa
344
347
  import udata.core.organization.api # noqa
345
348
  import udata.core.organization.apiv2 # noqa
349
+ import udata.core.pages.api # noqa
346
350
  import udata.core.followers.api # noqa
347
351
  import udata.core.jobs.api # noqa
348
352
  import udata.core.reports.api # noqa
udata/api_fields.py CHANGED
@@ -43,6 +43,8 @@ import flask_restx.fields as restx_fields
43
43
  import mongoengine
44
44
  import mongoengine.fields as mongo_fields
45
45
  from bson import DBRef, ObjectId
46
+ from flask import Request
47
+ from flask_restx import marshal
46
48
  from flask_restx.inputs import boolean
47
49
  from flask_restx.reqparse import RequestParser
48
50
  from flask_storage.mongo import ImageField as FlaskStorageImageField
@@ -60,6 +62,24 @@ lazy_reference = api.model(
60
62
  },
61
63
  )
62
64
 
65
+ DEFAULT_GENERIC_KEY = "class"
66
+
67
+ classes_by_names = {}
68
+ classes_by_parents = {}
69
+
70
+
71
+ class GenericField(restx_fields.Raw):
72
+ def __init__(self, fields_by_type):
73
+ super().__init__(self)
74
+ self.default = None
75
+ self.fields_by_type = fields_by_type
76
+
77
+ def format(self, value):
78
+ # Value is one of the generic object
79
+ data = marshal(value, self.fields_by_type[value.__class__.__name__])
80
+ data[DEFAULT_GENERIC_KEY] = value.__class__.__name__
81
+ return data
82
+
63
83
 
64
84
  def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | None]:
65
85
  """Map a Mongo field to a Flask RestX field.
@@ -96,12 +116,19 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
96
116
  params["enum"] = field.choices
97
117
  if field.validation:
98
118
  params["validation"] = validation_to_type(field.validation)
119
+ elif isinstance(field, mongo_fields.UUIDField):
120
+ # TODO add validation?
121
+ constructor = restx_fields.String
99
122
  elif isinstance(field, mongo_fields.ObjectIdField):
100
123
  constructor = restx_fields.String
101
124
  elif isinstance(field, mongo_fields.FloatField):
102
125
  constructor = restx_fields.Float
103
126
  params["min"] = field.min # TODO min_value?
104
127
  params["max"] = field.max
128
+ elif isinstance(field, mongo_fields.IntField):
129
+ constructor = restx_fields.Integer
130
+ params["min"] = field.min_value
131
+ params["max"] = field.max_value
105
132
  elif isinstance(field, mongo_fields.BooleanField):
106
133
  constructor = restx_fields.Boolean
107
134
  elif isinstance(field, mongo_fields.DateTimeField):
@@ -125,6 +152,7 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
125
152
  # For lists, we can expose them only by showing a link to the API
126
153
  # with the results of the list to avoid listing a lot of sub-ressources
127
154
  # (for example for a dataservices with thousands of datasets).
155
+
128
156
  href = info.get("href", None)
129
157
  if href:
130
158
 
@@ -147,9 +175,36 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
147
175
  # 2. `__additional_field_info__` of the inner field
148
176
  # 3. `__additional_field_info__` of the parent
149
177
  inner_info: dict = getattr(field.field, "__additional_field_info__", {})
150
- field_read, field_write = convert_db_to_field(
151
- f"{key}.inner", field.field, {**info, **inner_info, **info.get("inner_field_info", {})}
178
+ nested_info = {**info, **inner_info, **info.get("inner_field_info", {})}
179
+
180
+ generic = info.get("generic", False)
181
+
182
+ allowed_classes = (
183
+ classes_by_parents[field.field.document_type_obj]
184
+ if isinstance(field.field, mongoengine.fields.EmbeddedDocumentField)
185
+ and field.field.document_type_obj in classes_by_parents
186
+ else set()
152
187
  )
188
+ if generic and allowed_classes:
189
+ generic_fields = {
190
+ cls.__name__: convert_db_to_field(
191
+ f"{key}.{cls.__name__}",
192
+ # Instead of having EmbeddedDocumentField(Bloc) we'll create fields for each
193
+ # of the subclasses with EmbededdDocumentField(DatasetsListBloc), EmbeddedDocumentFied(DataservicesListBloc)…
194
+ mongoengine.fields.EmbeddedDocumentField(cls),
195
+ nested_info,
196
+ )
197
+ for cls in allowed_classes
198
+ }
199
+
200
+ field_read = GenericField({k: v[0].model for k, v in generic_fields.items()})
201
+ field_write = GenericField({k: v[1].model for k, v in generic_fields.items()})
202
+ else:
203
+ field_read, field_write = convert_db_to_field(
204
+ f"{key}.inner",
205
+ field.field,
206
+ nested_info,
207
+ )
153
208
 
154
209
  if constructor_read is None:
155
210
  # We don't want to set the `constructor_read` if it's already set
@@ -243,6 +298,19 @@ def get_fields(cls) -> Iterable[tuple[str, Callable, dict]]:
243
298
  )
244
299
 
245
300
 
301
+ def save_class_by_parents(cls):
302
+ from udata.mongo.engine import db
303
+
304
+ for parent in cls.__bases__:
305
+ if parent == db.Document:
306
+ return
307
+
308
+ classes_by_parents[parent] = (
309
+ classes_by_parents[parent] if parent in classes_by_parents else set()
310
+ )
311
+ classes_by_parents[parent].add(cls)
312
+
313
+
246
314
  def generate_fields(**kwargs) -> Callable:
247
315
  """Mongoengine document decorator.
248
316
 
@@ -271,6 +339,9 @@ def generate_fields(**kwargs) -> Callable:
271
339
 
272
340
  read_fields["id"] = restx_fields.String(required=True, readonly=True)
273
341
 
342
+ classes_by_names[cls.__name__] = cls
343
+ save_class_by_parents(cls)
344
+
274
345
  for key, field, info in get_fields(cls):
275
346
  sortable_key: bool = info.get("sortable", False)
276
347
  if sortable_key:
@@ -513,10 +584,13 @@ def patch(obj, request) -> type:
513
584
  """
514
585
  from udata.mongo.engine import db
515
586
 
516
- for key, value in request.json.items():
587
+ data = request.json if isinstance(request, Request) else request
588
+ for key, value in data.items():
517
589
  field = obj.__write_fields__.get(key)
518
590
  if field is not None and not field.readonly:
519
591
  model_attribute = getattr(obj.__class__, key)
592
+ info = getattr(model_attribute, "__additional_field_info__", {})
593
+
520
594
  if hasattr(model_attribute, "from_input"):
521
595
  value = model_attribute.from_input(value)
522
596
  elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
@@ -546,18 +620,27 @@ def patch(obj, request) -> type:
546
620
  model_attribute,
547
621
  mongoengine.fields.EmbeddedDocumentField,
548
622
  ):
549
- embedded_field = model_attribute.document_type()
550
- value = embedded_field._from_son(value)
623
+ embedded_field = model_attribute.document_type().__class__
624
+ value = patch(embedded_field(), value)
551
625
  elif value and isinstance(
552
626
  model_attribute,
553
627
  mongoengine.fields.EmbeddedDocumentListField,
554
628
  ):
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]
559
-
560
- info = getattr(model_attribute, "__additional_field_info__", {})
629
+ base_embedded_field = model_attribute.field.document_type().__class__
630
+ generic = info.get("generic", False)
631
+ generic_key = info.get("generic_key", DEFAULT_GENERIC_KEY)
632
+
633
+ objects = []
634
+ for embedded_value in value:
635
+ # TODO add validation on generic_key presence and value
636
+ embedded_field = (
637
+ classes_by_names[embedded_value[generic_key]]
638
+ if generic
639
+ else base_embedded_field
640
+ )
641
+ objects.append(patch(embedded_field(), embedded_value))
642
+
643
+ value = objects
561
644
 
562
645
  # `checks` field attribute allows to do validation from the request before setting
563
646
  # the attribute
@@ -6,7 +6,6 @@ from mongoengine import Q
6
6
  from mongoengine.signals import post_save
7
7
 
8
8
  import udata.core.contact_point.api_fields as contact_api_fields
9
- import udata.core.dataset.api_fields as datasets_api_fields
10
9
  from udata.api import api, fields
11
10
  from udata.api_fields import field, function_field, generate_fields
12
11
  from udata.core.activity.models import Auditable
@@ -16,6 +15,7 @@ from udata.core.dataservices.constants import (
16
15
  DATASERVICE_ACCESS_TYPES,
17
16
  DATASERVICE_FORMATS,
18
17
  )
18
+ from udata.core.dataset.api_fields import dataset_ref_fields
19
19
  from udata.core.dataset.models import Dataset
20
20
  from udata.core.linkable import Linkable
21
21
  from udata.core.metrics.helpers import get_stock_metrics
@@ -264,7 +264,7 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
264
264
  db.ListField(
265
265
  field(
266
266
  db.LazyReferenceField(Dataset, passthrough=True),
267
- nested_fields=datasets_api_fields.dataset_ref_fields,
267
+ nested_fields=dataset_ref_fields,
268
268
  )
269
269
  ),
270
270
  filterable={
@@ -18,6 +18,11 @@ from udata.utils import to_iso_datetime
18
18
  __all__ = ("DatasetSearch",)
19
19
 
20
20
 
21
+ # This const is kept to prevent creating huge documents and paylods for datasets
22
+ # with a large number of resources
23
+ MAX_NUMBER_OF_RESOURCES_TO_INDEX = 500
24
+
25
+
21
26
  @register
22
27
  class DatasetSearch(ModelSearchAdapter):
23
28
  model = Dataset
@@ -99,6 +104,10 @@ class DatasetSearch(ModelSearchAdapter):
99
104
  "reuses": dataset.metrics.get("reuses", 0),
100
105
  "featured": 1 if dataset.featured else 0,
101
106
  "resources_count": len(dataset.resources),
107
+ "resources": [
108
+ {"id": str(res.id), "title": res.title}
109
+ for res in dataset.resources[:MAX_NUMBER_OF_RESOURCES_TO_INDEX]
110
+ ],
102
111
  "organization": organization,
103
112
  "owner": str(owner.id) if owner else None,
104
113
  "format": [r.format.lower() for r in dataset.resources if r.format],
@@ -0,0 +1,54 @@
1
+ from flask import request
2
+ from flask_login import current_user
3
+
4
+ from udata.api import API, api
5
+ from udata.api_fields import patch
6
+ from udata.auth import admin_permission
7
+
8
+ from .models import Page
9
+
10
+ ns = api.namespace("pages", "Pages related operations (beta)")
11
+
12
+ common_doc = {"params": {"page": "The page ID or slug"}}
13
+
14
+
15
+ @ns.route("/", endpoint="pages")
16
+ class PagesAPI(API):
17
+ @api.secure(admin_permission)
18
+ @api.doc("list_pages")
19
+ @api.expect(Page.__index_parser__)
20
+ @api.marshal_with(Page.__page_fields__)
21
+ def get(self):
22
+ """List or search all pages"""
23
+ return Page.apply_pagination(Page.objects)
24
+
25
+ @api.secure
26
+ @api.doc("create_page", responses={400: "Validation error"})
27
+ @api.expect(Page.__write_fields__)
28
+ @api.marshal_with(Page.__read_fields__, code=201)
29
+ def post(self):
30
+ page = patch(Page(), request)
31
+ if not page.owner and not page.organization:
32
+ page.owner = current_user._get_current_object()
33
+
34
+ page.save()
35
+ return page, 201
36
+
37
+
38
+ @ns.route("/<page:page>/", endpoint="page")
39
+ class PageAPI(API):
40
+ @api.doc("get_page")
41
+ @api.marshal_with(Page.__read_fields__)
42
+ def get(self, page: Page):
43
+ return page
44
+
45
+ @api.secure
46
+ @api.doc("update_page")
47
+ @api.expect(Page.__write_fields__)
48
+ @api.marshal_with(Page.__read_fields__)
49
+ def put(self, page: Page):
50
+ page.permissions["edit"].test()
51
+ patch(page, request)
52
+
53
+ page.save()
54
+ return page
@@ -0,0 +1,10 @@
1
+ from udata.factories import ModelFactory
2
+
3
+ from .models import Page
4
+
5
+
6
+ class PageFactory(ModelFactory):
7
+ class Meta:
8
+ model = Page
9
+
10
+ blocs = []
@@ -0,0 +1,103 @@
1
+ from udata.api import api, fields
2
+ from udata.api_fields import field, function_field, generate_fields
3
+ from udata.core.activity.models import Auditable
4
+ from udata.core.dataservices.models import Dataservice
5
+ from udata.core.dataset.api_fields import dataset_fields
6
+ from udata.core.owned import Owned
7
+ from udata.core.reuse.models import Reuse
8
+ from udata.models import db
9
+ from udata.mongo.datetime_fields import Datetimed
10
+
11
+ page_permissions_fields = api.model(
12
+ "PagePermissions",
13
+ {
14
+ "delete": fields.Permission(),
15
+ "edit": fields.Permission(),
16
+ },
17
+ )
18
+
19
+
20
+ @generate_fields()
21
+ class Bloc(db.EmbeddedDocument):
22
+ meta = {"allow_inheritance": True}
23
+
24
+ id = field(db.AutoUUIDField(primary_key=True))
25
+
26
+
27
+ class BlocWithTitleMixin:
28
+ title = field(db.StringField(required=True))
29
+ subtitle = field(db.StringField())
30
+
31
+
32
+ @generate_fields(
33
+ mask="*,datasets{id,title,uri,page,private,archived,organization,owner,last_update,quality,metrics,description}"
34
+ )
35
+ class DatasetsListBloc(BlocWithTitleMixin, Bloc):
36
+ datasets = field(
37
+ db.ListField(
38
+ field(
39
+ db.ReferenceField("Dataset"),
40
+ nested_fields=dataset_fields,
41
+ )
42
+ )
43
+ )
44
+
45
+
46
+ @generate_fields()
47
+ class ReusesListBloc(BlocWithTitleMixin, Bloc):
48
+ reuses = field(
49
+ db.ListField(
50
+ field(
51
+ db.ReferenceField("Reuse"),
52
+ nested_fields=Reuse.__read_fields__,
53
+ )
54
+ )
55
+ )
56
+
57
+
58
+ @generate_fields()
59
+ class DataservicesListBloc(BlocWithTitleMixin, Bloc):
60
+ dataservices = field(
61
+ db.ListField(
62
+ field(
63
+ db.ReferenceField("Dataservice"),
64
+ nested_fields=Dataservice.__read_fields__,
65
+ )
66
+ )
67
+ )
68
+
69
+
70
+ @generate_fields()
71
+ class LinkInBloc(db.EmbeddedDocument):
72
+ title = field(db.StringField(required=True))
73
+ color = field(db.StringField())
74
+ url = field(db.StringField())
75
+
76
+
77
+ @generate_fields()
78
+ class LinksListBloc(BlocWithTitleMixin, Bloc):
79
+ paragraph = field(db.StringField())
80
+ main_link_url = field(db.StringField())
81
+ main_link_title = field(db.StringField())
82
+
83
+ links = field(db.EmbeddedDocumentListField(LinkInBloc))
84
+
85
+
86
+ @generate_fields()
87
+ class Page(Auditable, Owned, Datetimed, db.Document):
88
+ blocs = field(
89
+ db.EmbeddedDocumentListField(Bloc),
90
+ generic=True,
91
+ )
92
+
93
+ @property
94
+ @function_field(
95
+ nested_fields=page_permissions_fields,
96
+ )
97
+ def permissions(self):
98
+ from .permissions import PageEditPermission
99
+
100
+ return {
101
+ "delete": PageEditPermission(self),
102
+ "edit": PageEditPermission(self),
103
+ }
@@ -0,0 +1,7 @@
1
+ from udata.core.dataset.permissions import OwnablePermission
2
+
3
+
4
+ class PageEditPermission(OwnablePermission):
5
+ """Permissions to edit a Page"""
6
+
7
+ pass
@@ -0,0 +1,75 @@
1
+ from flask import url_for
2
+
3
+ from udata.core.dataset.factories import DatasetFactory
4
+ from udata.core.pages.models import Page
5
+ from udata.tests.api import APITestCase
6
+
7
+
8
+ class PageAPITest(APITestCase):
9
+ modules = []
10
+
11
+ def test_create_get_update(self):
12
+ self.login()
13
+ datasets = DatasetFactory.create_batch(3)
14
+
15
+ response = self.post(
16
+ url_for("api.pages"),
17
+ {
18
+ "blocs": [
19
+ {
20
+ "class": "DatasetsListBloc",
21
+ "title": "My awesome title",
22
+ "datasets": [str(d.id) for d in datasets],
23
+ }
24
+ ],
25
+ },
26
+ )
27
+ self.assert201(response)
28
+
29
+ self.assertEqual(len(response.json["blocs"][0]["datasets"]), 3)
30
+ self.assertEqual("DatasetsListBloc", response.json["blocs"][0]["class"])
31
+ self.assertEqual("My awesome title", response.json["blocs"][0]["title"])
32
+ self.assertIsNone(response.json["blocs"][0]["subtitle"])
33
+ self.assertEqual(str(datasets[0].id), response.json["blocs"][0]["datasets"][0]["id"])
34
+ self.assertEqual(str(datasets[1].id), response.json["blocs"][0]["datasets"][1]["id"])
35
+ self.assertEqual(str(datasets[2].id), response.json["blocs"][0]["datasets"][2]["id"])
36
+
37
+ page = Page.objects().first()
38
+ self.assertEqual(str(page.id), response.json["id"])
39
+ self.assertEqual("My awesome title", page.blocs[0].title)
40
+ self.assertIsNone(page.blocs[0].subtitle)
41
+ self.assertEqual(datasets[0].id, page.blocs[0].datasets[0].id)
42
+ self.assertEqual(datasets[1].id, page.blocs[0].datasets[1].id)
43
+ self.assertEqual(datasets[2].id, page.blocs[0].datasets[2].id)
44
+
45
+ response = self.get(url_for("api.page", page=page))
46
+ self.assert200(response)
47
+
48
+ self.assertEqual(len(response.json["blocs"][0]["datasets"]), 3)
49
+ self.assertEqual("DatasetsListBloc", response.json["blocs"][0]["class"])
50
+ self.assertEqual("My awesome title", response.json["blocs"][0]["title"])
51
+ self.assertIsNone(response.json["blocs"][0]["subtitle"])
52
+ self.assertEqual(str(datasets[0].id), response.json["blocs"][0]["datasets"][0]["id"])
53
+ self.assertEqual(str(datasets[1].id), response.json["blocs"][0]["datasets"][1]["id"])
54
+ self.assertEqual(str(datasets[2].id), response.json["blocs"][0]["datasets"][2]["id"])
55
+
56
+ response = self.put(
57
+ url_for("api.page", page=page),
58
+ {
59
+ "blocs": [
60
+ {
61
+ "class": "DatasetsListBloc",
62
+ "title": "My awesome title",
63
+ "subtitle": "more information",
64
+ "datasets": [{"id": str(datasets[2].id)}],
65
+ }
66
+ ],
67
+ },
68
+ )
69
+ self.assert200(response)
70
+
71
+ self.assertEqual("DatasetsListBloc", response.json["blocs"][0]["class"])
72
+ self.assertEqual("My awesome title", response.json["blocs"][0]["title"])
73
+ self.assertEqual("more information", response.json["blocs"][0]["subtitle"])
74
+ self.assertEqual(len(response.json["blocs"][0]["datasets"]), 1)
75
+ self.assertEqual(str(datasets[2].id), response.json["blocs"][0]["datasets"][0]["id"])
udata/core/site/api.py CHANGED
@@ -1,13 +1,12 @@
1
- from bson import ObjectId
2
1
  from flask import current_app, json, make_response, redirect, request, url_for
3
2
 
4
- from udata.api import API, api, fields
3
+ from udata.api import API, api
4
+ from udata.api_fields import patch
5
5
  from udata.auth import admin_permission
6
6
  from udata.core import csv
7
7
  from udata.core.dataservices.csv import DataserviceCsvAdapter
8
8
  from udata.core.dataservices.models import Dataservice
9
9
  from udata.core.dataset.api import DatasetApiParser
10
- from udata.core.dataset.api_fields import dataset_fields
11
10
  from udata.core.dataset.csv import ResourcesCsvAdapter
12
11
  from udata.core.dataset.search import DatasetSearch
13
12
  from udata.core.dataset.tasks import get_queryset as get_csv_queryset
@@ -22,74 +21,28 @@ from udata.models import Dataset, Reuse
22
21
  from udata.rdf import CONTEXT, RDF_EXTENSIONS, graph_response, negociate_content
23
22
  from udata.utils import multi_to_dict
24
23
 
25
- from .models import current_site
24
+ from .models import Site, current_site
26
25
  from .rdf import build_catalog
27
26
 
28
- site_fields = api.model(
29
- "Site",
30
- {
31
- "id": fields.String(description="The Site unique identifier", required=True),
32
- "title": fields.String(description="The site display title", required=True),
33
- "metrics": fields.Raw(
34
- attribute=lambda o: o.get_metrics(), description="The associated metrics", default={}
35
- ),
36
- },
37
- )
38
-
39
27
 
40
28
  @api.route("/site/", endpoint="site")
41
29
  class SiteAPI(API):
42
30
  @api.doc(id="get_site")
43
- @api.marshal_with(site_fields)
31
+ @api.marshal_with(Site.__read_fields__)
44
32
  def get(self):
45
33
  """Site-wide variables"""
46
34
  return current_site
47
35
 
48
-
49
- @api.route("/site/home/datasets/", endpoint="home_datasets")
50
- class SiteHomeDatasetsAPI(API):
51
- @api.doc("get_home_datasets")
52
- # @api.secure(admin_permission)
53
- @api.marshal_list_with(dataset_fields)
54
- def get(self):
55
- """List homepage datasets"""
56
- return current_site.settings.home_datasets
57
-
58
36
  @api.secure(admin_permission)
59
- @api.doc("set_home_datasets")
60
- @api.expect(([str], "Dataset IDs to put in homepage"))
61
- @api.marshal_list_with(dataset_fields)
62
- def put(self):
63
- """Set the homepage datasets editorial selection"""
64
- if not isinstance(request.json, list):
65
- api.abort(400, "Expect a list of dataset IDs")
66
- ids = [ObjectId(id) for id in request.json]
67
- current_site.settings.home_datasets = Dataset.objects.bulk_list(ids)
68
- current_site.save()
69
- return current_site.settings.home_datasets
70
-
37
+ @api.doc(id="set_site")
38
+ @api.expect(Site.__write_fields__)
39
+ @api.marshal_with(Site.__read_fields__)
40
+ def patch(self):
41
+ patch(current_site, request)
71
42
 
72
- @api.route("/site/home/reuses/", endpoint="home_reuses")
73
- class SiteHomeReusesAPI(API):
74
- @api.doc("get_home_reuses")
75
- @api.secure(admin_permission)
76
- @api.marshal_list_with(Reuse.__read_fields__)
77
- def get(self):
78
- """List homepage featured reuses"""
79
- return current_site.settings.home_reuses
80
-
81
- @api.secure(admin_permission)
82
- @api.doc("set_home_reuses")
83
- @api.expect(([str], "Reuse IDs to put in homepage"))
84
- @api.marshal_list_with(Reuse.__read_fields__)
85
- def put(self):
86
- """Set the homepage reuses editorial selection"""
87
- if not isinstance(request.json, list):
88
- api.abort(400, "Expect a list of reuse IDs")
89
- ids = [ObjectId(id) for id in request.json]
90
- current_site.settings.home_reuses = Reuse.objects.bulk_list(ids)
91
43
  current_site.save()
92
- return current_site.settings.home_reuses
44
+ current_site.reload()
45
+ return current_site
93
46
 
94
47
 
95
48
  @api.route("/site/data.<format>", endpoint="site_dataportal")
udata/core/site/models.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from flask import current_app, g
2
2
  from werkzeug.local import LocalProxy
3
3
 
4
+ from udata.api_fields import field, generate_fields
4
5
  from udata.core.dataservices.models import Dataservice
5
6
  from udata.core.dataset.models import Dataset
6
7
  from udata.core.metrics.helpers import get_metrics_for_model, get_stock_metrics
@@ -19,14 +20,18 @@ class SiteSettings(db.EmbeddedDocument):
19
20
  home_reuses = db.ListField(db.ReferenceField(Reuse))
20
21
 
21
22
 
23
+ @generate_fields()
22
24
  class Site(WithMetrics, db.Document):
23
- id = db.StringField(primary_key=True)
24
- title = db.StringField(required=True)
25
- keywords = db.ListField(db.StringField())
26
- feed_size = db.IntField(required=True, default=DEFAULT_FEED_SIZE)
25
+ id = field(db.StringField(primary_key=True), readonly=True)
26
+ title = field(db.StringField(required=True), description="The site display title")
27
+ keywords = field(db.ListField(db.StringField()))
28
+ feed_size = field(db.IntField(required=True, default=DEFAULT_FEED_SIZE))
27
29
  configs = db.DictField()
28
30
  themes = db.DictField()
29
31
  settings = db.EmbeddedDocumentField(SiteSettings, default=SiteSettings)
32
+ datasets_page = field(db.ReferenceField("Page"), attribute="datasets_page.id")
33
+ reuses_page = field(db.ReferenceField("Page"), attribute="reuses_page.id")
34
+ dataservices_page = field(db.ReferenceField("Page"), attribute="dataservices_page.id")
30
35
 
31
36
  __metrics_keys__ = [
32
37
  "max_dataset_followers",
udata/models/__init__.py CHANGED
@@ -23,6 +23,7 @@ from udata.core.tags.models import * # noqa
23
23
  from udata.core.spam.models import * # noqa
24
24
  from udata.core.reports.models import * # noqa
25
25
  from udata.core.dataservices.models import * # noqa
26
+ from udata.core.pages.models import * # noqa
26
27
 
27
28
  from udata.features.transfer.models import * # noqa
28
29
  from udata.features.territories.models import * # noqa
udata/routing.py CHANGED
@@ -126,6 +126,10 @@ class DatasetConverter(ModelConverter):
126
126
  model = models.Dataset
127
127
 
128
128
 
129
+ class PageConverter(ModelConverter):
130
+ model = models.Page
131
+
132
+
129
133
  class DatasetWithoutResourcesConverter(ModelConverter):
130
134
  model = models.Dataset
131
135
 
@@ -250,6 +254,7 @@ def init_app(app):
250
254
  app.url_map.converters["reuse"] = ReuseConverter
251
255
  app.url_map.converters["user"] = UserConverter
252
256
  app.url_map.converters["topic"] = TopicConverter
257
+ app.url_map.converters["page"] = PageConverter
253
258
  app.url_map.converters["post"] = PostConverter
254
259
  app.url_map.converters["territory"] = TerritoryConverter
255
260
  app.url_map.converters["contact_point"] = ContactPointConverter