Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/custom_queryset.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ flask-mongoengine attaches the following methods to Mongoengine's default QueryS
* **first_or_404**: same as above, except for .first().
Optional arguments: *message* - custom message to display.
* **paginate**: paginates the QuerySet. Takes two required arguments, *page* and *per_page*.
And one optional arguments *max_depth*.
And two optional arguments: *max_depth*, *first_page_index*.
* **paginate_by_keyset**: paginates the QuerySet. Takes two required arguments,
*per_page* and *field_filter_by*.
from the second page you need also the last id of the previous page.
Arguments: *per_page*, *field_filter_by*, *last_field_value*.
* **paginate_field**: paginates a field from one document in the QuerySet.
Arguments: *field_name*, *doc_id*, *page*, *per_page*.

And two optional arguments: *total*, *first_page_index*
Examples:

```python
Expand Down
18 changes: 14 additions & 4 deletions flask_mongoengine/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@ def first_or_404(self, _message_404=None):
"""
return self.first() or self._abort_404(_message_404)

def paginate(self, page, per_page):
def paginate(self, page, per_page, first_page_index=1):
"""
Paginate the QuerySet with a certain number of docs per page
and return docs for a given page.
"""
return Pagination(self, page, per_page)
return Pagination(
self, page=page, per_page=per_page, first_page_index=first_page_index
)

def paginate_by_keyset(self, per_page, field_filter_by, last_field_value):
"""
Expand All @@ -71,7 +73,9 @@ def paginate_by_keyset(self, per_page, field_filter_by, last_field_value):
"""
return KeysetPagination(self, per_page, field_filter_by, last_field_value)

def paginate_field(self, field_name, doc_id, page, per_page, total=None):
def paginate_field(
self, field_name, doc_id, page, per_page, total=None, first_page_index=1
):
"""
Paginate items within a list field from one document in the
QuerySet.
Expand All @@ -81,7 +85,13 @@ def paginate_field(self, field_name, doc_id, page, per_page, total=None):
count = getattr(item, f"{field_name}_count", "")
total = total or count or len(getattr(item, field_name))
return ListFieldPagination(
self, doc_id, field_name, page, per_page, total=total
self,
doc_id,
field_name,
page,
per_page,
total=total,
first_page_index=first_page_index,
)


Expand Down
24 changes: 24 additions & 0 deletions flask_mongoengine/pagination/abc_pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import math
from abc import ABC, abstractmethod


class ABCPagination(ABC):
@property
def pages(self) -> int:
"""The total number of pages"""
return int(math.ceil(self.total / float(self.per_page)))

@abstractmethod
def prev(self, error_out=False):
"""Returns a :class:`Pagination` object for the previous page."""
raise NotImplementedError

@property
def prev_num(self) -> int:
"""Number of the previous page."""
return self.page - 1

@abstractmethod
def next(self, error_out=False):
"""Returns a :class:`Pagination` object for the next page."""
raise NotImplementedError
81 changes: 55 additions & 26 deletions flask_mongoengine/pagination/basic_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,52 @@
from flask import abort
from mongoengine.queryset import QuerySet


class Pagination(object):
def __init__(self, iterable, page: int, per_page: int, max_depth: int = None):
from flask_mongoengine.pagination.abc_pagination import ABCPagination


class Pagination(ABCPagination):
def __init__(
self,
iterable,
page: int,
per_page: int,
max_depth: int = None,
first_page_index: int = 1,
):
"""
:param iterable: iterable object .
:param page: Required page number start from 1.
:param per_page: Required number of documents per page.
:param max_depth: Option for limit number of dereference documents.
:param first_page_index: Option for change first page index.
"""

if page < 1:
abort(404)
if page < first_page_index:
abort(404, "Invalid page number.")

self.iterable = iterable
self.page = page
self.per_page = per_page
self.first_page_index = first_page_index

if isinstance(self.iterable, QuerySet):
self.total = iterable.count()
self.items = self.iterable.skip(self.per_page * (self.page - 1)).limit(
self.per_page
)
self.items = self.iterable.skip(
self.per_page * (self.page - self.first_page_index)
).limit(self.per_page)
if max_depth is not None:
self.items = self.items.select_related(max_depth)
else:
start_index = (page - 1) * per_page
end_index = page * per_page
start_index = (page - self.first_page_index) * per_page
end_index = start_index + per_page

self.total = len(iterable)
self.items = iterable[start_index:end_index]
if not self.items and page != 1:
abort(404)
self.items = iterable[start_index : min(end_index, self.total)]
if not self.items and page != self.first_page_index:
abort(404, "Invalid page number.")

@property
def pages(self):
def pages(self) -> int:
"""The total number of pages"""
return int(math.ceil(self.total / float(self.per_page)))

Expand All @@ -50,17 +61,22 @@ def prev(self, error_out=False):
if isinstance(iterable, QuerySet):
iterable._skip = None
iterable._limit = None
return self.__class__(iterable, self.page - 1, self.per_page)
return self.__class__(
iterable,
page=self.page - 1,
per_page=self.per_page,
first_page_index=self.first_page_index,
)

@property
def prev_num(self):
def prev_num(self) -> int:
"""Number of the previous page."""
return self.page - 1

@property
def has_prev(self):
def has_prev(self) -> bool:
"""True if a previous page exists"""
return self.page > 1
return self.page > self.first_page_index

def next(self, error_out=False):
"""Returns a :class:`Pagination` object for the next page."""
Expand All @@ -71,19 +87,31 @@ def next(self, error_out=False):
if isinstance(iterable, QuerySet):
iterable._skip = None
iterable._limit = None
return self.__class__(iterable, self.page + 1, self.per_page)
return self.__class__(
iterable,
page=self.page + 1,
per_page=self.per_page,
first_page_index=self.first_page_index,
)

@property
def has_next(self):
def has_next(self) -> bool:
"""True if a next page exists."""
return self.page < self.pages
return self.page < self.last_page_index

@property
def next_num(self):
def last_page_index(self) -> int:
"""True if a next page exists."""
return self.pages - 1 + self.first_page_index

@property
def next_num(self) -> int:
"""Number of the next page"""
return self.page + 1

def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2):
def iter_pages(
self, left_edge=2, left_current=2, right_current=5, right_edge=2
): # TODO: documentation!!!
"""Iterates over the page numbers in the pagination. The four
parameters control the thresholds how many numbers should be produced
from the sides. Skipped page numbers are represented as `None`.
Expand All @@ -107,8 +135,9 @@ def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2)
</div>
{% endmacro %}
"""
last = 0
for num in range(1, self.pages + 1):

last = -1 + self.first_page_index
for num in range(self.first_page_index, self.pages + self.first_page_index):
if (
num <= left_edge
or num > self.pages - right_edge
Expand All @@ -120,5 +149,5 @@ def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2)
yield None
yield num
last = num
if last != self.pages:
if last != self.last_page_index:
yield None
4 changes: 2 additions & 2 deletions flask_mongoengine/pagination/keyset_pagination.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from mongoengine.queryset import QuerySet

from flask_mongoengine.pagination.basic_pagination import Pagination
from flask_mongoengine.pagination.abc_pagination import ABCPagination


class KeysetPagination(Pagination):
class KeysetPagination(ABCPagination):
def __init__(
self,
iterable,
Expand Down
27 changes: 21 additions & 6 deletions flask_mongoengine/pagination/list_field_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@


class ListFieldPagination(Pagination):
def __init__(self, queryset, doc_id, field_name, page, per_page, total=None):
def __init__(
self,
queryset,
doc_id,
field_name,
page: int,
per_page: int,
total=None,
first_page_index: int = 1,
):
"""Allows an array within a document to be paginated.

Queryset must contain the document which has the array we're
Expand All @@ -15,18 +24,22 @@ def __init__(self, queryset, doc_id, field_name, page, per_page, total=None):
Page and per_page work just like in Pagination.
Total is an argument because it can be computed more efficiently
elsewhere, but we still use array.length as a fallback.

first_page_index is option for change first page index.

"""
if page < 1:
abort(404)
if page < first_page_index:
abort(404, "Invalid page number.")

self.page = page
self.per_page = per_page
self.first_page_index = first_page_index

self.queryset = queryset
self.doc_id = doc_id
self.field_name = field_name

start_index = (page - 1) * per_page
start_index = (page - self.first_page_index) * per_page

field_attrs = {field_name: {"$slice": [start_index, per_page]}}

Expand All @@ -36,8 +49,8 @@ def __init__(self, queryset, doc_id, field_name, page, per_page, total=None):
getattr(qs.fields(**{field_name: 1}).first(), field_name)
)

if not self.items and page != 1:
abort(404)
if not self.items and page != self.first_page_index:
abort(404, "Invalid page number.")

def prev(self, error_out=False):
"""Returns a :class:`Pagination` object for the previous page."""
Expand All @@ -51,6 +64,7 @@ def prev(self, error_out=False):
self.page - 1,
self.per_page,
self.total,
self.first_page_index,
)

def next(self, error_out=False):
Expand All @@ -65,4 +79,5 @@ def next(self, error_out=False):
self.page + 1,
self.per_page,
self.total,
self.first_page_index,
)
16 changes: 14 additions & 2 deletions flask_mongoengine/wtf/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
]
from typing import Callable, Optional

import wtforms
from flask import json
from mongoengine.queryset import DoesNotExist
from wtforms import fields as wtf_fields
from wtforms import validators as wtf_validators
from wtforms import widgets as wtf_widgets

wtf_version = list(wtforms.__version__.split("."))
new_wtf_version = (int(wtf_version[0]) >= 3 and int(wtf_version[1]) >= 1) or int(
wtf_version[0]
) > 3


def coerce_boolean(value: Optional[str]) -> Optional[bool]:
"""Transform SelectField boolean value from string and in reverse direction."""
Expand Down Expand Up @@ -77,7 +83,10 @@ def iter_choices(self):
iterable of (value, label, selected) tuples.
"""
if self.allow_blank:
yield "__None", self.blank_text, self.data is None
if new_wtf_version:
yield "__None", self.blank_text, self.data is None, self.render_kw or dict()
else:
yield "__None", self.blank_text, self.data is None

if self.queryset is None:
return
Expand All @@ -94,7 +103,10 @@ def iter_choices(self):
selected = obj in self.data
else:
selected = self._is_selected(obj)
yield obj.id, label, selected
if new_wtf_version:
yield obj.id, label, selected, self.render_kw or {}
else:
yield obj.id, label, selected

def process_formdata(self, valuelist):
"""
Expand Down
Loading