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