Skip to content

Commit 432fb1c

Browse files
committed
Fixes to pagination
1 parent 37d990f commit 432fb1c

File tree

5 files changed

+393
-225
lines changed

5 files changed

+393
-225
lines changed

flask_mongoengine/__init__.py

Lines changed: 3 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import absolute_import
33

4-
import datetime
5-
import math
6-
import uuid
7-
84
from flask import abort
9-
from flask.sessions import SessionInterface, SessionMixin
10-
from werkzeug.datastructures import CallbackDict
115

126
import mongoengine
137

148
from mongoengine.queryset import MultipleObjectsReturned, DoesNotExist, QuerySet
159
from mongoengine.base import ValidationError
1610

11+
from .sessions import *
12+
from .pagination import *
13+
1714

1815
def _include_mongoengine(obj):
1916
for module in mongoengine, mongoengine.fields:
@@ -104,222 +101,3 @@ class DynamicDocument(mongoengine.DynamicDocument):
104101

105102
meta = {'abstract': True,
106103
'queryset_class': BaseQuerySet}
107-
108-
109-
class Pagination(object):
110-
111-
def __init__(self, iterable, page, per_page):
112-
113-
if page < 1:
114-
abort(404)
115-
116-
self.iterable = iterable
117-
self.page = page
118-
self.per_page = per_page
119-
self.total = len(iterable)
120-
121-
start_index = (page - 1) * per_page
122-
end_index = page * per_page
123-
124-
self.items = iterable[start_index:end_index]
125-
if isinstance(self.items, QuerySet):
126-
self.items = self.items.select_related()
127-
if not self.items and page != 1:
128-
abort(404)
129-
130-
@property
131-
def pages(self):
132-
"""The total number of pages"""
133-
return int(math.ceil(self.total / float(self.per_page)))
134-
135-
def prev(self, error_out=False):
136-
"""Returns a :class:`Pagination` object for the previous page."""
137-
assert self.iterable is not None, ('an object is required '
138-
'for this method to work')
139-
iterable = self.iterable
140-
if isinstance(iterable, QuerySet):
141-
iterable._skip = None
142-
iterable._limit = None
143-
iterable = iterable.clone()
144-
return self.__class__(iterable, self.page - 1, self.per_page)
145-
146-
@property
147-
def prev_num(self):
148-
"""Number of the previous page."""
149-
return self.page - 1
150-
151-
@property
152-
def has_prev(self):
153-
"""True if a previous page exists"""
154-
return self.page > 1
155-
156-
def next(self, error_out=False):
157-
"""Returns a :class:`Pagination` object for the next page."""
158-
assert self.iterable is not None, ('an object is required '
159-
'for this method to work')
160-
iterable = self.iterable
161-
if isinstance(iterable, QuerySet):
162-
iterable._skip = None
163-
iterable._limit = None
164-
iterable = iterable.clone()
165-
return self.__class__(iterable, self.page + 1, self.per_page)
166-
167-
@property
168-
def has_next(self):
169-
"""True if a next page exists."""
170-
return self.page < self.pages
171-
172-
@property
173-
def next_num(self):
174-
"""Number of the next page"""
175-
return self.page + 1
176-
177-
def iter_pages(self, left_edge=2, left_current=2,
178-
right_current=5, right_edge=2):
179-
"""Iterates over the page numbers in the pagination. The four
180-
parameters control the thresholds how many numbers should be produced
181-
from the sides. Skipped page numbers are represented as `None`.
182-
This is how you could render such a pagination in the templates:
183-
184-
.. sourcecode:: html+jinja
185-
186-
{% macro render_pagination(pagination, endpoint) %}
187-
<div class=pagination>
188-
{%- for page in pagination.iter_pages() %}
189-
{% if page %}
190-
{% if page != pagination.page %}
191-
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
192-
{% else %}
193-
<strong>{{ page }}</strong>
194-
{% endif %}
195-
{% else %}
196-
<span class=ellipsis>…</span>
197-
{% endif %}
198-
{%- endfor %}
199-
</div>
200-
{% endmacro %}
201-
"""
202-
last = 0
203-
for num in xrange(1, self.pages + 1):
204-
if num <= left_edge or \
205-
(num > self.page - left_current - 1 and
206-
num < self.page + right_current) or \
207-
num > self.pages - right_edge:
208-
if last + 1 != num:
209-
yield None
210-
yield num
211-
last = num
212-
213-
214-
class ListFieldPagination(Pagination):
215-
216-
def __init__(self, queryset, field_name, doc_id, page, per_page,
217-
total=None):
218-
"""Allows an array within a document to be paginated.
219-
220-
Queryset must contain the document which has the array we're
221-
paginating, and doc_id should be it's _id.
222-
Field name is the name of the array we're paginating.
223-
Page and per_page work just like in Pagination.
224-
Total is an argument because it can be computed more efficiently
225-
elsewhere, but we still use array.length as a fallback.
226-
"""
227-
if page < 1:
228-
abort(404)
229-
230-
self.page = page
231-
self.per_page = per_page
232-
233-
self.queryset = queryset
234-
self.doc_id = doc_id
235-
self.field_name = field_name
236-
237-
start_index = (page - 1) * per_page
238-
239-
field_attrs = {field_name: {"$slice": [start_index, per_page]}}
240-
241-
self.items = getattr(queryset().fields(**field_attrs).first(), field_name)
242-
243-
self.total = total or len(self.items)
244-
245-
if not self.items and page != 1:
246-
abort(404)
247-
248-
def prev(self, error_out=False):
249-
"""Returns a :class:`Pagination` object for the previous page."""
250-
assert self.items is not None, ('a query object is required '
251-
'for this method to work')
252-
return self.__class__(self.queryset, self.doc_id, self.field_name,
253-
self.page - 1, self.per_page, self.total)
254-
255-
def next(self, error_out=False):
256-
"""Returns a :class:`Pagination` object for the next page."""
257-
assert self.iterable is not None, ('a query object is required '
258-
'for this method to work')
259-
return self.__class__(self.queryset, self.doc_id, self.field_name,
260-
self.page + 1, self.per_page, self.total)
261-
262-
263-
class MongoEngineSession(CallbackDict, SessionMixin):
264-
265-
def __init__(self, initial=None, sid=None):
266-
def on_update(self):
267-
self.modified = True
268-
CallbackDict.__init__(self, initial, on_update)
269-
self.sid = sid
270-
self.modified = False
271-
272-
273-
class MongoEngineSessionInterface(SessionInterface):
274-
"""SessionInterface for mongoengine"""
275-
276-
def __init__(self, db, collection='session'):
277-
"""
278-
The MongoSessionInterface
279-
280-
:param db: The app's db eg: MongoEngine()
281-
:param collection: The session collection name defaults to "session"
282-
"""
283-
284-
if not isinstance(collection, basestring):
285-
raise ValueError('collection argument should be string or unicode')
286-
287-
class DBSession(db.Document):
288-
sid = db.StringField(primary_key=True)
289-
data = db.DictField()
290-
expiration = db.DateTimeField()
291-
meta = {
292-
'collection': collection,
293-
'indexes': [{'fields': ['expiration'],
294-
'expireAfterSeconds': 60 * 60 * 24 * 7 * 31}]
295-
}
296-
297-
self.cls = DBSession
298-
299-
def get_expiration_time(self, app, session):
300-
if session.permanent:
301-
return app.permanent_session_lifetime
302-
return datetime.timedelta(days=1)
303-
304-
def open_session(self, app, request):
305-
sid = request.cookies.get(app.session_cookie_name)
306-
if sid:
307-
stored_session = self.cls.objects(sid=sid).first()
308-
if stored_session and stored_session.expiration > datetime.datetime.utcnow():
309-
return MongoEngineSession(initial=stored_session.data, sid=stored_session.sid)
310-
return MongoEngineSession(sid=str(uuid.uuid4()))
311-
312-
def save_session(self, app, session, response):
313-
domain = self.get_cookie_domain(app)
314-
if not session:
315-
if session.modified:
316-
response.delete_cookie(app.session_cookie_name, domain=domain)
317-
return
318-
319-
expiration = datetime.datetime.now() + self.get_expiration_time(app, session)
320-
321-
if session.modified:
322-
self.cls(sid=session.sid, data=session, expiration=expiration).save()
323-
324-
response.set_cookie(app.session_cookie_name, session.sid,
325-
expires=expiration, httponly=True, domain=domain)

0 commit comments

Comments
 (0)