Reputation: 902
Is there a known method of extending the MongoDB cursor object to make it compatible with Django's django.core.paginator.Paginator
class?
Or, maybe, extend Django's class?
Upvotes: 3
Views: 2246
Reputation: 532
The approach from @cpury didn't work for me - maybe there were some pymongo/django changes. Here is a simpler version that creates a paginator-like object for the ListView to interact with. Note that in your template you'll need to use page_obj for attributes like has_other_pages, has_next, has_previous etc.
class MongoQuerySet:
def __init__(self, cursor, results_count):
self.cursor = cursor
self.count = results_count
def __getitem__(self, key):
if isinstance(key, slice):
return list(self.cursor[key.start:key.stop])
else:
return self.cursor[key]
def __len__(self):
return self.count
class MongoSearchView(LoginRequiredMixin, ListView):
template_name = 'pages/results.html'
context_object_name = 'results'
paginate_by = 15
def get_queryset(self):
client = MongoClient(
host=settings.MONGO_HOST,
port=settings.MONGO_PORT,
username=settings.MONGO_USER,
password=settings.MONGO_PASSWORD,
authSource=settings.MONGO_AUTH_SOURCE
)
db = client[settings.MONGO_DB]
collection = db[settings.MONGO_COLL]
index_info = collection.index_information()
if "filename_text" not in index_info:
collection.create_index([("filename", "text")])
query = self.request.GET.get('q', '')
regex_pattern = f".*{re.escape(query)}.*"
projection = {"filename": 1, "description": 1, "created": 1}
filter_dict = {"filename": {"$regex": regex_pattern, "$options": "i"}}
results = collection.find(filter_dict, projection)
results_count = collection.count_documents(filter_dict)
return MongoQuerySet(results, results_count)
Upvotes: 0
Reputation: 1023
I was facing the same issue and implemented my own Paginator class that works. Here's the code:
from django.core.paginator import Paginator, Page
class MongoPaginator(Paginator):
"""
Custom subclass of Django's Paginator to work with Mongo cursors.
"""
def _get_page(self, *args, **kwargs):
"""
Returns an instance of a single page. Replaced with our custom
MongoPage class.
"""
return MongoPage(*args, **kwargs)
def page(self, number):
"""
Returns a Page object for the given 1-based page number.
Important difference to standard Paginator: Creates a clone of the
cursor so we can get multiple slices.
"""
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
return self._get_page(
self.object_list.clone()[bottom:top], number, self
)
class MongoPage(Page):
"""
Custom Page class for our MongoPaginator. Just makes sure the cursor is
directly converted to list so that we can use len(object_list).
"""
def __init__(self, object_list, number, paginator):
self.object_list = list(object_list)
self.number = number
self.paginator = paginator
The main changes are:
Here is a helper function to use it in views:
def get_paginated_cursor(request, cursor, per_page=25, param='page'):
"""
Helper to deal with some standard pagination. Pass a request and a
cursor and it will return a paginated version of it.
"""
paginator = MongoPaginator(cursor, per_page)
page = request.GET.get(param, 1)
try:
cursor = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
cursor = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
cursor = paginator.page(paginator.num_pages)
return cursor
Then you can do:
def some_view_function(request):
messages_cursor = db.messages.find({})
messages = get_paginated_cursor(request, messages_cursor)
Upvotes: 1
Reputation: 4972
Your temporary solution (https://gist.github.com/2351079) looks good - but instead of forcing the cursor to fetch all results with list()
and paginating with [bottom:top]
, maybe try using .skip()
and .limit()
on the cursor explicitly - it will probably improve performance.
Upvotes: 0