udata 12.0.2.dev2__py3-none-any.whl → 12.0.2.dev4__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/flask_mongoengine/__init__.py +12 -0
- udata/flask_mongoengine/connection.py +147 -0
- udata/flask_mongoengine/document.py +59 -0
- udata/flask_mongoengine/engine.py +203 -0
- udata/flask_mongoengine/fields.py +99 -0
- udata/flask_mongoengine/json.py +38 -0
- udata/flask_mongoengine/model_form.py +23 -0
- udata/flask_mongoengine/pagination.py +166 -0
- udata/flask_mongoengine/wtf.py +39 -0
- udata/forms/__init__.py +2 -2
- udata/forms/fields.py +2 -2
- udata/mongo/document.py +1 -1
- udata/mongo/engine.py +2 -2
- udata/mongo/queryset.py +1 -1
- udata/mongo/slug_fields.py +1 -1
- udata/settings.py +0 -1
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/METADATA +1 -35
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/RECORD +22 -13
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/WHEEL +0 -0
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/entry_points.txt +0 -0
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/licenses/LICENSE +0 -0
- {udata-12.0.2.dev2.dist-info → udata-12.0.2.dev4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is a copy-paste of the components we use from flask-mongoengine.
|
|
3
|
+
|
|
4
|
+
The original flask-mongoengine project (https://github.com/MongoEngine/flask-mongoengine/)
|
|
5
|
+
is no longer maintained. While there are some forks like
|
|
6
|
+
https://github.com/idoshr/flask-mongoengine, they don't seem to work well and also
|
|
7
|
+
appear to be poorly maintained.
|
|
8
|
+
|
|
9
|
+
Since we want to be able to update Flask and flask-mongoengine is a blocking dependency
|
|
10
|
+
for the upgrade, we decided to internalize it. There isn't much code, and we should also
|
|
11
|
+
be able to simplify this implementation as we simplify udata over time.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import mongoengine
|
|
2
|
+
from pymongo import ReadPreference, uri_parser
|
|
3
|
+
|
|
4
|
+
__all__ = (
|
|
5
|
+
"create_connections",
|
|
6
|
+
"get_connection_settings",
|
|
7
|
+
"InvalidSettingsError",
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MONGODB_CONF_VARS = (
|
|
12
|
+
"MONGODB_ALIAS",
|
|
13
|
+
"MONGODB_DB",
|
|
14
|
+
"MONGODB_HOST",
|
|
15
|
+
"MONGODB_IS_MOCK",
|
|
16
|
+
"MONGODB_PASSWORD",
|
|
17
|
+
"MONGODB_PORT",
|
|
18
|
+
"MONGODB_USERNAME",
|
|
19
|
+
"MONGODB_CONNECT",
|
|
20
|
+
"MONGODB_TZ_AWARE",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidSettingsError(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _sanitize_settings(settings):
|
|
29
|
+
"""Given a dict of connection settings, sanitize the keys and fall
|
|
30
|
+
back to some sane defaults.
|
|
31
|
+
"""
|
|
32
|
+
# Remove the "MONGODB_" prefix and make all settings keys lower case.
|
|
33
|
+
resolved_settings = {}
|
|
34
|
+
for k, v in settings.items():
|
|
35
|
+
if k.startswith("MONGODB_"):
|
|
36
|
+
k = k[len("MONGODB_") :]
|
|
37
|
+
k = k.lower()
|
|
38
|
+
resolved_settings[k] = v
|
|
39
|
+
|
|
40
|
+
# Handle uri style connections
|
|
41
|
+
if "://" in resolved_settings.get("host", ""):
|
|
42
|
+
# this section pulls the database name from the URI
|
|
43
|
+
# PyMongo requires URI to start with mongodb:// to parse
|
|
44
|
+
# this workaround allows mongomock to work
|
|
45
|
+
uri_to_check = resolved_settings["host"]
|
|
46
|
+
|
|
47
|
+
if uri_to_check.startswith("mongomock://"):
|
|
48
|
+
uri_to_check = uri_to_check.replace("mongomock://", "mongodb://")
|
|
49
|
+
|
|
50
|
+
uri_dict = uri_parser.parse_uri(uri_to_check)
|
|
51
|
+
resolved_settings["db"] = uri_dict["database"]
|
|
52
|
+
|
|
53
|
+
# Add a default name param or use the "db" key if exists
|
|
54
|
+
if resolved_settings.get("db"):
|
|
55
|
+
resolved_settings["name"] = resolved_settings.pop("db")
|
|
56
|
+
else:
|
|
57
|
+
resolved_settings["name"] = "test"
|
|
58
|
+
|
|
59
|
+
# Add various default values.
|
|
60
|
+
resolved_settings["alias"] = resolved_settings.get(
|
|
61
|
+
"alias", mongoengine.DEFAULT_CONNECTION_NAME
|
|
62
|
+
) # TODO do we have to specify it here? MongoEngine should take care of that
|
|
63
|
+
resolved_settings["host"] = resolved_settings.get(
|
|
64
|
+
"host", "localhost"
|
|
65
|
+
) # TODO this is the default host in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here
|
|
66
|
+
resolved_settings["port"] = resolved_settings.get(
|
|
67
|
+
"port", 27017
|
|
68
|
+
) # TODO this is the default port in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here
|
|
69
|
+
|
|
70
|
+
# Default to ReadPreference.PRIMARY if no read_preference is supplied
|
|
71
|
+
resolved_settings["read_preference"] = resolved_settings.get(
|
|
72
|
+
"read_preference", ReadPreference.PRIMARY
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Clean up empty values
|
|
76
|
+
for k, v in list(resolved_settings.items()):
|
|
77
|
+
if v is None:
|
|
78
|
+
del resolved_settings[k]
|
|
79
|
+
|
|
80
|
+
return resolved_settings
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_connection_settings(config):
|
|
84
|
+
"""
|
|
85
|
+
Given a config dict, return a sanitized dict of MongoDB connection
|
|
86
|
+
settings that we can then use to establish connections. For new
|
|
87
|
+
applications, settings should exist in a "MONGODB_SETTINGS" key, but
|
|
88
|
+
for backward compactibility we also support several config keys
|
|
89
|
+
prefixed by "MONGODB_", e.g. "MONGODB_HOST", "MONGODB_PORT", etc.
|
|
90
|
+
"""
|
|
91
|
+
# Sanitize all the settings living under a "MONGODB_SETTINGS" config var
|
|
92
|
+
if "MONGODB_SETTINGS" in config:
|
|
93
|
+
settings = config["MONGODB_SETTINGS"]
|
|
94
|
+
|
|
95
|
+
# If MONGODB_SETTINGS is a list of settings dicts, sanitize each
|
|
96
|
+
# dict separately.
|
|
97
|
+
if isinstance(settings, list):
|
|
98
|
+
# List of connection settings.
|
|
99
|
+
settings_list = []
|
|
100
|
+
for setting in settings:
|
|
101
|
+
settings_list.append(_sanitize_settings(setting))
|
|
102
|
+
return settings_list
|
|
103
|
+
|
|
104
|
+
# Otherwise, it should be a single dict describing a single connection.
|
|
105
|
+
else:
|
|
106
|
+
return _sanitize_settings(settings)
|
|
107
|
+
|
|
108
|
+
# If "MONGODB_SETTINGS" doesn't exist, sanitize the "MONGODB_" keys
|
|
109
|
+
# as if they all describe a single connection.
|
|
110
|
+
else:
|
|
111
|
+
config = dict(
|
|
112
|
+
(k, v) for k, v in config.items() if k in MONGODB_CONF_VARS
|
|
113
|
+
) # ugly dict comprehention in order to support python 2.6
|
|
114
|
+
return _sanitize_settings(config)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_connections(config):
|
|
118
|
+
"""
|
|
119
|
+
Given Flask application's config dict, extract relevant config vars
|
|
120
|
+
out of it and establish MongoEngine connection(s) based on them.
|
|
121
|
+
"""
|
|
122
|
+
# Validate that the config is a dict
|
|
123
|
+
if config is None or not isinstance(config, dict):
|
|
124
|
+
raise InvalidSettingsError("Invalid application configuration")
|
|
125
|
+
|
|
126
|
+
# Get sanitized connection settings based on the config
|
|
127
|
+
conn_settings = get_connection_settings(config)
|
|
128
|
+
|
|
129
|
+
# If conn_settings is a list, set up each item as a separate connection
|
|
130
|
+
# and return a dict of connection aliases and their connections.
|
|
131
|
+
if isinstance(conn_settings, list):
|
|
132
|
+
connections = {}
|
|
133
|
+
for each in conn_settings:
|
|
134
|
+
alias = each["alias"]
|
|
135
|
+
connections[alias] = _connect(each)
|
|
136
|
+
return connections
|
|
137
|
+
|
|
138
|
+
# Otherwise, return a single connection
|
|
139
|
+
return _connect(conn_settings)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _connect(conn_settings):
|
|
143
|
+
"""Given a dict of connection settings, create a connection to
|
|
144
|
+
MongoDB by calling mongoengine.connect and return its result.
|
|
145
|
+
"""
|
|
146
|
+
db_name = conn_settings.pop("name")
|
|
147
|
+
return mongoengine.connect(db_name, **conn_settings)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import mongoengine
|
|
2
|
+
from flask import abort
|
|
3
|
+
from mongoengine.errors import DoesNotExist
|
|
4
|
+
from mongoengine.queryset import QuerySet
|
|
5
|
+
|
|
6
|
+
from .pagination import ListFieldPagination, Pagination
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseQuerySet(QuerySet):
|
|
10
|
+
"""Mongoengine's queryset extended with handy extras."""
|
|
11
|
+
|
|
12
|
+
def get_or_404(self, *args, **kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Get a document and raise a 404 Not Found error if it doesn't
|
|
15
|
+
exist.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
return self.get(*args, **kwargs)
|
|
19
|
+
except DoesNotExist:
|
|
20
|
+
message = kwargs.get("message", None)
|
|
21
|
+
abort(404, message) if message else abort(404)
|
|
22
|
+
|
|
23
|
+
def first_or_404(self, message=None):
|
|
24
|
+
"""Same as get_or_404, but uses .first, not .get."""
|
|
25
|
+
obj = self.first()
|
|
26
|
+
return obj if obj else abort(404, message) if message else abort(404)
|
|
27
|
+
|
|
28
|
+
def paginate(self, page, per_page, **kwargs):
|
|
29
|
+
"""
|
|
30
|
+
Paginate the QuerySet with a certain number of docs per page
|
|
31
|
+
and return docs for a given page.
|
|
32
|
+
"""
|
|
33
|
+
return Pagination(self, page, per_page)
|
|
34
|
+
|
|
35
|
+
def paginate_field(self, field_name, doc_id, page, per_page, total=None):
|
|
36
|
+
"""
|
|
37
|
+
Paginate items within a list field from one document in the
|
|
38
|
+
QuerySet.
|
|
39
|
+
"""
|
|
40
|
+
# TODO this doesn't sound useful at all - remove in next release?
|
|
41
|
+
item = self.get(id=doc_id)
|
|
42
|
+
count = getattr(item, field_name + "_count", "")
|
|
43
|
+
total = total or count or len(getattr(item, field_name))
|
|
44
|
+
return ListFieldPagination(self, doc_id, field_name, page, per_page, total=total)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Document(mongoengine.Document):
|
|
48
|
+
"""Abstract document with extra helpers in the queryset class"""
|
|
49
|
+
|
|
50
|
+
meta = {"abstract": True, "queryset_class": BaseQuerySet}
|
|
51
|
+
|
|
52
|
+
def paginate_field(self, field_name, page, per_page, total=None):
|
|
53
|
+
"""Paginate items within a list field."""
|
|
54
|
+
# TODO this doesn't sound useful at all - remove in next release?
|
|
55
|
+
count = getattr(self, field_name + "_count", "")
|
|
56
|
+
total = total or count or len(getattr(self, field_name))
|
|
57
|
+
return ListFieldPagination(
|
|
58
|
+
self.__class__.objects, self.pk, field_name, page, per_page, total=total
|
|
59
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
import mongoengine
|
|
4
|
+
from flask import Flask, abort, current_app
|
|
5
|
+
from mongoengine.base.fields import BaseField
|
|
6
|
+
from mongoengine.errors import DoesNotExist
|
|
7
|
+
from mongoengine.queryset import QuerySet
|
|
8
|
+
|
|
9
|
+
from .connection import create_connections
|
|
10
|
+
from .json import override_json_encoder
|
|
11
|
+
from .pagination import ListFieldPagination, Pagination
|
|
12
|
+
from .wtf import WtfBaseField
|
|
13
|
+
|
|
14
|
+
VERSION = (1, 0, 0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_version():
|
|
18
|
+
"""Return the VERSION as a string."""
|
|
19
|
+
return ".".join(map(str, VERSION))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__version__ = get_version()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _patch_base_field(obj, name):
|
|
26
|
+
"""
|
|
27
|
+
If the object submitted has a class whose base class is
|
|
28
|
+
mongoengine.base.fields.BaseField, then monkey patch to
|
|
29
|
+
replace it with flask_mongoengine.wtf.WtfBaseField.
|
|
30
|
+
|
|
31
|
+
@note: WtfBaseField is an instance of BaseField - but
|
|
32
|
+
gives us the flexibility to extend field parameters
|
|
33
|
+
and settings required of WTForm via model form generator.
|
|
34
|
+
|
|
35
|
+
@see: flask_mongoengine.wtf.base.WtfBaseField.
|
|
36
|
+
@see: model_form in flask_mongoengine.wtf.orm
|
|
37
|
+
|
|
38
|
+
@param obj: MongoEngine instance in which we should locate the class.
|
|
39
|
+
@param name: Name of an attribute which may or may not be a BaseField.
|
|
40
|
+
"""
|
|
41
|
+
# TODO is there a less hacky way to accomplish the same level of
|
|
42
|
+
# extensibility/control?
|
|
43
|
+
|
|
44
|
+
# get an attribute of the MongoEngine class and return if it's not
|
|
45
|
+
# a class
|
|
46
|
+
cls = getattr(obj, name)
|
|
47
|
+
if not inspect.isclass(cls):
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
# if it is a class, inspect all of its parent classes
|
|
51
|
+
cls_bases = list(cls.__bases__)
|
|
52
|
+
|
|
53
|
+
# if any of them is a BaseField, replace it with WtfBaseField
|
|
54
|
+
for index, base in enumerate(cls_bases):
|
|
55
|
+
if base == BaseField:
|
|
56
|
+
cls_bases[index] = WtfBaseField
|
|
57
|
+
cls.__bases__ = tuple(cls_bases)
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
# re-assign the class back to the MongoEngine instance
|
|
61
|
+
delattr(obj, name)
|
|
62
|
+
setattr(obj, name, cls)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _include_mongoengine(obj):
|
|
66
|
+
"""
|
|
67
|
+
Copy all of the attributes from mongoengine and mongoengine.fields
|
|
68
|
+
onto obj (which should be an instance of the MongoEngine class).
|
|
69
|
+
"""
|
|
70
|
+
# TODO why do we need this? What's wrong with importing from the
|
|
71
|
+
# original modules?
|
|
72
|
+
for module in (mongoengine, mongoengine.fields):
|
|
73
|
+
for attr_name in module.__all__:
|
|
74
|
+
if not hasattr(obj, attr_name):
|
|
75
|
+
setattr(obj, attr_name, getattr(module, attr_name))
|
|
76
|
+
|
|
77
|
+
# patch BaseField if available
|
|
78
|
+
_patch_base_field(obj, attr_name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def current_mongoengine_instance():
|
|
82
|
+
"""Return a MongoEngine instance associated with current Flask app."""
|
|
83
|
+
me = current_app.extensions.get("mongoengine", {})
|
|
84
|
+
for k, v in me.items():
|
|
85
|
+
if isinstance(k, MongoEngine):
|
|
86
|
+
return k
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MongoEngine(object):
|
|
90
|
+
"""Main class used for initialization of Flask-MongoEngine."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, app=None, config=None):
|
|
93
|
+
_include_mongoengine(self)
|
|
94
|
+
|
|
95
|
+
self.app = None
|
|
96
|
+
self.config = config
|
|
97
|
+
self.Document = Document
|
|
98
|
+
self.DynamicDocument = DynamicDocument
|
|
99
|
+
|
|
100
|
+
if app is not None:
|
|
101
|
+
self.init_app(app, config)
|
|
102
|
+
|
|
103
|
+
def init_app(self, app, config=None):
|
|
104
|
+
if not app or not isinstance(app, Flask):
|
|
105
|
+
raise TypeError("Invalid Flask application instance")
|
|
106
|
+
|
|
107
|
+
self.app = app
|
|
108
|
+
|
|
109
|
+
app.extensions = getattr(app, "extensions", {})
|
|
110
|
+
|
|
111
|
+
# Make documents JSON serializable
|
|
112
|
+
override_json_encoder(app)
|
|
113
|
+
|
|
114
|
+
if "mongoengine" not in app.extensions:
|
|
115
|
+
app.extensions["mongoengine"] = {}
|
|
116
|
+
|
|
117
|
+
if self in app.extensions["mongoengine"]:
|
|
118
|
+
# Raise an exception if extension already initialized as
|
|
119
|
+
# potentially new configuration would not be loaded.
|
|
120
|
+
raise ValueError("Extension already initialized")
|
|
121
|
+
|
|
122
|
+
if config:
|
|
123
|
+
# Passed config have max priority, over init config.
|
|
124
|
+
self.config = config
|
|
125
|
+
|
|
126
|
+
if not self.config:
|
|
127
|
+
# If no configs passed, use app.config.
|
|
128
|
+
config = app.config
|
|
129
|
+
|
|
130
|
+
# Obtain db connection(s)
|
|
131
|
+
connections = create_connections(config)
|
|
132
|
+
|
|
133
|
+
# Store objects in application instance so that multiple apps do not
|
|
134
|
+
# end up accessing the same objects.
|
|
135
|
+
s = {"app": app, "conn": connections}
|
|
136
|
+
app.extensions["mongoengine"][self] = s
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def connection(self):
|
|
140
|
+
"""
|
|
141
|
+
Return MongoDB connection(s) associated with this MongoEngine
|
|
142
|
+
instance.
|
|
143
|
+
"""
|
|
144
|
+
return current_app.extensions["mongoengine"][self]["conn"]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BaseQuerySet(QuerySet):
|
|
148
|
+
"""Mongoengine's queryset extended with handy extras."""
|
|
149
|
+
|
|
150
|
+
def get_or_404(self, *args, **kwargs):
|
|
151
|
+
"""
|
|
152
|
+
Get a document and raise a 404 Not Found error if it doesn't
|
|
153
|
+
exist.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
return self.get(*args, **kwargs)
|
|
157
|
+
except DoesNotExist:
|
|
158
|
+
message = kwargs.get("message", None)
|
|
159
|
+
abort(404, message) if message else abort(404)
|
|
160
|
+
|
|
161
|
+
def first_or_404(self, message=None):
|
|
162
|
+
"""Same as get_or_404, but uses .first, not .get."""
|
|
163
|
+
obj = self.first()
|
|
164
|
+
return obj if obj else abort(404, message) if message else abort(404)
|
|
165
|
+
|
|
166
|
+
def paginate(self, page, per_page, **kwargs):
|
|
167
|
+
"""
|
|
168
|
+
Paginate the QuerySet with a certain number of docs per page
|
|
169
|
+
and return docs for a given page.
|
|
170
|
+
"""
|
|
171
|
+
return Pagination(self, page, per_page)
|
|
172
|
+
|
|
173
|
+
def paginate_field(self, field_name, doc_id, page, per_page, total=None):
|
|
174
|
+
"""
|
|
175
|
+
Paginate items within a list field from one document in the
|
|
176
|
+
QuerySet.
|
|
177
|
+
"""
|
|
178
|
+
# TODO this doesn't sound useful at all - remove in next release?
|
|
179
|
+
item = self.get(id=doc_id)
|
|
180
|
+
count = getattr(item, field_name + "_count", "")
|
|
181
|
+
total = total or count or len(getattr(item, field_name))
|
|
182
|
+
return ListFieldPagination(self, doc_id, field_name, page, per_page, total=total)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Document(mongoengine.Document):
|
|
186
|
+
"""Abstract document with extra helpers in the queryset class"""
|
|
187
|
+
|
|
188
|
+
meta = {"abstract": True, "queryset_class": BaseQuerySet}
|
|
189
|
+
|
|
190
|
+
def paginate_field(self, field_name, page, per_page, total=None):
|
|
191
|
+
"""Paginate items within a list field."""
|
|
192
|
+
# TODO this doesn't sound useful at all - remove in next release?
|
|
193
|
+
count = getattr(self, field_name + "_count", "")
|
|
194
|
+
total = total or count or len(getattr(self, field_name))
|
|
195
|
+
return ListFieldPagination(
|
|
196
|
+
self.__class__.objects, self.pk, field_name, page, per_page, total=total
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class DynamicDocument(mongoengine.DynamicDocument):
|
|
201
|
+
"""Abstract Dynamic document with extra helpers in the queryset class"""
|
|
202
|
+
|
|
203
|
+
meta = {"abstract": True, "queryset_class": BaseQuerySet}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from gettext import gettext as _
|
|
2
|
+
|
|
3
|
+
from mongoengine.queryset import DoesNotExist
|
|
4
|
+
from wtforms import widgets
|
|
5
|
+
from wtforms.fields import SelectFieldBase
|
|
6
|
+
from wtforms.validators import ValidationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class QuerySetSelectField(SelectFieldBase):
|
|
10
|
+
"""
|
|
11
|
+
Given a QuerySet either at initialization or inside a view, will display a
|
|
12
|
+
select drop-down field of choices. The `data` property actually will
|
|
13
|
+
store/keep an ORM model instance, not the ID. Submitting a choice which is
|
|
14
|
+
not in the queryset will result in a validation error.
|
|
15
|
+
|
|
16
|
+
Specifying `label_attr` in the constructor will use that property of the
|
|
17
|
+
model instance for display in the list, else the model object's `__str__`
|
|
18
|
+
or `__unicode__` will be used.
|
|
19
|
+
|
|
20
|
+
If `allow_blank` is set to `True`, then a blank choice will be added to the
|
|
21
|
+
top of the list. Selecting this choice will result in the `data` property
|
|
22
|
+
being `None`. The label for the blank choice can be set by specifying the
|
|
23
|
+
`blank_text` parameter.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
widget = widgets.Select()
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
label="",
|
|
31
|
+
validators=None,
|
|
32
|
+
queryset=None,
|
|
33
|
+
label_attr="",
|
|
34
|
+
allow_blank=False,
|
|
35
|
+
blank_text="---",
|
|
36
|
+
label_modifier=None,
|
|
37
|
+
**kwargs,
|
|
38
|
+
):
|
|
39
|
+
super(QuerySetSelectField, self).__init__(label, validators, **kwargs)
|
|
40
|
+
self.label_attr = label_attr
|
|
41
|
+
self.allow_blank = allow_blank
|
|
42
|
+
self.blank_text = blank_text
|
|
43
|
+
self.label_modifier = label_modifier
|
|
44
|
+
self.queryset = queryset
|
|
45
|
+
|
|
46
|
+
def iter_choices(self):
|
|
47
|
+
if self.allow_blank:
|
|
48
|
+
yield ("__None", self.blank_text, self.data is None)
|
|
49
|
+
|
|
50
|
+
if self.queryset is None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
self.queryset.rewind()
|
|
54
|
+
for obj in self.queryset:
|
|
55
|
+
label = (
|
|
56
|
+
self.label_modifier(obj)
|
|
57
|
+
if self.label_modifier
|
|
58
|
+
else (self.label_attr and getattr(obj, self.label_attr) or obj)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if isinstance(self.data, list):
|
|
62
|
+
selected = obj in self.data
|
|
63
|
+
else:
|
|
64
|
+
selected = self._is_selected(obj)
|
|
65
|
+
yield (obj.id, label, selected)
|
|
66
|
+
|
|
67
|
+
def process_formdata(self, valuelist):
|
|
68
|
+
if valuelist:
|
|
69
|
+
if valuelist[0] == "__None":
|
|
70
|
+
self.data = None
|
|
71
|
+
else:
|
|
72
|
+
if self.queryset is None:
|
|
73
|
+
self.data = None
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
obj = self.queryset.get(pk=valuelist[0])
|
|
78
|
+
self.data = obj
|
|
79
|
+
except DoesNotExist:
|
|
80
|
+
self.data = None
|
|
81
|
+
|
|
82
|
+
def pre_validate(self, form):
|
|
83
|
+
if not self.allow_blank or self.data is not None:
|
|
84
|
+
if not self.data:
|
|
85
|
+
raise ValidationError(_("Not a valid choice"))
|
|
86
|
+
|
|
87
|
+
def _is_selected(self, item):
|
|
88
|
+
return item == self.data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ModelSelectField(QuerySetSelectField):
|
|
92
|
+
"""
|
|
93
|
+
Like a QuerySetSelectField, except takes a model class instead of a
|
|
94
|
+
queryset and lists everything in it.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, label="", validators=None, model=None, **kwargs):
|
|
98
|
+
queryset = kwargs.pop("queryset", model.objects)
|
|
99
|
+
super(ModelSelectField, self).__init__(label, validators, queryset=queryset, **kwargs)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from bson import json_util
|
|
2
|
+
from flask.json import JSONEncoder
|
|
3
|
+
from mongoengine.base import BaseDocument
|
|
4
|
+
from mongoengine.queryset import QuerySet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _make_encoder(superclass):
|
|
8
|
+
class MongoEngineJSONEncoder(superclass):
|
|
9
|
+
"""
|
|
10
|
+
A JSONEncoder which provides serialization of MongoEngine
|
|
11
|
+
documents and queryset objects.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def default(self, obj):
|
|
15
|
+
if isinstance(obj, BaseDocument):
|
|
16
|
+
return json_util._json_convert(obj.to_mongo())
|
|
17
|
+
elif isinstance(obj, QuerySet):
|
|
18
|
+
return json_util._json_convert(obj.as_pymongo())
|
|
19
|
+
return superclass.default(self, obj)
|
|
20
|
+
|
|
21
|
+
return MongoEngineJSONEncoder
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
MongoEngineJSONEncoder = _make_encoder(JSONEncoder)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def override_json_encoder(app):
|
|
28
|
+
"""
|
|
29
|
+
A function to dynamically create a new MongoEngineJSONEncoder class
|
|
30
|
+
based upon a custom base class.
|
|
31
|
+
This function allows us to combine MongoEngine serialization with
|
|
32
|
+
any changes to Flask's JSONEncoder which a user may have made
|
|
33
|
+
prior to calling init_app.
|
|
34
|
+
|
|
35
|
+
NOTE: This does not cover situations where users override
|
|
36
|
+
an instance's json_encoder after calling init_app.
|
|
37
|
+
"""
|
|
38
|
+
app.json_encoder = _make_encoder(app.json_encoder)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from flask_wtf import FlaskForm
|
|
2
|
+
from flask_wtf.form import _Auto
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModelForm(FlaskForm):
|
|
6
|
+
"""A WTForms mongoengine model form"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, formdata=_Auto, **kwargs):
|
|
9
|
+
self.instance = kwargs.pop("instance", None) or kwargs.get("obj")
|
|
10
|
+
if self.instance and not formdata:
|
|
11
|
+
kwargs["obj"] = self.instance
|
|
12
|
+
self.formdata = formdata
|
|
13
|
+
super(ModelForm, self).__init__(formdata, **kwargs)
|
|
14
|
+
|
|
15
|
+
def save(self, commit=True, **kwargs):
|
|
16
|
+
if self.instance:
|
|
17
|
+
self.populate_obj(self.instance)
|
|
18
|
+
else:
|
|
19
|
+
self.instance = self.model_class(**self.data)
|
|
20
|
+
|
|
21
|
+
if commit:
|
|
22
|
+
self.instance.save(**kwargs)
|
|
23
|
+
return self.instance
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from flask import abort
|
|
4
|
+
from mongoengine.queryset import QuerySet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Pagination(object):
|
|
8
|
+
def __init__(self, iterable, page, per_page):
|
|
9
|
+
if page < 1:
|
|
10
|
+
abort(404)
|
|
11
|
+
|
|
12
|
+
self.iterable = iterable
|
|
13
|
+
self.page = page
|
|
14
|
+
self.per_page = per_page
|
|
15
|
+
|
|
16
|
+
if isinstance(iterable, QuerySet):
|
|
17
|
+
self.total = iterable.count()
|
|
18
|
+
else:
|
|
19
|
+
self.total = len(iterable)
|
|
20
|
+
|
|
21
|
+
start_index = (page - 1) * per_page
|
|
22
|
+
end_index = page * per_page
|
|
23
|
+
|
|
24
|
+
self.items = iterable[start_index:end_index]
|
|
25
|
+
if isinstance(self.items, QuerySet):
|
|
26
|
+
self.items = self.items.select_related()
|
|
27
|
+
if not self.items and page != 1:
|
|
28
|
+
abort(404)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def pages(self):
|
|
32
|
+
"""The total number of pages"""
|
|
33
|
+
return int(math.ceil(self.total / float(self.per_page)))
|
|
34
|
+
|
|
35
|
+
def prev(self, error_out=False):
|
|
36
|
+
"""Returns a :class:`Pagination` object for the previous page."""
|
|
37
|
+
assert self.iterable is not None, "an object is required for this method to work"
|
|
38
|
+
iterable = self.iterable
|
|
39
|
+
if isinstance(iterable, QuerySet):
|
|
40
|
+
iterable._skip = None
|
|
41
|
+
iterable._limit = None
|
|
42
|
+
return self.__class__(iterable, self.page - 1, self.per_page)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def prev_num(self):
|
|
46
|
+
"""Number of the previous page."""
|
|
47
|
+
return self.page - 1
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def has_prev(self):
|
|
51
|
+
"""True if a previous page exists"""
|
|
52
|
+
return self.page > 1
|
|
53
|
+
|
|
54
|
+
def next(self, error_out=False):
|
|
55
|
+
"""Returns a :class:`Pagination` object for the next page."""
|
|
56
|
+
assert self.iterable is not None, "an object is required for this method to work"
|
|
57
|
+
iterable = self.iterable
|
|
58
|
+
if isinstance(iterable, QuerySet):
|
|
59
|
+
iterable._skip = None
|
|
60
|
+
iterable._limit = None
|
|
61
|
+
return self.__class__(iterable, self.page + 1, self.per_page)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def has_next(self):
|
|
65
|
+
"""True if a next page exists."""
|
|
66
|
+
return self.page < self.pages
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def next_num(self):
|
|
70
|
+
"""Number of the next page"""
|
|
71
|
+
return self.page + 1
|
|
72
|
+
|
|
73
|
+
def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2):
|
|
74
|
+
"""Iterates over the page numbers in the pagination. The four
|
|
75
|
+
parameters control the thresholds how many numbers should be produced
|
|
76
|
+
from the sides. Skipped page numbers are represented as `None`.
|
|
77
|
+
This is how you could render such a pagination in the templates:
|
|
78
|
+
|
|
79
|
+
.. sourcecode:: html+jinja
|
|
80
|
+
|
|
81
|
+
{% macro render_pagination(pagination, endpoint) %}
|
|
82
|
+
<div class=pagination>
|
|
83
|
+
{%- for page in pagination.iter_pages() %}
|
|
84
|
+
{% if page %}
|
|
85
|
+
{% if page != pagination.page %}
|
|
86
|
+
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
|
|
87
|
+
{% else %}
|
|
88
|
+
<strong>{{ page }}</strong>
|
|
89
|
+
{% endif %}
|
|
90
|
+
{% else %}
|
|
91
|
+
<span class=ellipsis>…</span>
|
|
92
|
+
{% endif %}
|
|
93
|
+
{%- endfor %}
|
|
94
|
+
</div>
|
|
95
|
+
{% endmacro %}
|
|
96
|
+
"""
|
|
97
|
+
last = 0
|
|
98
|
+
for num in range(1, self.pages + 1):
|
|
99
|
+
if (
|
|
100
|
+
num <= left_edge
|
|
101
|
+
or num > self.pages - right_edge
|
|
102
|
+
or (num >= self.page - left_current and num <= self.page + right_current)
|
|
103
|
+
):
|
|
104
|
+
if last + 1 != num:
|
|
105
|
+
yield None
|
|
106
|
+
yield num
|
|
107
|
+
last = num
|
|
108
|
+
if last != self.pages:
|
|
109
|
+
yield None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ListFieldPagination(Pagination):
|
|
113
|
+
def __init__(self, queryset, doc_id, field_name, page, per_page, total=None):
|
|
114
|
+
"""Allows an array within a document to be paginated.
|
|
115
|
+
|
|
116
|
+
Queryset must contain the document which has the array we're
|
|
117
|
+
paginating, and doc_id should be it's _id.
|
|
118
|
+
Field name is the name of the array we're paginating.
|
|
119
|
+
Page and per_page work just like in Pagination.
|
|
120
|
+
Total is an argument because it can be computed more efficiently
|
|
121
|
+
elsewhere, but we still use array.length as a fallback.
|
|
122
|
+
"""
|
|
123
|
+
if page < 1:
|
|
124
|
+
abort(404)
|
|
125
|
+
|
|
126
|
+
self.page = page
|
|
127
|
+
self.per_page = per_page
|
|
128
|
+
|
|
129
|
+
self.queryset = queryset
|
|
130
|
+
self.doc_id = doc_id
|
|
131
|
+
self.field_name = field_name
|
|
132
|
+
|
|
133
|
+
start_index = (page - 1) * per_page
|
|
134
|
+
|
|
135
|
+
field_attrs = {field_name: {"$slice": [start_index, per_page]}}
|
|
136
|
+
|
|
137
|
+
qs = queryset(pk=doc_id)
|
|
138
|
+
self.items = getattr(qs.fields(**field_attrs).first(), field_name)
|
|
139
|
+
self.total = total or len(getattr(qs.fields(**{field_name: 1}).first(), field_name))
|
|
140
|
+
|
|
141
|
+
if not self.items and page != 1:
|
|
142
|
+
abort(404)
|
|
143
|
+
|
|
144
|
+
def prev(self, error_out=False):
|
|
145
|
+
"""Returns a :class:`Pagination` object for the previous page."""
|
|
146
|
+
assert self.items is not None, "a query object is required for this method to work"
|
|
147
|
+
return self.__class__(
|
|
148
|
+
self.queryset,
|
|
149
|
+
self.doc_id,
|
|
150
|
+
self.field_name,
|
|
151
|
+
self.page - 1,
|
|
152
|
+
self.per_page,
|
|
153
|
+
self.total,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def next(self, error_out=False):
|
|
157
|
+
"""Returns a :class:`Pagination` object for the next page."""
|
|
158
|
+
assert self.items is not None, "a query object is required for this method to work"
|
|
159
|
+
return self.__class__(
|
|
160
|
+
self.queryset,
|
|
161
|
+
self.doc_id,
|
|
162
|
+
self.field_name,
|
|
163
|
+
self.page + 1,
|
|
164
|
+
self.per_page,
|
|
165
|
+
self.total,
|
|
166
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from mongoengine.base import BaseField
|
|
2
|
+
|
|
3
|
+
__all__ = "WtfBaseField"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WtfBaseField(BaseField):
|
|
7
|
+
"""
|
|
8
|
+
Extension wrapper class for mongoengine BaseField.
|
|
9
|
+
|
|
10
|
+
This enables flask-mongoengine wtf to extend the
|
|
11
|
+
number of field parameters, and settings on behalf
|
|
12
|
+
of document model form generator for WTForm.
|
|
13
|
+
|
|
14
|
+
@param validators: wtf model form field validators.
|
|
15
|
+
@param filters: wtf model form field filters.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, validators=None, filters=None, **kwargs):
|
|
19
|
+
self.validators = self._ensure_callable_or_list(validators, "validators")
|
|
20
|
+
self.filters = self._ensure_callable_or_list(filters, "filters")
|
|
21
|
+
|
|
22
|
+
BaseField.__init__(self, **kwargs)
|
|
23
|
+
|
|
24
|
+
def _ensure_callable_or_list(self, field, msg_flag):
|
|
25
|
+
"""
|
|
26
|
+
Ensure the value submitted via field is either
|
|
27
|
+
a callable object to convert to list or it is
|
|
28
|
+
in fact a valid list value.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
if field is not None:
|
|
32
|
+
if callable(field):
|
|
33
|
+
field = [field]
|
|
34
|
+
else:
|
|
35
|
+
msg = "Argument '%s' must be a list value" % msg_flag
|
|
36
|
+
if not isinstance(field, list):
|
|
37
|
+
raise TypeError(msg)
|
|
38
|
+
|
|
39
|
+
return field
|
udata/forms/__init__.py
CHANGED
|
@@ -2,10 +2,10 @@ import logging
|
|
|
2
2
|
|
|
3
3
|
import wtforms_json
|
|
4
4
|
|
|
5
|
+
from udata.flask_mongoengine.model_form import ModelForm as MEModelForm
|
|
6
|
+
|
|
5
7
|
wtforms_json.init()
|
|
6
|
-
from flask_mongoengine.wtf import model_form # noqa
|
|
7
8
|
|
|
8
|
-
from flask_mongoengine.wtf.models import ModelForm as MEModelForm # noqa
|
|
9
9
|
from flask_wtf import FlaskForm # noqa
|
|
10
10
|
|
|
11
11
|
from udata import i18n # noqa
|
udata/forms/fields.py
CHANGED
|
@@ -2,7 +2,6 @@ import uuid
|
|
|
2
2
|
|
|
3
3
|
from dateutil.parser import parse
|
|
4
4
|
from flask import url_for
|
|
5
|
-
from flask_mongoengine.wtf import fields as mefields
|
|
6
5
|
from flask_storage.mongo import ImageReference
|
|
7
6
|
from speaklater import is_lazy_string
|
|
8
7
|
from wtforms import Field as WTField
|
|
@@ -16,6 +15,7 @@ from udata import tags, uris
|
|
|
16
15
|
from udata.auth import admin_permission, current_user
|
|
17
16
|
from udata.core.organization.permissions import OrganizationPrivatePermission
|
|
18
17
|
from udata.core.storages import tmp
|
|
18
|
+
from udata.flask_mongoengine.fields import ModelSelectField as BaseModelSelectField
|
|
19
19
|
from udata.forms import ModelForm
|
|
20
20
|
from udata.i18n import lazy_gettext as _
|
|
21
21
|
from udata.models import ContactPoint, Dataset, Organization, Reuse, User, datastore, db
|
|
@@ -381,7 +381,7 @@ class SelectField(FieldHelper, fields.SelectField):
|
|
|
381
381
|
self._choices = value
|
|
382
382
|
|
|
383
383
|
|
|
384
|
-
class ModelSelectField(FieldHelper,
|
|
384
|
+
class ModelSelectField(FieldHelper, BaseModelSelectField):
|
|
385
385
|
pass
|
|
386
386
|
|
|
387
387
|
|
udata/mongo/document.py
CHANGED
udata/mongo/engine.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from bson import DBRef, ObjectId
|
|
2
|
-
from flask_mongoengine import MongoEngine, MongoEngineSessionInterface
|
|
3
2
|
from flask_storage.mongo import FileField, ImageField
|
|
4
3
|
from mongoengine.base import TopLevelDocumentMetaclass, get_document
|
|
5
4
|
from mongoengine.errors import ValidationError
|
|
6
5
|
from mongoengine.signals import post_save, pre_save
|
|
7
6
|
|
|
7
|
+
from udata.flask_mongoengine.engine import MongoEngine
|
|
8
|
+
|
|
8
9
|
from .datetime_fields import DateField, DateRange, Datetimed
|
|
9
10
|
from .document import DomainModel, UDataDocument
|
|
10
11
|
from .extras_fields import ExtrasField, OrganizationExtrasField
|
|
@@ -64,4 +65,3 @@ class UDataMongoEngine(MongoEngine):
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
db = UDataMongoEngine()
|
|
67
|
-
session_interface = MongoEngineSessionInterface(db)
|
udata/mongo/queryset.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from bson import DBRef, ObjectId
|
|
4
|
-
from flask_mongoengine import BaseQuerySet
|
|
5
4
|
from mongoengine.signals import post_save
|
|
6
5
|
|
|
6
|
+
from udata.flask_mongoengine.document import BaseQuerySet
|
|
7
7
|
from udata.utils import Paginable
|
|
8
8
|
|
|
9
9
|
log = logging.getLogger(__name__)
|
udata/mongo/slug_fields.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
import slugify
|
|
4
|
-
from flask_mongoengine import Document
|
|
5
4
|
from mongoengine.fields import StringField
|
|
6
5
|
from mongoengine.signals import post_delete, pre_save
|
|
7
6
|
|
|
7
|
+
from udata.flask_mongoengine.document import Document
|
|
8
8
|
from udata.utils import is_uuid
|
|
9
9
|
|
|
10
10
|
from .queryset import UDataQuerySet
|
udata/settings.py
CHANGED
|
@@ -676,7 +676,6 @@ class Debug(Defaults):
|
|
|
676
676
|
"flask_debugtoolbar.panels.template.TemplateDebugPanel",
|
|
677
677
|
"flask_debugtoolbar.panels.logger.LoggingPanel",
|
|
678
678
|
"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
|
|
679
|
-
"flask_mongoengine.panels.MongoDebugPanel",
|
|
680
679
|
)
|
|
681
680
|
CACHE_TYPE = "flask_caching.backends.null"
|
|
682
681
|
CACHE_NO_NULL_WARNING = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: udata
|
|
3
|
-
Version: 12.0.2.
|
|
3
|
+
Version: 12.0.2.dev4
|
|
4
4
|
Summary: Open data portal
|
|
5
5
|
Author-email: Opendata Team <opendatateam@data.gouv.fr>
|
|
6
6
|
Maintainer-email: Opendata Team <opendatateam@data.gouv.fr>
|
|
@@ -25,34 +25,21 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or l
|
|
|
25
25
|
Requires-Python: <3.14,>=3.11
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: amqp==5.3.1
|
|
29
|
-
Requires-Dist: aniso8601==10.0.0
|
|
30
|
-
Requires-Dist: attrs==25.3.0
|
|
31
28
|
Requires-Dist: authlib==1.5.1
|
|
32
29
|
Requires-Dist: awesome-slugify==1.6.5
|
|
33
30
|
Requires-Dist: babel==2.17.0
|
|
34
31
|
Requires-Dist: bcrypt==4.3.0
|
|
35
|
-
Requires-Dist: billiard==4.2.1
|
|
36
32
|
Requires-Dist: bleach[css]==6.2.0
|
|
37
33
|
Requires-Dist: blinker==1.5
|
|
38
34
|
Requires-Dist: boto3==1.26.102
|
|
39
35
|
Requires-Dist: botocore==1.29.165
|
|
40
|
-
Requires-Dist: cachelib==0.13.0
|
|
41
36
|
Requires-Dist: celery==5.4.0
|
|
42
37
|
Requires-Dist: celerybeat-mongo==0.2.0
|
|
43
|
-
Requires-Dist: certifi==2025.1.31
|
|
44
|
-
Requires-Dist: cffi==1.17.1
|
|
45
|
-
Requires-Dist: charset-normalizer==3.4.1
|
|
46
38
|
Requires-Dist: click==8.1.8
|
|
47
|
-
Requires-Dist: click-didyoumean==0.3.1
|
|
48
|
-
Requires-Dist: click-plugins==1.1.1
|
|
49
|
-
Requires-Dist: click-repl==0.3.0
|
|
50
39
|
Requires-Dist: cryptography==44.0.2
|
|
51
|
-
Requires-Dist: dnspython==2.7.0
|
|
52
40
|
Requires-Dist: email-validator==2.2.0
|
|
53
41
|
Requires-Dist: factory-boy==3.3.3
|
|
54
42
|
Requires-Dist: feedgenerator==2.1.0
|
|
55
|
-
Requires-Dist: filelock==3.18.0
|
|
56
43
|
Requires-Dist: flask==2.1.2
|
|
57
44
|
Requires-Dist: flask-babel==4.0.0
|
|
58
45
|
Requires-Dist: flask-caching==2.3.1
|
|
@@ -60,7 +47,6 @@ Requires-Dist: flask-cdn==1.5.3
|
|
|
60
47
|
Requires-Dist: flask-gravatar==0.5.0
|
|
61
48
|
Requires-Dist: flask-login==0.6.3
|
|
62
49
|
Requires-Dist: flask-mail==0.10.0
|
|
63
|
-
Requires-Dist: flask-mongoengine==1.0.0
|
|
64
50
|
Requires-Dist: flask-navigation-temp-python-311==0.2.1
|
|
65
51
|
Requires-Dist: flask-principal==0.4.0
|
|
66
52
|
Requires-Dist: flask-restx==1.3.0
|
|
@@ -72,13 +58,10 @@ Requires-Dist: geojson==3.2.0
|
|
|
72
58
|
Requires-Dist: geomet==1.1.0
|
|
73
59
|
Requires-Dist: html2text==2024.2.26
|
|
74
60
|
Requires-Dist: humanfriendly==10.0
|
|
75
|
-
Requires-Dist: idna==3.10
|
|
76
61
|
Requires-Dist: importlib-resources==6.5.2
|
|
77
62
|
Requires-Dist: itsdangerous==2.2.0
|
|
78
63
|
Requires-Dist: jinja2==3.1.6
|
|
79
|
-
Requires-Dist: jmespath==1.0.1
|
|
80
64
|
Requires-Dist: jsonschema==4.23.0
|
|
81
|
-
Requires-Dist: jsonschema-specifications==2024.10.1
|
|
82
65
|
Requires-Dist: kombu[redis]==5.5.0
|
|
83
66
|
Requires-Dist: langdetect==1.0.9
|
|
84
67
|
Requires-Dist: levenshtein==0.27.1
|
|
@@ -87,40 +70,23 @@ Requires-Dist: markupsafe==2.1.2
|
|
|
87
70
|
Requires-Dist: mistune==3.1.3
|
|
88
71
|
Requires-Dist: mongoengine==0.29.1
|
|
89
72
|
Requires-Dist: netaddr==1.3.0
|
|
90
|
-
Requires-Dist: passlib==1.7.4
|
|
91
73
|
Requires-Dist: pillow==9.2.0
|
|
92
|
-
Requires-Dist: platformdirs==4.3.6
|
|
93
|
-
Requires-Dist: prompt-toolkit==3.0.50
|
|
94
|
-
Requires-Dist: pycparser==2.22
|
|
95
74
|
Requires-Dist: pydenticon==0.3.1
|
|
96
75
|
Requires-Dist: pymongo==4.11.3
|
|
97
|
-
Requires-Dist: pyparsing==3.2.1
|
|
98
76
|
Requires-Dist: python-dateutil==2.9.0.post0
|
|
99
77
|
Requires-Dist: pytz==2025.1
|
|
100
|
-
Requires-Dist: rapidfuzz==3.12.2
|
|
101
78
|
Requires-Dist: rdflib==7.1.3
|
|
102
79
|
Requires-Dist: redis==5.2.1
|
|
103
|
-
Requires-Dist: referencing==0.36.2
|
|
104
|
-
Requires-Dist: regex==2024.11.6
|
|
105
80
|
Requires-Dist: requests==2.32.4
|
|
106
|
-
Requires-Dist: rpds-py==0.23.1
|
|
107
|
-
Requires-Dist: s3transfer==0.6.2
|
|
108
81
|
Requires-Dist: saxonche==12.8.0
|
|
109
82
|
Requires-Dist: sentry-sdk[flask]==2.23.1
|
|
110
|
-
Requires-Dist: six==1.17.0
|
|
111
83
|
Requires-Dist: speaklater==1.3
|
|
112
|
-
Requires-Dist: tinycss2==1.4.0
|
|
113
84
|
Requires-Dist: tlds==2025022800
|
|
114
85
|
Requires-Dist: typing-extensions==4.12.2
|
|
115
86
|
Requires-Dist: tzdata==2025.1
|
|
116
|
-
Requires-Dist: unidecode==0.4.21
|
|
117
|
-
Requires-Dist: uritools==4.0.3
|
|
118
87
|
Requires-Dist: urlextract==1.9.0
|
|
119
88
|
Requires-Dist: urllib3==1.26.20
|
|
120
|
-
Requires-Dist: vine==5.1.0
|
|
121
89
|
Requires-Dist: voluptuous==0.15.2
|
|
122
|
-
Requires-Dist: wcwidth==0.2.13
|
|
123
|
-
Requires-Dist: webencodings==0.5.1
|
|
124
90
|
Requires-Dist: werkzeug==2.2.3
|
|
125
91
|
Requires-Dist: wtforms[email]==3.2.1
|
|
126
92
|
Requires-Dist: wtforms-json==0.3.5
|
|
@@ -10,7 +10,7 @@ udata/mail.py,sha256=Huhx_1QthJkLvuRUuP6jqb5Qq5R4iSmqeEpLVO9ZkQ4,2671
|
|
|
10
10
|
udata/rdf.py,sha256=aJKmnE1r6YyMKXLo-VRlUvOXoZSJuvNeNbMCqEl0kdY,19370
|
|
11
11
|
udata/routing.py,sha256=Hnc1ktmKVS-RUHNKw2zYTft2HJ903FhjtlcenQ9igwI,8044
|
|
12
12
|
udata/sentry.py,sha256=ekcxqUSqxfM98TtvCsPaOoX5i2l6PEcYt7kb4l3od-Q,3223
|
|
13
|
-
udata/settings.py,sha256=
|
|
13
|
+
udata/settings.py,sha256=o6sV0gAXgZY4if2pU017yR0pKwDTEFBFbnEWYDyBxZg,22110
|
|
14
14
|
udata/sitemap.py,sha256=oRRWoPI7ZsFFnUAOqGT1YuXFFKHBe8EcRnUCNHD7xjM,979
|
|
15
15
|
udata/tags.py,sha256=ydq4uokd6bzdeGVSpEXASVtGvDfO2LfQs9mptvvKJCM,631
|
|
16
16
|
udata/tasks.py,sha256=Sv01dhvATtq_oHOBp3J1j1VT1HQe0Pab7zxwIeIdKoo,5122
|
|
@@ -281,8 +281,17 @@ udata/features/transfer/factories.py,sha256=2dAvRaemGieLn5aVEXQ6cmIGD2dTX5I0Ql2R
|
|
|
281
281
|
udata/features/transfer/models.py,sha256=sFmdbEHL7eUxYe2XdtjZ2zhrUWOW_ryP_5T_8N1qUEA,958
|
|
282
282
|
udata/features/transfer/notifications.py,sha256=jeH5buqTuWcokgXfEj-EZ7C3YCrtw-NikfrH2bqFyHM,1121
|
|
283
283
|
udata/features/transfer/permissions.py,sha256=0Iwt_I3S3QACpq4Ba6Ovb8RUBuozj2pbXDtFFp9jdLI,953
|
|
284
|
-
udata/
|
|
285
|
-
udata/
|
|
284
|
+
udata/flask_mongoengine/__init__.py,sha256=Ijqs6lu009OSWgUFdw1AdihjbbLL7-BekwZojXg92bU,599
|
|
285
|
+
udata/flask_mongoengine/connection.py,sha256=b4dPVa8X_xbCFJ8ngt-RWLZtjNCHyZQSw-0fhRL6ySA,5191
|
|
286
|
+
udata/flask_mongoengine/document.py,sha256=KY_PtwbLuqtnlV105YX6ot5GVHq9k61EGv-V6Nf8r78,2200
|
|
287
|
+
udata/flask_mongoengine/engine.py,sha256=vZnACne6XWTpHKvA95KWx8Bh-0hXY58xaGnXjgEc5ow,6769
|
|
288
|
+
udata/flask_mongoengine/fields.py,sha256=moNEAt6_QYu3-Tv9uwI7Okak61iLQSn5_J-0IiP1pB0,3290
|
|
289
|
+
udata/flask_mongoengine/json.py,sha256=ChAgqhfCoSljjcAzDm0-B_vAMlLsPT-1bynZa7A3YnU,1242
|
|
290
|
+
udata/flask_mongoengine/model_form.py,sha256=LI6eqd2zykhron29U280mjt0uZlBpM8GP9NDKrB46Tw,718
|
|
291
|
+
udata/flask_mongoengine/pagination.py,sha256=AmKZqC6Pe4K8Y2TF1kj5O4gM5_m1NVthtJQVvoayX2M,5592
|
|
292
|
+
udata/flask_mongoengine/wtf.py,sha256=BU2ahk6xkUmM18oaUf539RXFTP04O8S2CvEniYGJ1co,1226
|
|
293
|
+
udata/forms/__init__.py,sha256=WdBOu40HijJi0A6kjUkuT-WEP2YNW3k_ftSzQS_Zl9Y,789
|
|
294
|
+
udata/forms/fields.py,sha256=UgC7NQvVAvs5dULUJA9T_MWUADhhwzXTJ509owKZNnc,30042
|
|
286
295
|
udata/forms/validators.py,sha256=CRgmB6oow5O8LDR45LajlJJ9HX3RBCI08fapoWMH1vo,2727
|
|
287
296
|
udata/forms/widgets.py,sha256=XMVxBlQMGfb0nQvqwLMsAVcEdsBdaZGQZ82F81FdmlM,1332
|
|
288
297
|
udata/frontend/__init__.py,sha256=DkqncR72V9TRlA3QEcpVtCIkUMUlnTytBlJFQ4HfEiA,6454
|
|
@@ -398,12 +407,12 @@ udata/migrations/__init__.py,sha256=RBCBDaTlLjuMs_Qzwji6Z6T4r7FCGXhESKoxQbT5qAA,
|
|
|
398
407
|
udata/models/__init__.py,sha256=77OriDFm4dJzQDIb-7ADKSkf9GAxkXbMHeWXoYYnTsk,1459
|
|
399
408
|
udata/mongo/__init__.py,sha256=y4Rv-kq3o_kcEulcNpePLzocXPBNpx3Jd82G-VZPaMc,1421
|
|
400
409
|
udata/mongo/datetime_fields.py,sha256=xACagQZu1OKPvpcznI-bMC1tJfAvo-VBUe7OOadnBdg,2089
|
|
401
|
-
udata/mongo/document.py,sha256=
|
|
402
|
-
udata/mongo/engine.py,sha256=
|
|
410
|
+
udata/mongo/document.py,sha256=yJl4rzE0L69SvNbtmnmyCALTGhXwBPrj7nvM-J6sDpE,1792
|
|
411
|
+
udata/mongo/engine.py,sha256=JF9N55j7joDIn9NrItMtlIrA5CwVLhS_jlB2ptX94oA,2408
|
|
403
412
|
udata/mongo/errors.py,sha256=SpTMAc_aNIfGkqyXGCbTlIAmYxU86rGM_NtIYaB642c,472
|
|
404
413
|
udata/mongo/extras_fields.py,sha256=FfyVvRkpLn4pUeCqwI33NBJblHOywGlnA05RCEZ-ugs,4139
|
|
405
|
-
udata/mongo/queryset.py,sha256=
|
|
406
|
-
udata/mongo/slug_fields.py,sha256=
|
|
414
|
+
udata/mongo/queryset.py,sha256=fXfYkUHsCWAUoub3OR7v825USPv-PQQIHkv4U5FnjYg,3954
|
|
415
|
+
udata/mongo/slug_fields.py,sha256=tEUlwozrdQfF42KR5dxk5PUNSX7zISTIXsSgHxR4YMg,7522
|
|
407
416
|
udata/mongo/taglist_field.py,sha256=RPi8DlgMEMK1wk8hbQDLAyH2GnzZCfNpWXQsllxPB6g,1371
|
|
408
417
|
udata/mongo/url_field.py,sha256=UmUr9c5SxDFDpS5QsRTq2pKcCTOr1SoB4UITwNjtuaI,1345
|
|
409
418
|
udata/mongo/uuid_fields.py,sha256=tuQ3zs_BnQHjaiKSIYv43jxvYtOvRLw9nP5CQ3fcMks,482
|
|
@@ -623,9 +632,9 @@ udata/translations/pt/LC_MESSAGES/udata.mo,sha256=U0abG-nBwCIoYxRZNsc4KOLeIRSqTV
|
|
|
623
632
|
udata/translations/pt/LC_MESSAGES/udata.po,sha256=eCG35rMzYLHXyLbsnLSexS1g0N_K-WpNHqrt_8y6I4E,48590
|
|
624
633
|
udata/translations/sr/LC_MESSAGES/udata.mo,sha256=IBcCAdmcvkeK7ZeRBNRI-wJ0jzWNM0eXM5VXAc1frWI,28692
|
|
625
634
|
udata/translations/sr/LC_MESSAGES/udata.po,sha256=yFxHEEB4behNwQ7JnyoYheiCKLNnMS_NV4guzgyzWcE,55332
|
|
626
|
-
udata-12.0.2.
|
|
627
|
-
udata-12.0.2.
|
|
628
|
-
udata-12.0.2.
|
|
629
|
-
udata-12.0.2.
|
|
630
|
-
udata-12.0.2.
|
|
631
|
-
udata-12.0.2.
|
|
635
|
+
udata-12.0.2.dev4.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
|
|
636
|
+
udata-12.0.2.dev4.dist-info/METADATA,sha256=qNMVQ7vPHh9mXWZZjVwrNmAbB0u0iJrjuUDHSiacRVU,5553
|
|
637
|
+
udata-12.0.2.dev4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
638
|
+
udata-12.0.2.dev4.dist-info/entry_points.txt,sha256=v2u12qO11i2lyLNIp136WmLJ-NHT-Kew3Duu8J-AXPM,614
|
|
639
|
+
udata-12.0.2.dev4.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
|
|
640
|
+
udata-12.0.2.dev4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|