Commit 9d8ae000 by johnk

Improved tests, fixed logic

Tests with and without pre-existing schema on
the server.  Cleaned up logic pyramids, eliminated
redundant flag. Found bug in test of inserting object
of incorrect type.
parent 9b4a6119
...@@ -136,10 +136,8 @@ class Relation(ParseType): ...@@ -136,10 +136,8 @@ class Relation(ParseType):
return cls(**kw) return cls(**kw)
def with_parent(self, **kw): def with_parent(self, **kw):
"""This is called on this object if it's been instantiated via """The parent calls this if the Relation already exists.
from_native, when it's retrieved from the server. After the parentObject and key are set, queries can be made.
with_parent "completes" the objects, injecting the parentObject and
key, so queries can be formed.
""" """
if 'parentObject' in kw: if 'parentObject' in kw:
self.parentObject = kw['parentObject'] self.parentObject = kw['parentObject']
...@@ -148,41 +146,34 @@ class Relation(ParseType): ...@@ -148,41 +146,34 @@ class Relation(ParseType):
def __init__(self, **kw): def __init__(self, **kw):
""" """
This constructor can be called either directly, or via from_native. Called either via Relation(), or via from_native().
In both cases, the Relation object is incomplete and cannot perform In both cases, the Relation object is incomplete,
queries until we know the parentObject, key on the parentObject, and and cannot perform queries.
the relatedClassName.
If it's called via from_native, then, a later call to with_parent will If it's called via from_native, then, a later call to
complete the Relation. with_parent() will complete the Relation.
If it's called directly, the relatedClassName is discovered either on If it's called directly, the relatedClassName is
the first added object, or by probing the server to retrieve an object. discovered either on the first added object, or
by probing the server to retrieve an object.
""" """
# Name of the key on the parent object. # Name of the key on the parent object.
self.key = None self.key = None
# The object of which this Relation is a field.
self.parentObject = None self.parentObject = None
# Name of the class with the related objects.
self.relatedClassName = None self.relatedClassName = None
# If we're called from from_native, we only know the related class. # Called via from_native()
# There is no way to know the parent object at this time, so we return.
if 'className' in kw: if 'className' in kw:
self.relatedClassName = kw['className'] self.relatedClassName = kw['className']
# If we're called to create a new Relation, the parentObject must # Called via Relation(...)
# be specified. We also defer creation of the relation until the
# first object is added.
if 'parentObject' in kw: if 'parentObject' in kw:
self.parentObject = kw['parentObject'] self.parentObject = kw['parentObject']
self.key = kw['key'] self.key = kw['key']
self._defer_relation_creation = True # self._defer_relation_creation = True
def _probe_for_relation_class(self): def _probe_for_relation_class(self):
"""Retrive the schema from the server to find out the related class. """Retrive the schema from the server to find related class."""
If this fails, there's no way to discover the class, raise an error.
"""
schema = self.parentObject.__class__.schema() schema = self.parentObject.__class__.schema()
fields = schema['fields'] fields = schema['fields']
relatedColumn = fields[self.key] relatedColumn = fields[self.key]
...@@ -190,11 +181,14 @@ class Relation(ParseType): ...@@ -190,11 +181,14 @@ class Relation(ParseType):
if columnType == 'Relation': if columnType == 'Relation':
self.relatedClassName = relatedColumn['targetClass'] self.relatedClassName = relatedColumn['targetClass']
else: else:
raise ParseError('Column type is %s, expected Relation' % (columnType,)) raise ParseError(
'Column type is %s, expected Relation' % (columnType,))
def _create_new_relation(self, obj): def _create_new_relation(self, obj):
self.relatedClassName = obj.className self.relatedClassName = obj.className
self.parentObject.__dict__[self.key] = self # create the column on the parent
setattr(self.parentObject, self.key, self)
# self._defer_relation_creation = False
def __repr__(self): def __repr__(self):
className = objectId = None className = objectId = None
...@@ -216,7 +210,8 @@ class Relation(ParseType): ...@@ -216,7 +210,8 @@ class Relation(ParseType):
def add(self, objs): def add(self, objs):
if type(objs) is not list: if type(objs) is not list:
objs = [objs] objs = [objs]
if self._defer_relation_creation: #if self._defer_relation_creation:
if self.relatedClassName is None:
self._create_new_relation(objs[0]) self._create_new_relation(objs[0])
objectsId = [] objectsId = []
for obj in objs: for obj in objs:
...@@ -551,13 +546,25 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)): ...@@ -551,13 +546,25 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)):
@classmethod @classmethod
def schema(cls): def schema(cls):
""" """Retrieves the class' schema."""
Retrieves the class' schema.
"""
root = '/'.join([API_ROOT, 'schemas', cls.__name__]) root = '/'.join([API_ROOT, 'schemas', cls.__name__])
schema = cls.GET(root) schema = cls.GET(root)
return schema return schema
@classmethod
def schema_delete_field(cls, key):
"""Deletes a field."""
root = '/'.join([API_ROOT, 'schemas', cls.__name__])
payload = {
'className': cls.__name__,
'fields': {
key: {
'__op': 'Delete'
}
}
}
cls.PUT(root, **payload)
@property @property
def _absolute_url(self): def _absolute_url(self):
if not self.objectId: if not self.objectId:
...@@ -618,10 +625,9 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)): ...@@ -618,10 +625,9 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)):
# self.__dict__[key] = '' # self.__dict__[key] = ''
def relation(self, key): def relation(self, key):
if hasattr(self, key): if not hasattr(self, key):
return Relation(parentObject=self, key=key)
try: try:
return getattr(self, key).with_parent(parentObject=self, key=key) return getattr(self, key).with_parent(parentObject=self, key=key)
except: except:
raise ParseError("Column '%s' is not a Relation." % (key,)) raise ParseError("Column '%s' is not a Relation." % (key,))
else:
return Relation(parentObject=self, key=key)
...@@ -60,7 +60,32 @@ class GameScore(Object): ...@@ -60,7 +60,32 @@ class GameScore(Object):
pass pass
class TestNoRelation(unittest.TestCase):
def setUp(self):
try:
Game.schema_delete_field('scores')
except ResourceRequestBadRequest:
# fails if the field doesn't exist
pass
self.game = Game(name="foobar")
def testQueryWithNoRelationOnline(self):
"""If the online schema lacks the relation, we cannot query."""
with self.assertRaises(KeyError):
rel = self.game.relation('scores')
rel.query()
class TestRelation(unittest.TestCase): class TestRelation(unittest.TestCase):
@classmethod
def setUpClass(cls):
# prime the schema with a relation field for GameScore
score1 = GameScore(score=1337, player_name='John Doe', cheat_mode=False)
game = Game(name="foobar")
game.save()
rel = game.relation('scores')
rel.add(score1)
def setUp(self): def setUp(self):
self.score1 = GameScore(score=1337, player_name='John Doe', cheat_mode=False) self.score1 = GameScore(score=1337, player_name='John Doe', cheat_mode=False)
self.score2 = GameScore(score=1337, player_name='Jane Doe', cheat_mode=False) self.score2 = GameScore(score=1337, player_name='Jane Doe', cheat_mode=False)
...@@ -78,6 +103,10 @@ class TestRelation(unittest.TestCase): ...@@ -78,6 +103,10 @@ class TestRelation(unittest.TestCase):
if game_name: if game_name:
ParseBatcher().batch_delete(Game.Query.filter(name=game_name)) ParseBatcher().batch_delete(Game.Query.filter(name=game_name))
@classmethod
def tearDownClass(cls):
Game.schema_delete_field('scores')
def testRelationsAdd(self): def testRelationsAdd(self):
"""Add multiple objects to a relation.""" """Add multiple objects to a relation."""
self.rel.add(self.score1) self.rel.add(self.score1)
...@@ -119,12 +148,14 @@ class TestRelation(unittest.TestCase): ...@@ -119,12 +148,14 @@ class TestRelation(unittest.TestCase):
self.assertEqual(fields['scores']['type'], 'Relation') self.assertEqual(fields['scores']['type'], 'Relation')
def testWrongType(self): def testWrongType(self):
"""You can't add two different classes to a relation.""" """Adding wrong type fails silently."""
with self.assertRaises(ResourceRequestBadRequest):
self.rel.add(self.score1) self.rel.add(self.score1)
self.rel.add(self.game) self.rel.add(self.score2)
self.rel.add(self.game) # should fail to add this
scores = self.rel.query()
self.assertEqual(len(scores), 2)
def testNoTypeSet(self): def testNoTypeSetParseHasColumn(self):
"""Query can run before anything is added to the relation, """Query can run before anything is added to the relation,
if the schema online has already defined the relation. if the schema online has already defined the relation.
""" """
...@@ -137,15 +168,15 @@ class TestRelation(unittest.TestCase): ...@@ -137,15 +168,15 @@ class TestRelation(unittest.TestCase):
""" """
with self.assertRaises(ParseError): with self.assertRaises(ParseError):
rel = self.game.relation("name") rel = self.game.relation("name")
bad = rel.query() rel.query()
def testWrongColumnForRelation(self): def testNonexistentColumnForRelation(self):
"""Should get a ParseError if we specify a relation on """Should get a ParseError if we specify a relation on
a column that is not a relation. a column that is not a relation.
""" """
with self.assertRaises(KeyError): with self.assertRaises(KeyError):
rel = self.game.relation("nonexistent") rel = self.game.relation("nonexistent")
bad = rel.query() rel.query()
def testRepr(self): def testRepr(self):
s = "*** %s ***" % (self.rel) s = "*** %s ***" % (self.rel)
...@@ -157,9 +188,10 @@ class TestRelation(unittest.TestCase): ...@@ -157,9 +188,10 @@ class TestRelation(unittest.TestCase):
retrieved from the server. This test is for code coverage. retrieved from the server. This test is for code coverage.
""" """
game2 = Game.Query.get(objectId=self.game.objectId) game2 = Game.Query.get(objectId=self.game.objectId)
# self.assertTrue(hasattr(game2, 'scores')) self.assertTrue(hasattr(game2, 'scores'))
rel2 = game2.relation('scores') rel2 = game2.relation('scores')
# self.assertIsInstance(rel2, Relation) self.assertIsInstance(rel2, Relation)
def run_tests(): def run_tests():
"""Run all tests in the parse_rest package""" """Run all tests in the parse_rest package"""
......
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