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.
#### 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}
high_scores = GameScore.Query.all().gte(score=1000)
high_scores = GameScore.Query.filter(score__gte=1000)
~~~~~
The available filter functions are:
* **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)
You can see the [full list of constraint operators defined by
Parse](https://www.parse.com/docs/rest#queries-constraints)
#### Sorting/Ordering
......
......@@ -15,6 +15,7 @@ import json
import collections
import copy
class QueryResourceDoesNotExist(Exception):
'''Query returned no results'''
pass
......@@ -26,6 +27,7 @@ class QueryResourceMultipleResultsReturned(Exception):
class QueryManager(object):
def __init__(self, model_class):
self.model_class = model_class
......@@ -37,29 +39,14 @@ class QueryManager(object):
def all(self):
return Queryset(self)
def where(self, **kw):
return self.all().where(**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 filter(self, **kw):
return self.all().filter(**kw)
def fetch(self):
return self.all().fetch()
def get(self, **kw):
return self.where(**kw).get()
return self.filter(**kw).get()
class QuerysetMetaclass(type):
......@@ -67,15 +54,6 @@ class QuerysetMetaclass(type):
def __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']:
def func(self, value, fname=fname):
s = copy.deepcopy(self)
......@@ -89,11 +67,23 @@ class QuerysetMetaclass(type):
class Queryset(object):
__metaclass__ = QuerysetMetaclass
OPERATORS = [
'lt', 'lte', 'gt', 'gte', 'ne', 'in', 'nin', 'exists', 'select', 'dontSelect', 'all'
]
@staticmethod
def convert_to_parse(value):
from datatypes import ParseType
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):
self._manager = manager
self._where = collections.defaultdict(dict)
......@@ -111,12 +101,14 @@ class Queryset(object):
return self._manager._fetch(**options)
def where(self, **kw):
return self.eq(**kw)
def eq(self, **kw):
def filter(self, **kw):
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
def order_by(self, order, descending=False):
......
......@@ -76,11 +76,11 @@ class TestObject(unittest.TestCase):
city_name = getattr(self.sao_paulo, 'name', None)
game_score = getattr(self.score, 'score', None)
if city_name:
for city in City.Query.where(name=city_name):
for city in City.Query.filter(name=city_name):
city.delete()
if game_score:
for score in GameScore.Query.where(score=game_score):
for score in GameScore.Query.filter(score=game_score):
score.delete()
def testCanInitialize(self):
......@@ -102,7 +102,7 @@ class TestObject(unittest.TestCase):
self.assert_(object_id is not None, 'Can not create object')
self.assert_(type(object_id) == unicode)
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')
def testCanUpdateExistingObject(self):
......@@ -118,14 +118,14 @@ class TestObject(unittest.TestCase):
self.score.save()
object_id = self.score.objectId
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)
def testCanIncrementField(self):
previous_score = self.score.score
self.score.save()
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')
def testAssociatedObject(self):
......@@ -194,61 +194,62 @@ class TestQuery(unittest.TestCase):
def testExists(self):
"""test the Queryset.exists() method"""
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")
self.assert_(not GameScore.Query.where(score=10).exists(),
self.assert_(not GameScore.Query.filter(score=10).exists(),
"exists giving false positive")
def testWhereGet(self):
"""test the Queryset.where() and Queryset.get() methods"""
def testCanFilter(self):
'''test the Queryset.filter() method'''
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,
"Getting object with .where() failed")
"Getting object with .filter() failed")
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,
GameScore.Query.gt(score=20).get)
GameScore.Query.filter(score__gt=20).get)
self.assertRaises(query.QueryResourceMultipleResultsReturned,
GameScore.Query.gt(score=3).get)
GameScore.Query.filter(score__gt=3).get)
def testCanQueryDates(self):
last_week = datetime.datetime.now() - datetime.timedelta(days=7)
score = GameScore(name='test', last_played=last_week)
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')
def testComparisons(self):
"""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.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.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.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.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.assert_(all([s.score != 2 for s in scores_ne_2]))
# test chaining
lt_4_gt_2 = list(GameScore.Query.all().lt(score=4).gt(score=2))
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")
q = GameScore.Query.all().gt(score=3).lt(score=3)
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_(lt_4_gt_2[0].score == 3, 'chained lt+gt not working')
q = GameScore.Query.filter(score__gt=3, score__lt=3)
self.assert_(not q.exists(), "chained lt+gt not working")
def testOptions(self):
......@@ -270,22 +271,21 @@ class TestQuery(unittest.TestCase):
def testCanCompareDateInequality(self):
today = datetime.datetime.today()
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')
def tearDown(self):
"""delete all GameScore objects"""
'''delete all GameScore objects'''
for s in GameScore.Query.all():
s.delete()
class TestFunction(unittest.TestCase):
def setUp(self):
"""create and deploy cloud functions"""
'''create and deploy cloud functions'''
original_dir = os.getcwd()
cloud_function_dir = os.path.join(os.path.split(__file__)[0],
"cloudcode")
cloud_function_dir = os.path.join(os.path.split(__file__)[0], 'cloudcode')
os.chdir(cloud_function_dir)
# write the config file
with open("config/global.json", "w") as outf:
......@@ -339,7 +339,7 @@ class TestUser(unittest.TestCase):
user and user.delete()
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)
else:
return self._get_user()
......@@ -377,7 +377,7 @@ class TestUser(unittest.TestCase):
user.phone = phone_number
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')
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