udata 12.0.2.dev3__py3-none-any.whl → 12.0.2.dev5__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.

@@ -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, mefields.ModelSelectField):
384
+ class ModelSelectField(FieldHelper, BaseModelSelectField):
385
385
  pass
386
386
 
387
387
 
udata/mongo/document.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from collections.abc import Iterable
3
3
 
4
- from flask_mongoengine import Document
4
+ from udata.flask_mongoengine.document import Document
5
5
 
6
6
  from .queryset import UDataQuerySet
7
7
 
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__)
@@ -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.dev3
3
+ Version: 12.0.2.dev5
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>
@@ -47,13 +47,12 @@ Requires-Dist: flask-cdn==1.5.3
47
47
  Requires-Dist: flask-gravatar==0.5.0
48
48
  Requires-Dist: flask-login==0.6.3
49
49
  Requires-Dist: flask-mail==0.10.0
50
- Requires-Dist: flask-mongoengine==1.0.0
51
50
  Requires-Dist: flask-navigation-temp-python-311==0.2.1
52
51
  Requires-Dist: flask-principal==0.4.0
53
52
  Requires-Dist: flask-restx==1.3.0
54
53
  Requires-Dist: flask-security-too==5.1.2
55
54
  Requires-Dist: flask-sitemap==0.4.0
56
- Requires-Dist: flask-storage==1.3.2
55
+ Requires-Dist: flask-storage~=1.4.0
57
56
  Requires-Dist: flask-wtf==1.2.2
58
57
  Requires-Dist: geojson==3.2.0
59
58
  Requires-Dist: geomet==1.1.0
@@ -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=HUoKXMBJBowxDIr6G0aKfRWk9AyQhKNkGV3UTGWKa-w,22162
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/forms/__init__.py,sha256=OXNShgt05tADLYQ-cXPdB16qQtj2i48GxSsCVdR6cfI,843
285
- udata/forms/fields.py,sha256=UezHtGuuYsk0FWFpmztWXlPUB4Wf1Rf9IX9ANM796l0,30016
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=pJJ5B-1EVbQUPMjRqgK51BHPY5eVnyaXgnBnzqwojtA,1777
402
- udata/mongo/engine.py,sha256=A5RBDM0zSBowkDvlPbDEY8oVYjsu5aPPoC3vhtWSIvQ,2475
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=yIECtsmdDAk_KqbLm0pFzWdon_1ybODfjRdThBdbjac,3939
406
- udata/mongo/slug_fields.py,sha256=oJo8yZKgpWD4xLaG1VHKjA40chbqdXFwLeGW2xSZFYg,7507
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.dev3.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
627
- udata-12.0.2.dev3.dist-info/METADATA,sha256=0joZgGkFGtJkwaL6Ri9Mu36vdPcw8UMdltYz1oQMeYE,5593
628
- udata-12.0.2.dev3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
629
- udata-12.0.2.dev3.dist-info/entry_points.txt,sha256=v2u12qO11i2lyLNIp136WmLJ-NHT-Kew3Duu8J-AXPM,614
630
- udata-12.0.2.dev3.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
631
- udata-12.0.2.dev3.dist-info/RECORD,,
635
+ udata-12.0.2.dev5.dist-info/licenses/LICENSE,sha256=V8j_M8nAz8PvAOZQocyRDX7keai8UJ9skgmnwqETmdY,34520
636
+ udata-12.0.2.dev5.dist-info/METADATA,sha256=BPrPKteXNiyrE6-zNGpGBb35lWFFO2c7LT5ewq8wAw8,5553
637
+ udata-12.0.2.dev5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
638
+ udata-12.0.2.dev5.dist-info/entry_points.txt,sha256=v2u12qO11i2lyLNIp136WmLJ-NHT-Kew3Duu8J-AXPM,614
639
+ udata-12.0.2.dev5.dist-info/top_level.txt,sha256=EF6CE6YSHd_og-8LCEA4q25ALUpWVe8D0okOLdMAE3A,6
640
+ udata-12.0.2.dev5.dist-info/RECORD,,