udata 10.8.4.dev37423__py2.py3-none-any.whl → 10.8.4.dev37435__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/__init__.py +5 -1
- udata/api_fields.py +94 -11
- udata/core/dataservices/models.py +2 -2
- udata/core/pages/api.py +54 -0
- udata/core/pages/factories.py +10 -0
- udata/core/pages/models.py +103 -0
- udata/core/pages/permissions.py +7 -0
- udata/core/pages/tests/test_api.py +75 -0
- udata/core/site/api.py +11 -58
- udata/core/site/models.py +9 -4
- udata/models/__init__.py +1 -0
- udata/routing.py +5 -0
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.0f04e49a40a0a381bcce.js} +3 -3
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.0f04e49a40a0a381bcce.js.map} +1 -1
- udata/static/chunks/{19.f03a102365af4315f9db.js → 19.0586efa786ebf09fb288.js} +3 -3
- udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.0586efa786ebf09fb288.js.map} +1 -1
- udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.5660483641193b7f8295.js} +3 -3
- udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.5660483641193b7f8295.js.map} +1 -1
- udata/static/chunks/{6.d663709d877baa44a71e.js → 6.30dce49d17db07600b06.js} +3 -3
- udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.30dce49d17db07600b06.js.map} +1 -1
- udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.b966402f5d680d4bdf4a.js} +2 -2
- udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.b966402f5d680d4bdf4a.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/tests/site/test_site_api.py +20 -42
- udata/tests/test_api_fields.py +12 -14
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.dist-info}/METADATA +1 -1
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.dist-info}/RECORD +32 -27
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.dist-info}/LICENSE +0 -0
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.dist-info}/WHEEL +0 -0
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.dist-info}/entry_points.txt +0 -0
- {udata-10.8.4.dev37423.dist-info → udata-10.8.4.dev37435.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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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=
|
|
267
|
+
nested_fields=dataset_ref_fields,
|
|
268
268
|
)
|
|
269
269
|
),
|
|
270
270
|
filterable={
|
udata/core/pages/api.py
ADDED
|
@@ -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,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,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
|
|
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(
|
|
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("
|
|
60
|
-
@api.expect(
|
|
61
|
-
@api.
|
|
62
|
-
def
|
|
63
|
-
|
|
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
|
-
|
|
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
|