udata 13.0.1.dev12__py3-none-any.whl → 14.4.1.dev7__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 +2 -8
- udata/api_fields.py +35 -4
- udata/app.py +30 -50
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +8 -6
- udata/auth/views.py +6 -3
- udata/commands/__init__.py +2 -14
- udata/commands/db.py +13 -25
- udata/commands/info.py +0 -16
- udata/commands/serve.py +3 -11
- udata/commands/tests/test_fixtures.py +9 -9
- udata/core/access_type/api.py +1 -1
- udata/core/access_type/constants.py +12 -8
- udata/core/activity/api.py +5 -6
- udata/core/avatars/api.py +43 -0
- udata/core/avatars/test_avatar_api.py +30 -0
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/models.py +15 -3
- udata/core/dataservices/tasks.py +7 -0
- udata/core/dataset/api.py +2 -0
- udata/core/dataset/models.py +2 -2
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/tasks.py +50 -10
- udata/core/discussions/models.py +1 -0
- udata/core/metrics/__init__.py +0 -6
- udata/core/organization/api.py +8 -5
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +9 -1
- udata/core/organization/notifications.py +84 -0
- udata/core/organization/permissions.py +1 -1
- udata/core/organization/tasks.py +3 -0
- udata/core/pages/tests/test_api.py +32 -0
- udata/core/post/api.py +24 -69
- udata/core/post/models.py +84 -16
- udata/core/post/tests/test_api.py +24 -1
- udata/core/reports/api.py +18 -0
- udata/core/reports/models.py +42 -2
- udata/core/reuse/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/site/models.py +2 -6
- udata/core/spatial/commands.py +2 -4
- udata/core/spatial/forms.py +2 -2
- udata/core/spatial/models.py +0 -10
- udata/core/spatial/tests/test_api.py +1 -36
- udata/core/user/models.py +15 -2
- udata/cors.py +2 -5
- udata/db/migrations.py +279 -0
- udata/features/notifications/api.py +7 -18
- udata/features/notifications/models.py +56 -0
- udata/features/notifications/tasks.py +25 -0
- udata/flask_mongoengine/engine.py +0 -4
- udata/frontend/__init__.py +3 -122
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +24 -9
- udata/harvest/api.py +30 -22
- udata/harvest/backends/__init__.py +21 -9
- udata/harvest/backends/base.py +29 -3
- udata/harvest/backends/ckan/harvesters.py +13 -2
- udata/harvest/backends/dcat.py +3 -0
- udata/harvest/backends/maaf.py +1 -0
- udata/harvest/commands.py +39 -4
- udata/harvest/filters.py +17 -6
- udata/harvest/forms.py +9 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tasks.py +3 -5
- udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
- udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
- udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
- udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
- udata/harvest/tests/dcat/udata.xml +6 -6
- udata/harvest/tests/factories.py +1 -1
- udata/harvest/tests/test_actions.py +63 -8
- udata/harvest/tests/test_api.py +278 -123
- udata/harvest/tests/test_base_backend.py +88 -1
- udata/harvest/tests/test_dcat_backend.py +60 -13
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +11 -273
- udata/mail.py +5 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/models/__init__.py +0 -8
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +45 -6
- udata/routing.py +2 -10
- udata/sentry.py +4 -10
- udata/settings.py +23 -17
- udata/tasks.py +4 -3
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +28 -12
- udata/tests/api/__init__.py +108 -21
- udata/tests/api/test_activities_api.py +36 -0
- udata/tests/api/test_auth_api.py +121 -95
- udata/tests/api/test_base_api.py +7 -4
- udata/tests/api/test_dataservices_api.py +29 -1
- udata/tests/api/test_datasets_api.py +45 -21
- udata/tests/api/test_organizations_api.py +192 -197
- udata/tests/api/test_reports_api.py +157 -0
- udata/tests/api/test_reuses_api.py +147 -147
- udata/tests/api/test_security_api.py +12 -12
- udata/tests/api/test_swagger.py +4 -4
- udata/tests/api/test_tags_api.py +8 -8
- udata/tests/api/test_user_api.py +13 -1
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/apiv2/test_topics.py +1 -1
- udata/tests/cli/test_cli_base.py +8 -9
- udata/tests/dataset/test_dataset_commands.py +4 -4
- udata/tests/dataset/test_dataset_model.py +66 -26
- udata/tests/dataset/test_dataset_rdf.py +99 -5
- udata/tests/dataset/test_resource_preview.py +0 -1
- udata/tests/frontend/test_auth.py +24 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +37 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_cors.py +1 -1
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_migrations.py +181 -481
- udata/tests/test_notifications.py +15 -57
- udata/tests/test_notifications_task.py +43 -0
- udata/tests/test_owned.py +81 -1
- udata/tests/test_storages.py +25 -19
- udata/tests/test_topics.py +77 -61
- udata/tests/test_uris.py +33 -0
- udata/tests/workers/test_jobs_commands.py +23 -23
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +187 -108
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +187 -108
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +187 -108
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +188 -109
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +187 -108
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +187 -108
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +187 -108
- udata/translations/udata.pot +215 -106
- udata/uris.py +0 -2
- udata/utils.py +5 -0
- udata-14.4.1.dev7.dist-info/METADATA +109 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
- udata/core/followers/views.py +0 -15
- udata/core/post/forms.py +0 -30
- udata/entrypoints.py +0 -93
- udata/features/identicon/__init__.py +0 -0
- udata/features/identicon/api.py +0 -13
- udata/features/identicon/backends.py +0 -131
- udata/features/identicon/tests/__init__.py +0 -0
- udata/features/identicon/tests/test_backends.py +0 -18
- udata/features/territories/__init__.py +0 -49
- udata/features/territories/api.py +0 -25
- udata/features/territories/models.py +0 -51
- udata/flask_mongoengine/json.py +0 -38
- udata/migrations/__init__.py +0 -367
- udata/templates/mail/base.html +0 -105
- udata/templates/mail/base.txt +0 -6
- udata/templates/mail/button.html +0 -3
- udata/templates/mail/layouts/1-column.html +0 -19
- udata/templates/mail/layouts/2-columns.html +0 -20
- udata/templates/mail/layouts/center-panel.html +0 -16
- udata/tests/cli/test_db_cli.py +0 -68
- udata/tests/features/territories/__init__.py +0 -20
- udata/tests/features/territories/test_territories_api.py +0 -185
- udata/tests/frontend/test_hooks.py +0 -149
- udata-13.0.1.dev12.dist-info/METADATA +0 -133
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
- {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/api/__init__.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import inspect
|
|
2
1
|
import logging
|
|
3
2
|
import urllib.parse
|
|
4
3
|
from functools import wraps
|
|
5
|
-
from importlib import import_module
|
|
6
4
|
|
|
7
5
|
import mongoengine
|
|
8
6
|
from flask import (
|
|
@@ -19,7 +17,7 @@ from flask_restx import Api, Resource
|
|
|
19
17
|
from flask_restx.reqparse import RequestParser
|
|
20
18
|
from flask_storage import UnauthorizedFileType
|
|
21
19
|
|
|
22
|
-
from udata import
|
|
20
|
+
from udata import tracking
|
|
23
21
|
from udata.app import csrf
|
|
24
22
|
from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
|
|
25
23
|
from udata.i18n import get_locale
|
|
@@ -358,13 +356,9 @@ def init_app(app):
|
|
|
358
356
|
import udata.core.contact_point.api # noqa
|
|
359
357
|
import udata.features.transfer.api # noqa
|
|
360
358
|
import udata.features.notifications.api # noqa
|
|
361
|
-
import udata.
|
|
362
|
-
import udata.features.territories.api # noqa
|
|
359
|
+
import udata.core.avatars.api # noqa
|
|
363
360
|
import udata.harvest.api # noqa
|
|
364
361
|
|
|
365
|
-
for module in entrypoints.get_enabled("udata.apis", app).values():
|
|
366
|
-
module if inspect.ismodule(module) else import_module(module)
|
|
367
|
-
|
|
368
362
|
# api.init_app(app)
|
|
369
363
|
app.register_blueprint(apiv1_blueprint)
|
|
370
364
|
app.register_blueprint(apiv2_blueprint)
|
udata/api_fields.py
CHANGED
|
@@ -29,7 +29,7 @@ import flask_restx.fields as restx_fields
|
|
|
29
29
|
import mongoengine
|
|
30
30
|
import mongoengine.fields as mongo_fields
|
|
31
31
|
from bson import DBRef, ObjectId
|
|
32
|
-
from flask import Request
|
|
32
|
+
from flask import Request, request
|
|
33
33
|
from flask_restx import marshal
|
|
34
34
|
from flask_restx.inputs import boolean
|
|
35
35
|
from flask_restx.reqparse import RequestParser
|
|
@@ -55,8 +55,8 @@ classes_by_parents = {}
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class GenericField(restx_fields.Raw):
|
|
58
|
-
def __init__(self, fields_by_type):
|
|
59
|
-
super().__init__(
|
|
58
|
+
def __init__(self, fields_by_type, **kwargs):
|
|
59
|
+
super(GenericField, self).__init__(**kwargs)
|
|
60
60
|
self.default = None
|
|
61
61
|
self.fields_by_type = fields_by_type
|
|
62
62
|
|
|
@@ -230,6 +230,23 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
|
|
|
230
230
|
|
|
231
231
|
write_params["description"] = "ID of the reference"
|
|
232
232
|
constructor_write = restx_fields.String
|
|
233
|
+
elif isinstance(field, mongo_fields.GenericEmbeddedDocumentField):
|
|
234
|
+
generic_fields = {
|
|
235
|
+
cls.__name__: convert_db_to_field(
|
|
236
|
+
f"{key}.{cls.__name__}",
|
|
237
|
+
# Instead of having GenericEmbeddedDocumentField() we'll create fields for each
|
|
238
|
+
# of the subclasses with EmbededdDocumentField(MembershipRequestNotificationDetails)…
|
|
239
|
+
mongoengine.fields.EmbeddedDocumentField(cls),
|
|
240
|
+
info,
|
|
241
|
+
)
|
|
242
|
+
for cls in field.choices
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def constructor_read(**kwargs):
|
|
246
|
+
return GenericField({k: v[0].model for k, v in generic_fields.items()}, **kwargs)
|
|
247
|
+
|
|
248
|
+
def constructor_write(**kwargs):
|
|
249
|
+
return GenericField({k: v[1].model for k, v in generic_fields.items()}, **kwargs)
|
|
233
250
|
elif isinstance(field, mongo_fields.EmbeddedDocumentField):
|
|
234
251
|
nested_fields = info.get("nested_fields")
|
|
235
252
|
if nested_fields is not None:
|
|
@@ -322,12 +339,12 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
322
339
|
write_fields: dict = {}
|
|
323
340
|
ref_fields: dict = {}
|
|
324
341
|
sortables: list = kwargs.get("additional_sorts", [])
|
|
342
|
+
default_sort: list = kwargs.get("default_sort", None)
|
|
325
343
|
|
|
326
344
|
filterables: list[dict] = kwargs.get("standalone_filters", [])
|
|
327
345
|
nested_filters: dict[str, dict] = get_fields_with_nested_filters(
|
|
328
346
|
kwargs.get("nested_filters", {})
|
|
329
347
|
)
|
|
330
|
-
|
|
331
348
|
if issubclass(cls, db.Document) or issubclass(cls, db.DynamicDocument):
|
|
332
349
|
read_fields["id"] = restx_fields.String(required=True, readonly=True)
|
|
333
350
|
|
|
@@ -472,6 +489,7 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
472
489
|
type=str,
|
|
473
490
|
location="args",
|
|
474
491
|
choices=choices,
|
|
492
|
+
default=default_sort,
|
|
475
493
|
help="The field (and direction) on which sorting apply",
|
|
476
494
|
)
|
|
477
495
|
|
|
@@ -511,6 +529,9 @@ def generate_fields(**kwargs) -> Callable:
|
|
|
511
529
|
phrase_query: str = " ".join([f'"{elem}"' for elem in args["q"].split(" ")])
|
|
512
530
|
base_query = base_query.search_text(phrase_query)
|
|
513
531
|
|
|
532
|
+
if "sort" not in request.args:
|
|
533
|
+
base_query = base_query.order_by("$text_score")
|
|
534
|
+
|
|
514
535
|
for filterable in filterables:
|
|
515
536
|
# If it's from an `nested_filter`, use the custom label instead of the key,
|
|
516
537
|
# eg use `organization_badge` instead of `organization.badges` which is
|
|
@@ -652,6 +673,9 @@ def patch(obj, request) -> type:
|
|
|
652
673
|
model_attribute = getattr(obj.__class__, key)
|
|
653
674
|
info = getattr(model_attribute, "__additional_field_info__", {})
|
|
654
675
|
|
|
676
|
+
if value == "" and isinstance(model_attribute, mongo_fields.StringField):
|
|
677
|
+
value = None
|
|
678
|
+
|
|
655
679
|
if hasattr(model_attribute, "from_input"):
|
|
656
680
|
value = model_attribute.from_input(value)
|
|
657
681
|
elif isinstance(model_attribute, mongoengine.fields.ListField) and isinstance(
|
|
@@ -677,6 +701,13 @@ def patch(obj, request) -> type:
|
|
|
677
701
|
value["id"],
|
|
678
702
|
document_type=db.resolve_model(value["class"]),
|
|
679
703
|
)
|
|
704
|
+
elif value and isinstance(
|
|
705
|
+
model_attribute,
|
|
706
|
+
mongoengine.fields.GenericEmbeddedDocumentField,
|
|
707
|
+
):
|
|
708
|
+
generic_key = info.get("generic_key", DEFAULT_GENERIC_KEY)
|
|
709
|
+
embedded_field = classes_by_names[value[generic_key]]
|
|
710
|
+
value = patch(embedded_field(), value)
|
|
680
711
|
elif value and isinstance(
|
|
681
712
|
model_attribute,
|
|
682
713
|
mongoengine.fields.EmbeddedDocumentField,
|
udata/app.py
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import importlib
|
|
3
1
|
import logging
|
|
4
2
|
import os
|
|
5
3
|
import types
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from importlib.metadata import entry_points
|
|
6
6
|
from os.path import abspath, dirname, exists, isfile, join
|
|
7
7
|
|
|
8
8
|
import bson
|
|
9
|
+
from bson import json_util
|
|
9
10
|
from flask import Blueprint as BaseBlueprint
|
|
10
11
|
from flask import (
|
|
11
12
|
Flask,
|
|
12
13
|
abort,
|
|
13
14
|
g,
|
|
14
|
-
json,
|
|
15
15
|
jsonify,
|
|
16
16
|
make_response,
|
|
17
17
|
render_template,
|
|
18
18
|
request,
|
|
19
19
|
send_from_directory,
|
|
20
20
|
)
|
|
21
|
+
from flask.json.provider import DefaultJSONProvider
|
|
21
22
|
from flask_caching import Cache
|
|
22
23
|
from flask_wtf.csrf import CSRFProtect
|
|
23
|
-
from
|
|
24
|
+
from mongoengine import EmbeddedDocument
|
|
25
|
+
from mongoengine.base import BaseDocument
|
|
24
26
|
from werkzeug.exceptions import NotFound
|
|
25
27
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
26
28
|
|
|
27
|
-
from udata import cors
|
|
29
|
+
from udata import cors
|
|
28
30
|
|
|
29
31
|
APP_NAME = __name__.split(".")[0]
|
|
30
32
|
ROOT_DIR = abspath(join(dirname(__file__)))
|
|
@@ -108,9 +110,9 @@ class Blueprint(BaseBlueprint):
|
|
|
108
110
|
return wrapper
|
|
109
111
|
|
|
110
112
|
|
|
111
|
-
class
|
|
113
|
+
class UdataJsonProvider(DefaultJSONProvider):
|
|
112
114
|
"""
|
|
113
|
-
A
|
|
115
|
+
A JSONProvider subclass to encode unsupported types:
|
|
114
116
|
|
|
115
117
|
- ObjectId
|
|
116
118
|
- datetime
|
|
@@ -120,21 +122,16 @@ class UDataJsonEncoder(json.JSONEncoder):
|
|
|
120
122
|
Ensure an app context is always present.
|
|
121
123
|
"""
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
@staticmethod
|
|
126
|
+
def default(obj):
|
|
127
|
+
if isinstance(obj, BaseDocument) or isinstance(obj, EmbeddedDocument):
|
|
128
|
+
return json_util._json_convert(obj.to_mongo())
|
|
129
|
+
elif isinstance(obj, bson.ObjectId):
|
|
125
130
|
return str(obj)
|
|
126
|
-
elif isinstance(obj,
|
|
127
|
-
return str(obj)
|
|
128
|
-
elif isinstance(obj, datetime.datetime):
|
|
131
|
+
elif isinstance(obj, datetime):
|
|
129
132
|
return obj.isoformat()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
elif hasattr(obj, "serialize"):
|
|
133
|
-
return obj.serialize()
|
|
134
|
-
# Serialize Raw data for Document and EmbeddedDocument.
|
|
135
|
-
elif hasattr(obj, "_data"):
|
|
136
|
-
return obj._data
|
|
137
|
-
return super(UDataJsonEncoder, self).default(obj)
|
|
133
|
+
|
|
134
|
+
return super(UdataJsonProvider, UdataJsonProvider).default(obj)
|
|
138
135
|
|
|
139
136
|
|
|
140
137
|
# These loggers are very verbose
|
|
@@ -148,8 +145,6 @@ def init_logging(app):
|
|
|
148
145
|
debug = app.debug or app.config.get("TESTING")
|
|
149
146
|
log_level = logging.DEBUG if debug else logging.WARNING
|
|
150
147
|
app.logger.setLevel(log_level)
|
|
151
|
-
for name in entrypoints.get_roots(): # Entrypoints loggers
|
|
152
|
-
logging.getLogger(name).setLevel(log_level)
|
|
153
148
|
for logger in VERBOSE_LOGGERS:
|
|
154
149
|
logging.getLogger(logger).setLevel(logging.WARNING)
|
|
155
150
|
return app
|
|
@@ -168,24 +163,11 @@ def create_app(config="udata.settings.Defaults", override=None, init_logging=ini
|
|
|
168
163
|
if override:
|
|
169
164
|
app.config.from_object(override)
|
|
170
165
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if pkg == "udata":
|
|
174
|
-
continue # Defaults are already loaded
|
|
175
|
-
module = "{}.settings".format(pkg)
|
|
176
|
-
try:
|
|
177
|
-
settings = importlib.import_module(module)
|
|
178
|
-
except ImportError:
|
|
179
|
-
continue
|
|
180
|
-
for key, default in settings.__dict__.items():
|
|
181
|
-
if key.startswith("__"):
|
|
182
|
-
continue
|
|
183
|
-
app.config.setdefault(key, default)
|
|
184
|
-
|
|
185
|
-
app.json_encoder = UDataJsonEncoder
|
|
166
|
+
app.json_provider_class = UdataJsonProvider
|
|
167
|
+
app.json = app.json_provider_class(app)
|
|
186
168
|
|
|
187
169
|
# `ujson` doesn't support `cls` parameter https://github.com/ultrajson/ultrajson/issues/124
|
|
188
|
-
app.config["RESTX_JSON"] = {"
|
|
170
|
+
app.config["RESTX_JSON"] = {"default": UdataJsonProvider.default}
|
|
189
171
|
|
|
190
172
|
app.debug = app.config["DEBUG"] and not app.config["TESTING"]
|
|
191
173
|
|
|
@@ -200,12 +182,21 @@ def create_app(config="udata.settings.Defaults", override=None, init_logging=ini
|
|
|
200
182
|
def standalone(app):
|
|
201
183
|
"""Factory for an all in one application"""
|
|
202
184
|
from udata import api, core, frontend
|
|
185
|
+
from udata.features import notifications
|
|
203
186
|
|
|
204
187
|
core.init_app(app)
|
|
205
188
|
frontend.init_app(app)
|
|
206
189
|
api.init_app(app)
|
|
190
|
+
notifications.init_app(app)
|
|
191
|
+
|
|
192
|
+
eps = entry_points(group="udata.plugins")
|
|
193
|
+
for ep in eps:
|
|
194
|
+
plugin_module = ep.load()
|
|
207
195
|
|
|
208
|
-
|
|
196
|
+
if hasattr(plugin_module, "init_app"):
|
|
197
|
+
plugin_module.init_app(app)
|
|
198
|
+
else:
|
|
199
|
+
log.error(f"Plugin {ep.name} ({ep.value}) doesn't expose an `init_app()` function.")
|
|
209
200
|
|
|
210
201
|
return app
|
|
211
202
|
|
|
@@ -215,7 +206,6 @@ def register_extensions(app):
|
|
|
215
206
|
auth,
|
|
216
207
|
i18n,
|
|
217
208
|
mail,
|
|
218
|
-
models,
|
|
219
209
|
mongo,
|
|
220
210
|
notifications, # noqa
|
|
221
211
|
routing,
|
|
@@ -229,7 +219,6 @@ def register_extensions(app):
|
|
|
229
219
|
tasks.init_app(app)
|
|
230
220
|
i18n.init_app(app)
|
|
231
221
|
mongo.init_app(app)
|
|
232
|
-
models.init_app(app)
|
|
233
222
|
routing.init_app(app)
|
|
234
223
|
auth.init_app(app)
|
|
235
224
|
cache.init_app(app)
|
|
@@ -278,12 +267,3 @@ def page_not_found(e: NotFound):
|
|
|
278
267
|
return render_template("404.html", homepage_url=homepage_url()), 404
|
|
279
268
|
|
|
280
269
|
return jsonify({"error": e.description, "status": 404}), 404
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def register_features(app):
|
|
284
|
-
from udata.features import notifications
|
|
285
|
-
|
|
286
|
-
notifications.init_app(app)
|
|
287
|
-
|
|
288
|
-
for ep in entrypoints.get_enabled("udata.plugins", app).values():
|
|
289
|
-
ep.init_app(app)
|
udata/auth/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import typing as t
|
|
2
3
|
|
|
3
4
|
from flask import render_template
|
|
4
5
|
from flask_principal import Permission as BasePermission
|
|
@@ -10,6 +11,7 @@ from flask_security import Security as Security
|
|
|
10
11
|
from flask_security import current_user as current_user
|
|
11
12
|
from flask_security import login_required as login_required
|
|
12
13
|
from flask_security import login_user as login_user
|
|
14
|
+
from flask_security import mail_util
|
|
13
15
|
|
|
14
16
|
from . import mails
|
|
15
17
|
|
|
@@ -24,9 +26,6 @@ def render_security_template(template_name_or_list, **kwargs):
|
|
|
24
26
|
return render_template(template_name_or_list, **kwargs)
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
security = Security()
|
|
28
|
-
|
|
29
|
-
|
|
30
29
|
class Permission(BasePermission):
|
|
31
30
|
def __init__(self, *needs):
|
|
32
31
|
"""Let administrator bypass all permissions"""
|
|
@@ -36,6 +35,24 @@ class Permission(BasePermission):
|
|
|
36
35
|
admin_permission = Permission()
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
class NoopMailUtil(mail_util.MailUtil):
|
|
39
|
+
def send_mail(
|
|
40
|
+
self,
|
|
41
|
+
template: str,
|
|
42
|
+
subject: str,
|
|
43
|
+
recipient: str,
|
|
44
|
+
sender: t.Union[str, tuple],
|
|
45
|
+
body: str,
|
|
46
|
+
html: t.Optional[str],
|
|
47
|
+
**kwargs: t.Any,
|
|
48
|
+
) -> None:
|
|
49
|
+
log.debug(f"Sending mail {subject} to {recipient}")
|
|
50
|
+
log.debug(body)
|
|
51
|
+
log.debug(html)
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
39
56
|
def init_app(app):
|
|
40
57
|
from udata.models import datastore
|
|
41
58
|
|
|
@@ -61,19 +78,25 @@ def init_app(app):
|
|
|
61
78
|
app.config["CDATA_BASE_URL"] + "?flash=confirm_error",
|
|
62
79
|
)
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
# Same logic as in our own mail system :DisableMail
|
|
82
|
+
debug = app.config.get("DEBUG", False)
|
|
83
|
+
send_mail = app.config.get("SEND_MAIL", not debug)
|
|
84
|
+
mail_util_cls = mail_util.MailUtil if send_mail else NoopMailUtil
|
|
85
|
+
|
|
86
|
+
security = Security(
|
|
66
87
|
datastore,
|
|
67
88
|
register_blueprint=False,
|
|
68
89
|
render_template=render_security_template,
|
|
69
90
|
login_form=ExtendedLoginForm,
|
|
70
|
-
confirm_register_form=ExtendedRegisterForm,
|
|
71
91
|
register_form=ExtendedRegisterForm,
|
|
72
92
|
reset_password_form=ExtendedResetPasswordForm,
|
|
73
93
|
forgot_password_form=ExtendedForgotPasswordForm,
|
|
74
94
|
password_util_cls=UdataPasswordUtil,
|
|
95
|
+
mail_util_cls=mail_util_cls,
|
|
75
96
|
)
|
|
76
97
|
|
|
98
|
+
security.init_app(app, datastore, register_blueprint=False)
|
|
99
|
+
|
|
77
100
|
security_bp = create_security_blueprint(app, app.extensions["security"], "security_blueprint")
|
|
78
101
|
|
|
79
102
|
app.register_blueprint(security_bp)
|
udata/auth/forms.py
CHANGED
|
@@ -8,7 +8,7 @@ from flask_security.forms import (
|
|
|
8
8
|
ForgotPasswordForm,
|
|
9
9
|
Form,
|
|
10
10
|
LoginForm,
|
|
11
|
-
|
|
11
|
+
RegisterFormV2,
|
|
12
12
|
ResetPasswordForm,
|
|
13
13
|
)
|
|
14
14
|
|
|
@@ -31,7 +31,7 @@ class WithCaptcha:
|
|
|
31
31
|
return False
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
class ExtendedRegisterForm(WithCaptcha,
|
|
34
|
+
class ExtendedRegisterForm(WithCaptcha, RegisterFormV2):
|
|
35
35
|
first_name = fields.StringField(
|
|
36
36
|
_("First name"),
|
|
37
37
|
[
|
|
@@ -54,13 +54,15 @@ class ExtendedRegisterForm(WithCaptcha, RegisterForm):
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
def validate(self, **kwargs):
|
|
57
|
-
|
|
58
|
-
if not super().validate(**kwargs) or current_app.config.get("READ_ONLY_MODE"):
|
|
57
|
+
if current_app.config.get("READ_ONLY_MODE"):
|
|
59
58
|
return False
|
|
60
59
|
|
|
61
60
|
if not self.validate_captcha():
|
|
62
61
|
return False
|
|
63
62
|
|
|
63
|
+
if not super().validate(**kwargs):
|
|
64
|
+
return False
|
|
65
|
+
|
|
64
66
|
return True
|
|
65
67
|
|
|
66
68
|
|
|
@@ -91,10 +93,10 @@ class ExtendedResetPasswordForm(ResetPasswordForm):
|
|
|
91
93
|
|
|
92
94
|
class ExtendedForgotPasswordForm(WithCaptcha, ForgotPasswordForm):
|
|
93
95
|
def validate(self, **kwargs):
|
|
94
|
-
if not
|
|
96
|
+
if not self.validate_captcha():
|
|
95
97
|
return False
|
|
96
98
|
|
|
97
|
-
if not
|
|
99
|
+
if not super().validate(**kwargs):
|
|
98
100
|
return False
|
|
99
101
|
|
|
100
102
|
return True
|
udata/auth/views.py
CHANGED
|
@@ -84,6 +84,11 @@ def confirm_change_email(token):
|
|
|
84
84
|
if flash:
|
|
85
85
|
return redirect(homepage_url(flash=flash, flash_data=flash_data))
|
|
86
86
|
|
|
87
|
+
# Check if the new email is already taken by another user
|
|
88
|
+
existing_user = _datastore.find_user(email=new_email)
|
|
89
|
+
if existing_user and existing_user.id != user.id:
|
|
90
|
+
return redirect(homepage_url(flash="change_email_already_taken"))
|
|
91
|
+
|
|
87
92
|
if user != current_user:
|
|
88
93
|
logout_user()
|
|
89
94
|
login_user(user)
|
|
@@ -140,10 +145,8 @@ def create_security_blueprint(app, state, import_name):
|
|
|
140
145
|
This creates an I18nBlueprint to use as a base.
|
|
141
146
|
"""
|
|
142
147
|
bp = I18nBlueprint(
|
|
143
|
-
|
|
148
|
+
"security",
|
|
144
149
|
import_name,
|
|
145
|
-
url_prefix=state.url_prefix,
|
|
146
|
-
subdomain=state.subdomain,
|
|
147
150
|
template_folder="templates",
|
|
148
151
|
)
|
|
149
152
|
|
udata/commands/__init__.py
CHANGED
|
@@ -4,12 +4,10 @@ import sys
|
|
|
4
4
|
from glob import iglob
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
|
-
import pkg_resources
|
|
8
7
|
from flask.cli import FlaskGroup, ScriptInfo, shell_command
|
|
9
8
|
|
|
10
|
-
from udata import entrypoints
|
|
11
9
|
from udata.app import VERBOSE_LOGGERS, create_app, standalone
|
|
12
|
-
from udata.utils import safe_unicode
|
|
10
|
+
from udata.utils import get_udata_version, safe_unicode
|
|
13
11
|
|
|
14
12
|
log = logging.getLogger(__name__)
|
|
15
13
|
|
|
@@ -149,11 +147,6 @@ def init_logging(app):
|
|
|
149
147
|
logger.handlers = []
|
|
150
148
|
logger.addHandler(handler)
|
|
151
149
|
|
|
152
|
-
for name in entrypoints.get_roots(): # Entrypoints loggers
|
|
153
|
-
logger = logging.getLogger(name)
|
|
154
|
-
logger.setLevel(log_level)
|
|
155
|
-
logger.handlers = []
|
|
156
|
-
|
|
157
150
|
app.logger.setLevel(log_level)
|
|
158
151
|
app.logger.handlers = []
|
|
159
152
|
app.logger.addHandler(handler)
|
|
@@ -208,7 +201,6 @@ class UdataGroup(FlaskGroup):
|
|
|
208
201
|
Load udata commands from:
|
|
209
202
|
- `udata.commands.*` module
|
|
210
203
|
- known internal modules with commands
|
|
211
|
-
- plugins exporting a `udata.commands` entrypoint
|
|
212
204
|
"""
|
|
213
205
|
if self._udata_commands_loaded:
|
|
214
206
|
return
|
|
@@ -229,10 +221,6 @@ class UdataGroup(FlaskGroup):
|
|
|
229
221
|
except Exception as e:
|
|
230
222
|
error("Unable to import {0}".format(module), e)
|
|
231
223
|
|
|
232
|
-
# Load commands from entry points for enabled plugins
|
|
233
|
-
app = ctx.ensure_object(ScriptInfo).load_app()
|
|
234
|
-
entrypoints.get_enabled("udata.commands", app)
|
|
235
|
-
|
|
236
224
|
# Ensure loading happens once
|
|
237
225
|
self._udata_commands_loaded = False
|
|
238
226
|
|
|
@@ -253,7 +241,7 @@ class UdataGroup(FlaskGroup):
|
|
|
253
241
|
def print_version(ctx, param, value):
|
|
254
242
|
if not value or ctx.resilient_parsing:
|
|
255
243
|
return
|
|
256
|
-
click.echo(
|
|
244
|
+
click.echo(get_udata_version())
|
|
257
245
|
ctx.exit()
|
|
258
246
|
|
|
259
247
|
|
udata/commands/db.py
CHANGED
|
@@ -11,9 +11,9 @@ import click
|
|
|
11
11
|
import mongoengine
|
|
12
12
|
from bson import DBRef
|
|
13
13
|
|
|
14
|
-
from udata import migrations
|
|
15
14
|
from udata.commands import cli, cyan, echo, green, magenta, red, white, yellow
|
|
16
15
|
from udata.core.dataset.models import Dataset, Resource
|
|
16
|
+
from udata.db import migrations
|
|
17
17
|
from udata.mongo.document import get_all_models
|
|
18
18
|
|
|
19
19
|
# Date format used to for display
|
|
@@ -31,8 +31,7 @@ def grp():
|
|
|
31
31
|
def log_status(migration, status):
|
|
32
32
|
"""Properly display a migration status line"""
|
|
33
33
|
name = os.path.splitext(migration.filename)[0]
|
|
34
|
-
|
|
35
|
-
log.info("%s [%s]", "{:.<70}".format(display), status)
|
|
34
|
+
echo("{:.<70} [{}]".format(name + " ", status))
|
|
36
35
|
|
|
37
36
|
|
|
38
37
|
def status_label(record):
|
|
@@ -78,11 +77,6 @@ def migrate(record, dry_run=False):
|
|
|
78
77
|
log_status(migration, status)
|
|
79
78
|
try:
|
|
80
79
|
output = migration.execute(recordonly=record, dryrun=dry_run)
|
|
81
|
-
except migrations.RollbackError as re:
|
|
82
|
-
format_output(re.migrate_exc.output, False)
|
|
83
|
-
log_status(migration, red("Rollback"))
|
|
84
|
-
format_output(re.output, not re.exc)
|
|
85
|
-
success = False
|
|
86
80
|
except migrations.MigrationError as me:
|
|
87
81
|
format_output(me.output, False, traceback=me.traceback)
|
|
88
82
|
success = False
|
|
@@ -92,35 +86,29 @@ def migrate(record, dry_run=False):
|
|
|
92
86
|
|
|
93
87
|
|
|
94
88
|
@grp.command()
|
|
95
|
-
@click.argument("
|
|
96
|
-
|
|
97
|
-
def unrecord(plugin_or_specs, filename):
|
|
89
|
+
@click.argument("filename")
|
|
90
|
+
def unrecord(filename):
|
|
98
91
|
"""
|
|
99
92
|
Remove a database migration record.
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
A record can be expressed with the following syntaxes:
|
|
103
|
-
- plugin filename
|
|
104
|
-
- plugin filename.js
|
|
105
|
-
- plugin:filename
|
|
106
|
-
- plugin:fliename.js
|
|
94
|
+
FILENAME is the migration filename (e.g., 2024-01-01-my-migration.py)
|
|
107
95
|
"""
|
|
108
|
-
|
|
109
|
-
removed = migration.unrecord()
|
|
96
|
+
removed = migrations.unrecord(filename)
|
|
110
97
|
if removed:
|
|
111
|
-
|
|
98
|
+
echo("Removed migration {}".format(filename))
|
|
112
99
|
else:
|
|
113
|
-
|
|
100
|
+
echo(red("Migration not found {}".format(filename)))
|
|
114
101
|
|
|
115
102
|
|
|
116
103
|
@grp.command()
|
|
117
|
-
@click.argument("
|
|
118
|
-
|
|
119
|
-
def info(plugin_or_specs, filename):
|
|
104
|
+
@click.argument("filename")
|
|
105
|
+
def info(filename):
|
|
120
106
|
"""
|
|
121
107
|
Display detailed info about a migration
|
|
108
|
+
|
|
109
|
+
FILENAME is the migration filename (e.g., 2024-01-01-my-migration.py)
|
|
122
110
|
"""
|
|
123
|
-
migration = migrations.get(
|
|
111
|
+
migration = migrations.get(filename)
|
|
124
112
|
log_status(migration, status_label(migration.record))
|
|
125
113
|
try:
|
|
126
114
|
echo(migration.module.__doc__)
|
udata/commands/info.py
CHANGED
|
@@ -3,9 +3,7 @@ import logging
|
|
|
3
3
|
from click import echo
|
|
4
4
|
from flask import current_app
|
|
5
5
|
|
|
6
|
-
from udata import entrypoints
|
|
7
6
|
from udata.commands import KO, OK, cli, green, red, white
|
|
8
|
-
from udata.features.identicon.backends import get_config as avatar_config
|
|
9
7
|
|
|
10
8
|
log = logging.getLogger(__name__)
|
|
11
9
|
|
|
@@ -35,17 +33,3 @@ def config():
|
|
|
35
33
|
if key.startswith("__") or not key.isupper():
|
|
36
34
|
continue
|
|
37
35
|
echo("{0}: {1}".format(white(key), current_app.config[key]))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@grp.command()
|
|
41
|
-
def plugins():
|
|
42
|
-
"""Display some details about the local plugins"""
|
|
43
|
-
plugins = current_app.config["PLUGINS"]
|
|
44
|
-
for name, description in entrypoints.ENTRYPOINTS.items():
|
|
45
|
-
echo("{0} ({1})".format(white(description), name))
|
|
46
|
-
if name == "udata.avatars":
|
|
47
|
-
actives = [avatar_config("provider")]
|
|
48
|
-
else:
|
|
49
|
-
actives = plugins
|
|
50
|
-
for ep in sorted(entrypoints.iter_all(name), key=by_name):
|
|
51
|
-
echo("> {0}: {1}".format(ep.name, is_active(ep, actives)))
|
udata/commands/serve.py
CHANGED
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
from flask import current_app
|
|
6
|
-
from flask.cli import
|
|
6
|
+
from flask.cli import pass_script_info
|
|
7
7
|
from werkzeug.serving import run_simple
|
|
8
8
|
|
|
9
9
|
from udata.commands import cli
|
|
@@ -26,17 +26,11 @@ log = logging.getLogger(__name__)
|
|
|
26
26
|
default=None,
|
|
27
27
|
help="Enable or disable the debugger. By default the debugger is active if debug is enabled.",
|
|
28
28
|
)
|
|
29
|
-
@click.option(
|
|
30
|
-
"--eager-loading/--lazy-loader",
|
|
31
|
-
default=None,
|
|
32
|
-
help="Enable or disable eager loading. By default eager "
|
|
33
|
-
"loading is enabled if the reloader is disabled.",
|
|
34
|
-
)
|
|
35
29
|
@click.option(
|
|
36
30
|
"--with-threads/--without-threads", default=True, help="Enable or disable multithreading."
|
|
37
31
|
)
|
|
38
32
|
@pass_script_info
|
|
39
|
-
def serve(info, host, port, reload, debugger,
|
|
33
|
+
def serve(info, host, port, reload, debugger, with_threads):
|
|
40
34
|
"""
|
|
41
35
|
Runs a local udata development server.
|
|
42
36
|
|
|
@@ -62,10 +56,8 @@ def serve(info, host, port, reload, debugger, eager_loading, with_threads):
|
|
|
62
56
|
reload = bool(debug)
|
|
63
57
|
if debugger is None:
|
|
64
58
|
debugger = bool(debug)
|
|
65
|
-
if eager_loading is None:
|
|
66
|
-
eager_loading = not reload
|
|
67
59
|
|
|
68
|
-
app =
|
|
60
|
+
app = info.load_app()
|
|
69
61
|
|
|
70
62
|
settings = os.environ.get("UDATA_SETTINGS", os.path.join(os.getcwd(), "udata.cfg"))
|
|
71
63
|
extra_files = [settings]
|