-
-
Notifications
You must be signed in to change notification settings - Fork 217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enterprise Form Submissions Iterators #35295
Open
mjriley
wants to merge
23
commits into
master
Choose a base branch
from
mjr/enterprise_iterators_draft
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+908
−27
Open
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b9d3b6d
Apply sorting to enterprise domain list
mjriley 463d97b
Add resumable iterator wrapper
mjriley 3eccde7
Add KeysetPaginator
mjriley 5154360
Added enterprise form iterators
mjriley 4112456
Rewire FormSubmissionResource to use iterators
mjriley 399b013
Moved generic API classes into the API application
mjriley 185a143
Removed ResumableIteratorWrapper
mjriley 05eaa9a
Switched received filter to inserted
mjriley 2504668
Rename domain forms generator
mjriley dd334de
Make enterprise form api timezone aware
mjriley 080d837
Rename mobile_user field to username
mjriley 09c104b
Made enterprise form submission report iteration generic
mjriley 409f725
Added happy path test for form resource api
mjriley bff5fac
Remove superuser permissions from Enterprise Forms API test
mjriley c65f0b6
rename api test
mjriley 593882c
isort
mjriley bee7055
Refactor domain iteration logic
mjriley 7082597
rename domain looping functions
mjriley b85a5f5
Added authentication tests
mjriley 2d9d74b
isort
mjriley dedb429
Additional clarifying comments/structures
mjriley d489a36
Allow the iterable query to use a generic converter
mjriley c5d96fb
Changed "test-domain" to "testing-domain" to try to isolate a testing…
mjriley File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from itertools import islice | ||
from django.http.request import QueryDict | ||
from urllib.parse import urlencode | ||
from tastypie.paginator import Paginator | ||
|
||
|
||
class KeysetPaginator(Paginator): | ||
''' | ||
An alternate paginator meant to support paginating by keyset rather than by index/offset. | ||
`objects` is expected to represent a query object that exposes an `.execute(limit)` | ||
method that returns an iterable, and a `get_query_params(object)` method to retrieve the parameters | ||
for the next query | ||
Because keyset pagination does not efficiently handle slicing or offset operations, | ||
these methods have been intentionally disabled | ||
''' | ||
def __init__(self, request_data, objects, | ||
resource_uri=None, limit=None, max_limit=1000, collection_name='objects'): | ||
super().__init__( | ||
request_data, | ||
objects, | ||
resource_uri=resource_uri, | ||
limit=limit, | ||
max_limit=max_limit, | ||
collection_name=collection_name | ||
) | ||
|
||
def get_offset(self): | ||
raise NotImplementedError() | ||
|
||
def get_slice(self, limit, offset): | ||
raise NotImplementedError() | ||
|
||
def get_count(self): | ||
raise NotImplementedError() | ||
|
||
def get_previous(self, limit, offset): | ||
raise NotImplementedError() | ||
|
||
def get_next(self, limit, **next_params): | ||
return self._generate_uri(limit, **next_params) | ||
|
||
def _generate_uri(self, limit, **next_params): | ||
if self.resource_uri is None: | ||
return None | ||
|
||
if isinstance(self.request_data, QueryDict): | ||
# Because QueryDict allows multiple values for the same key, we need to remove existing values | ||
# prior to updating | ||
request_params = self.request_data.copy() | ||
if 'limit' in request_params: | ||
del request_params['limit'] | ||
for key in next_params: | ||
if key in request_params: | ||
del request_params[key] | ||
|
||
request_params.update({'limit': str(limit), **next_params}) | ||
encoded_params = request_params.urlencode() | ||
else: | ||
request_params = {} | ||
for k, v in self.request_data.items(): | ||
if isinstance(v, str): | ||
request_params[k] = v.encode('utf-8') | ||
else: | ||
request_params[k] = v | ||
|
||
request_params.update({'limit': limit, **next_params}) | ||
encoded_params = urlencode(request_params) | ||
|
||
return '%s?%s' % ( | ||
self.resource_uri, | ||
encoded_params | ||
) | ||
|
||
def page(self): | ||
""" | ||
Generates all pertinent data about the requested page. | ||
""" | ||
limit = self.get_limit() | ||
padded_limit = limit + 1 if limit else limit | ||
# Fetch 1 more record than requested to allow us to determine if further queries will be needed | ||
it = iter(self.objects.execute(limit=padded_limit)) | ||
objects = list(islice(it, limit)) | ||
|
||
try: | ||
next(it) | ||
has_more = True | ||
except StopIteration: | ||
has_more = False | ||
|
||
meta = { | ||
'limit': limit, | ||
} | ||
|
||
if limit and has_more: | ||
last_fetched = objects[-1] | ||
next_page_params = self.objects.get_query_params(last_fetched) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check if there is a missing test for this? didn't fail for misnamed method name |
||
meta['next'] = self.get_next(limit, **next_page_params) | ||
|
||
return { | ||
self.collection_name: objects, | ||
'meta': meta, | ||
} | ||
|
||
|
||
class PageableQueryInterface: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this still being used? |
||
def execute(limit=None): | ||
''' | ||
Should return an iterable that exposes a `.get_query_params()` method | ||
''' | ||
raise NotImplementedError() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
from django.test import SimpleTestCase | ||
from django.http import QueryDict | ||
from corehq.apps.api.keyset_paginator import KeysetPaginator | ||
|
||
|
||
class SequenceQuery: | ||
def __init__(self, seq): | ||
self.seq = seq | ||
|
||
def execute(self, limit=None): | ||
return self.seq | ||
|
||
@classmethod | ||
def get_query_params(cls, form): | ||
return {'next': form} | ||
|
||
|
||
class KeysetPaginatorTests(SimpleTestCase): | ||
def test_page_fetches_all_results_below_limit(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects, limit=10) | ||
page = paginator.page() | ||
self.assertEqual(page['objects'], [0, 1, 2, 3, 4]) | ||
self.assertEqual(page['meta'], {'limit': 10}) | ||
|
||
def test_page_includes_next_information_when_more_results_are_available(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects, resource_uri='http://test.com/', limit=3) | ||
page = paginator.page() | ||
self.assertEqual(page['objects'], [0, 1, 2]) | ||
self.assertEqual(page['meta'], {'limit': 3, 'next': 'http://test.com/?limit=3&next=2'}) | ||
|
||
def test_does_not_include_duplicate_limits(self): | ||
request_data = QueryDict(mutable=True) | ||
request_data['limit'] = 3 | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(request_data, objects, resource_uri='http://test.com/') | ||
page = paginator.page() | ||
self.assertEqual(page['meta']['next'], 'http://test.com/?limit=3&next=2') | ||
|
||
def test_supports_dict_request_data(self): | ||
request_data = { | ||
'limit': 3, | ||
'some_param': 'yes' | ||
} | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(request_data, objects, resource_uri='http://test.com/') | ||
page = paginator.page() | ||
self.assertEqual(page['meta']['next'], 'http://test.com/?limit=3&some_param=yes&next=2') | ||
|
||
def test_get_offset_not_implemented(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects) | ||
|
||
with self.assertRaises(NotImplementedError): | ||
paginator.get_offset() | ||
|
||
def test_get_slice_not_implemented(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects) | ||
|
||
with self.assertRaises(NotImplementedError): | ||
paginator.get_slice(limit=10, offset=20) | ||
|
||
def test_get_count_not_implemented(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects) | ||
|
||
with self.assertRaises(NotImplementedError): | ||
paginator.get_count() | ||
|
||
def test_get_previous_not_implemented(self): | ||
objects = SequenceQuery(range(5)) | ||
paginator = KeysetPaginator(QueryDict(), objects) | ||
|
||
with self.assertRaises(NotImplementedError): | ||
paginator.get_previous(limit=10, offset=20) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
potentially rename limit to indicate page size as it is used?