Commit 6ebd8e7b by Raphael Lullis

Django-style query filtering added. Documentation updated.

parent befdd54f
...@@ -250,31 +250,14 @@ operate on the data, or when you iterate on the Queryset. ...@@ -250,31 +250,14 @@ operate on the data, or when you iterate on the Queryset.
#### Filtering #### Filtering
Querysets can be filtered: Like Django, Querysets can have constraints added by appending the name of the filter operator to name of the attribute:
~~~~~ {python} ~~~~~ {python}
high_scores = GameScore.Query.all().gte(score=1000) high_scores = GameScore.Query.filter(score__gte=1000)
~~~~~ ~~~~~
The available filter functions are: You can see the [full list of constraint operators defined by
Parse](https://www.parse.com/docs/rest#queries-constraints)
* **Less Than**
* lt(**_parameters_)
* **Less Than Or Equal To**
* lte(**_parameters_)
* **Greater Than**
* gt(**_parameters_)
* **Greater Than Or Equal To**
* gte(**_parameters_)
* **Not Equal To**
* ne(**_parameters_)
* **Equal to**
* eq(**_parameters_) // alias: where
**Warning**: We may change the way to use filtering functions in the
near future, and favor a parameter-suffix based approach (similar to
Django)
#### Sorting/Ordering #### Sorting/Ordering
......
...@@ -15,6 +15,7 @@ import json ...@@ -15,6 +15,7 @@ import json
import collections import collections
import copy import copy
class QueryResourceDoesNotExist(Exception): class QueryResourceDoesNotExist(Exception):
'''Query returned no results''' '''Query returned no results'''
pass pass
...@@ -26,6 +27,7 @@ class QueryResourceMultipleResultsReturned(Exception): ...@@ -26,6 +27,7 @@ class QueryResourceMultipleResultsReturned(Exception):
class QueryManager(object): class QueryManager(object):
def __init__(self, model_class): def __init__(self, model_class):
self.model_class = model_class self.model_class = model_class
...@@ -37,29 +39,14 @@ class QueryManager(object): ...@@ -37,29 +39,14 @@ class QueryManager(object):
def all(self): def all(self):
return Queryset(self) return Queryset(self)
def where(self, **kw): def filter(self, **kw):
return self.all().where(**kw) return self.all().filter(**kw)
def lt(self, **kw):
return self.all().lt(**kw)
def lte(self, **kw):
return self.all().lte(**kw)
def ne(self, **kw):
return self.all().ne(**kw)
def gt(self, **kw):
return self.all().gt(**kw)
def gte(self, **kw):
return self.all().gte(**kw)
def fetch(self): def fetch(self):
return self.all().fetch() return self.all().fetch()
def get(self, **kw): def get(self, **kw):
return self.where(**kw).get() return self.filter(**kw).get()
class QuerysetMetaclass(type): class QuerysetMetaclass(type):
...@@ -67,15 +54,6 @@ class QuerysetMetaclass(type): ...@@ -67,15 +54,6 @@ class QuerysetMetaclass(type):
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct) cls = super(QuerysetMetaclass, cls).__new__(cls, name, bases, dct)
# add comparison functions and option functions
for fname in ['lt', 'lte', 'gt', 'gte', 'ne']:
def func(self, fname=fname, **kwargs):
s = copy.deepcopy(self)
for k, v in kwargs.items():
s._where[k]['$' + fname] = cls.convert_to_parse(v)
return s
setattr(cls, fname, func)
for fname in ['limit', 'skip']: for fname in ['limit', 'skip']:
def func(self, value, fname=fname): def func(self, value, fname=fname):
s = copy.deepcopy(self) s = copy.deepcopy(self)
...@@ -89,11 +67,23 @@ class QuerysetMetaclass(type): ...@@ -89,11 +67,23 @@ class QuerysetMetaclass(type):
class Queryset(object): class Queryset(object):
__metaclass__ = QuerysetMetaclass __metaclass__ = QuerysetMetaclass
OPERATORS = [
'lt', 'lte', 'gt', 'gte', 'ne', 'in', 'nin', 'exists', 'select', 'dontSelect', 'all'
]
@staticmethod @staticmethod
def convert_to_parse(value): def convert_to_parse(value):
from datatypes import ParseType from datatypes import ParseType
return ParseType.convert_to_parse(value) return ParseType.convert_to_parse(value)
@classmethod
def extract_filter_operator(cls, parameter):
for op in cls.OPERATORS:
underscored = '__%s' % op
if parameter.endswith(underscored):
return parameter[:-len(underscored)], op
return parameter, None
def __init__(self, manager): def __init__(self, manager):
self._manager = manager self._manager = manager
self._where = collections.defaultdict(dict) self._where = collections.defaultdict(dict)
...@@ -111,12 +101,14 @@ class Queryset(object): ...@@ -111,12 +101,14 @@ class Queryset(object):
return self._manager._fetch(**options) return self._manager._fetch(**options)
def where(self, **kw): def filter(self, **kw):
return self.eq(**kw)
def eq(self, **kw):
for name, value in kw.items(): for name, value in kw.items():
self._where[name] = Queryset.convert_to_parse(value) parse_value = Queryset.convert_to_parse(value)
attr, operator = Queryset.extract_filter_operator(name)
if operator is None:
self._where[attr] = parse_value
else:
self._where[attr]['$' + operator] = parse_value
return self return self
def order_by(self, order, descending=False): def order_by(self, order, descending=False):
......
...@@ -76,11 +76,11 @@ class TestObject(unittest.TestCase): ...@@ -76,11 +76,11 @@ class TestObject(unittest.TestCase):
city_name = getattr(self.sao_paulo, 'name', None) city_name = getattr(self.sao_paulo, 'name', None)
game_score = getattr(self.score, 'score', None) game_score = getattr(self.score, 'score', None)
if city_name: if city_name:
for city in City.Query.where(name=city_name): for city in City.Query.filter(name=city_name):
city.delete() city.delete()
if game_score: if game_score:
for score in GameScore.Query.where(score=game_score): for score in GameScore.Query.filter(score=game_score):
score.delete() score.delete()
def testCanInitialize(self): def testCanInitialize(self):
...@@ -102,7 +102,7 @@ class TestObject(unittest.TestCase): ...@@ -102,7 +102,7 @@ class TestObject(unittest.TestCase):
self.assert_(object_id is not None, 'Can not create object') self.assert_(object_id is not None, 'Can not create object')
self.assert_(type(object_id) == unicode) self.assert_(type(object_id) == unicode)
self.assert_(type(self.score.createdAt) == datetime.datetime) self.assert_(type(self.score.createdAt) == datetime.datetime)
self.assert_(GameScore.Query.where(objectId=object_id).exists(), self.assert_(GameScore.Query.filter(objectId=object_id).exists(),
'Can not create object') 'Can not create object')
def testCanUpdateExistingObject(self): def testCanUpdateExistingObject(self):
...@@ -118,14 +118,14 @@ class TestObject(unittest.TestCase): ...@@ -118,14 +118,14 @@ class TestObject(unittest.TestCase):
self.score.save() self.score.save()
object_id = self.score.objectId object_id = self.score.objectId
self.score.delete() self.score.delete()
self.assert_(not GameScore.Query.where(objectId=object_id).exists(), self.assert_(not GameScore.Query.filter(objectId=object_id).exists(),
'Failed to delete object %s on Parse ' % self.score) 'Failed to delete object %s on Parse ' % self.score)
def testCanIncrementField(self): def testCanIncrementField(self):
previous_score = self.score.score previous_score = self.score.score
self.score.save() self.score.save()
self.score.increment('score') self.score.increment('score')
self.assert_(GameScore.Query.where(score=previous_score + 1).exists(), self.assert_(GameScore.Query.filter(score=previous_score + 1).exists(),
'Failed to increment score on backend') 'Failed to increment score on backend')
def testAssociatedObject(self): def testAssociatedObject(self):
...@@ -194,61 +194,62 @@ class TestQuery(unittest.TestCase): ...@@ -194,61 +194,62 @@ class TestQuery(unittest.TestCase):
def testExists(self): def testExists(self):
"""test the Queryset.exists() method""" """test the Queryset.exists() method"""
for s in range(1, 6): for s in range(1, 6):
self.assert_(GameScore.Query.where(score=s).exists(), self.assert_(GameScore.Query.filter(score=s).exists(),
"exists giving false negative") "exists giving false negative")
self.assert_(not GameScore.Query.where(score=10).exists(), self.assert_(not GameScore.Query.filter(score=10).exists(),
"exists giving false positive") "exists giving false positive")
def testWhereGet(self): def testCanFilter(self):
"""test the Queryset.where() and Queryset.get() methods""" '''test the Queryset.filter() method'''
for s in self.scores: for s in self.scores:
qobj = GameScore.Query.where(objectId=s.objectId).get() qobj = GameScore.Query.filter(objectId=s.objectId).get()
self.assert_(qobj.objectId == s.objectId, self.assert_(qobj.objectId == s.objectId,
"Getting object with .where() failed") "Getting object with .filter() failed")
self.assert_(qobj.score == s.score, self.assert_(qobj.score == s.score,
"Getting object with .where() failed") "Getting object with .filter() failed")
# test the two exceptions get can raise def testGetExceptions(self):
'''test possible exceptions raised by Queryset.get() method'''
self.assertRaises(query.QueryResourceDoesNotExist, self.assertRaises(query.QueryResourceDoesNotExist,
GameScore.Query.gt(score=20).get) GameScore.Query.filter(score__gt=20).get)
self.assertRaises(query.QueryResourceMultipleResultsReturned, self.assertRaises(query.QueryResourceMultipleResultsReturned,
GameScore.Query.gt(score=3).get) GameScore.Query.filter(score__gt=3).get)
def testCanQueryDates(self): def testCanQueryDates(self):
last_week = datetime.datetime.now() - datetime.timedelta(days=7) last_week = datetime.datetime.now() - datetime.timedelta(days=7)
score = GameScore(name='test', last_played=last_week) score = GameScore(name='test', last_played=last_week)
score.save() score.save()
self.assert_(GameScore.Query.where(last_played=last_week).exists(), self.assert_(GameScore.Query.filter(last_played=last_week).exists(),
'Could not run query with dates') 'Could not run query with dates')
def testComparisons(self): def testComparisons(self):
"""test comparison operators- gt, gte, lt, lte, ne""" """test comparison operators- gt, gte, lt, lte, ne"""
scores_gt_3 = list(GameScore.Query.all().gt(score=3)) scores_gt_3 = list(GameScore.Query.filter(score__gt=3))
self.assertEqual(len(scores_gt_3), 2) self.assertEqual(len(scores_gt_3), 2)
self.assert_(all([s.score > 3 for s in scores_gt_3])) self.assert_(all([s.score > 3 for s in scores_gt_3]))
scores_gte_3 = list(GameScore.Query.all().gte(score=3)) scores_gte_3 = list(GameScore.Query.filter(score__gte=3))
self.assertEqual(len(scores_gte_3), 3) self.assertEqual(len(scores_gte_3), 3)
self.assert_(all([s.score >= 3 for s in scores_gt_3])) self.assert_(all([s.score >= 3 for s in scores_gt_3]))
scores_lt_4 = list(GameScore.Query.all().lt(score=4)) scores_lt_4 = list(GameScore.Query.filter(score__lt=4))
self.assertEqual(len(scores_lt_4), 3) self.assertEqual(len(scores_lt_4), 3)
self.assert_(all([s.score < 4 for s in scores_lt_4])) self.assert_(all([s.score < 4 for s in scores_lt_4]))
scores_lte_4 = list(GameScore.Query.all().lte(score=4)) scores_lte_4 = list(GameScore.Query.filter(score__lte=4))
self.assertEqual(len(scores_lte_4), 4) self.assertEqual(len(scores_lte_4), 4)
self.assert_(all([s.score <= 4 for s in scores_lte_4])) self.assert_(all([s.score <= 4 for s in scores_lte_4]))
scores_ne_2 = list(GameScore.Query.all().ne(score=2)) scores_ne_2 = list(GameScore.Query.filter(score__ne=2))
self.assertEqual(len(scores_ne_2), 4) self.assertEqual(len(scores_ne_2), 4)
self.assert_(all([s.score != 2 for s in scores_ne_2])) self.assert_(all([s.score != 2 for s in scores_ne_2]))
# test chaining # test chaining
lt_4_gt_2 = list(GameScore.Query.all().lt(score=4).gt(score=2)) lt_4_gt_2 = list(GameScore.Query.filter(score__lt=4).filter(score__gt=2))
self.assert_(len(lt_4_gt_2) == 1, "chained lt+gt not working") self.assert_(len(lt_4_gt_2) == 1, 'chained lt+gt not working')
self.assert_(lt_4_gt_2[0].score == 3, "chained lt+gt not working") self.assert_(lt_4_gt_2[0].score == 3, 'chained lt+gt not working')
q = GameScore.Query.all().gt(score=3).lt(score=3) q = GameScore.Query.filter(score__gt=3, score__lt=3)
self.assert_(not q.exists(), "chained lt+gt not working") self.assert_(not q.exists(), "chained lt+gt not working")
def testOptions(self): def testOptions(self):
...@@ -270,22 +271,21 @@ class TestQuery(unittest.TestCase): ...@@ -270,22 +271,21 @@ class TestQuery(unittest.TestCase):
def testCanCompareDateInequality(self): def testCanCompareDateInequality(self):
today = datetime.datetime.today() today = datetime.datetime.today()
tomorrow = today + datetime.timedelta(days=1) tomorrow = today + datetime.timedelta(days=1)
self.assert_(GameScore.Query.lte(createdAt=tomorrow).count() == 5, self.assert_(GameScore.Query.filter(createdAt__lte=tomorrow).count() == 5,
'Could not make inequality comparison with dates') 'Could not make inequality comparison with dates')
def tearDown(self): def tearDown(self):
"""delete all GameScore objects""" '''delete all GameScore objects'''
for s in GameScore.Query.all(): for s in GameScore.Query.all():
s.delete() s.delete()
class TestFunction(unittest.TestCase): class TestFunction(unittest.TestCase):
def setUp(self): def setUp(self):
"""create and deploy cloud functions""" '''create and deploy cloud functions'''
original_dir = os.getcwd() original_dir = os.getcwd()
cloud_function_dir = os.path.join(os.path.split(__file__)[0], cloud_function_dir = os.path.join(os.path.split(__file__)[0], 'cloudcode')
"cloudcode")
os.chdir(cloud_function_dir) os.chdir(cloud_function_dir)
# write the config file # write the config file
with open("config/global.json", "w") as outf: with open("config/global.json", "w") as outf:
...@@ -339,7 +339,7 @@ class TestUser(unittest.TestCase): ...@@ -339,7 +339,7 @@ class TestUser(unittest.TestCase):
user and user.delete() user and user.delete()
def _get_logged_user(self): def _get_logged_user(self):
if User.Query.where(username=self.username).exists(): if User.Query.filter(username=self.username).exists():
return User.login(self.username, self.password) return User.login(self.username, self.password)
else: else:
return self._get_user() return self._get_user()
...@@ -377,7 +377,7 @@ class TestUser(unittest.TestCase): ...@@ -377,7 +377,7 @@ class TestUser(unittest.TestCase):
user.phone = phone_number user.phone = phone_number
user.save() user.save()
self.assert_(User.Query.where(phone=phone_number).exists(), self.assert_(User.Query.filter(phone=phone_number).exists(),
'Failed to update user data. New info not on Parse') 'Failed to update user data. New info not on Parse')
if __name__ == "__main__": if __name__ == "__main__":
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment