Commit 6bf7dbd1 by johnk

Clone of Parse.Relation

Adds a relation() method to objects. It returns a Relation
with methods to add(), remove(), and query() for related
objects.
parent bfb76788
......@@ -130,7 +130,121 @@ class EmbeddedObject(ParseType):
class Relation(ParseType):
@classmethod
def from_native(cls, **kw):
pass
"""When rehydrating from native, we know only the className of the
destination. kw looks like { 'className': 'SomeClass' }
"""
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.
"""
if 'parentObject' in kw:
self.parentObject = kw['parentObject']
self.key = kw['key']
return self
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.
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.
"""
# 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.
if 'ClassName' in kw:
self.relatedClassName = kw['className']
return self
# 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.
if 'parentObject' in kw:
self.parentObject = kw['parentObject']
self.key = kw['key']
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.
"""
schema = self.parentObject.__class__.schema()
fields = schema['fields']
relatedColumn = fields[self.key]
columnType = relatedColumn['type']
if columnType == 'Relation':
self.relatedClassName = relatedColumn['targetClass']
else:
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
def __repr__(self):
repr = u'<Relation where %s:%s for %s>' % \
(self.parentObject.className,
self.parentObject.objectId,
self.relatedClassName)
return repr
def _to_native(self):
return {
'__type': 'Relation',
'className': self.relatedClassName
}
def add(self, objs):
if type(objs) is not list:
objs = [objs]
if self._defer_relation_creation:
self._create_new_relation(objs[0])
objectsId = []
for obj in objs:
if not hasattr(obj, 'objectId') or obj.objectId is None:
obj.save()
objectsId.append(obj.objectId)
self.parentObject.addRelation(self.key,
self.relatedClassName,
objectsId)
def remove(self, objs):
if type(objs) is not list:
objs = [objs]
objectsId = []
for obj in objs:
if hasattr(obj, 'objectId'):
objectsId.append(obj.objectId)
self.parentObject.removeRelation(self.key,
self.relatedClassName,
objectsId)
def query(self):
if self.relatedClassName is None:
self._probe_for_relation_class()
key = '%s__relatedTo' % (self.key,)
kw = {key: self.parentObject}
if not hasattr(self, 'relatedClassName'):
self._probe_for_relation_class()
relatedClass = Object.factory(self.relatedClassName)
q = relatedClass.Query.all().filter(**kw)
return q
@complex_type()
......@@ -434,6 +548,15 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)):
cls.ENDPOINT_ROOT = root
return cls.ENDPOINT_ROOT
@classmethod
def schema(cls):
"""
Retrieves the class' schema.
"""
root = '/'.join([API_ROOT, 'schemas', cls.__name__])
schema = cls.GET(root)
return schema
@property
def _absolute_url(self):
if not self.objectId:
......@@ -491,4 +614,10 @@ class Object(six.with_metaclass(ObjectMetaclass, ParseResource)):
}
}
self.__class__.PUT(self._absolute_url, **payload)
self.__dict__[key] = ''
# self.__dict__[key] = ''
def relation(self, key):
if hasattr(self, key):
return getattr(self, key).with_parent(parentObject=self, key=key)
else:
return Relation(parentObject=self, key=key)
#!/usr/bin/env python
#-*- coding: utf-8 -*-
"""
Contains unit tests for the Python Parse REST API wrapper
"""
from __future__ import print_function
import os
import sys
import subprocess
import unittest
import datetime
import six
from itertools import chain
from parse_rest.core import ResourceRequestNotFound
from parse_rest.core import ResourceRequestBadRequest
from parse_rest.connection import register, ParseBatcher
from parse_rest.datatypes import GeoPoint, Object, Function, Pointer, Relation
from parse_rest.user import User
from parse_rest import query
from parse_rest.installation import Push
try:
import settings_local
except ImportError:
sys.exit('You must create a settings_local.py file with APPLICATION_ID, ' \
'REST_API_KEY, MASTER_KEY variables set')
register(
getattr(settings_local, 'APPLICATION_ID'),
getattr(settings_local, 'REST_API_KEY'),
master_key=getattr(settings_local, 'MASTER_KEY')
)
GLOBAL_JSON_TEXT = """{
"applications": {
"_default": {
"link": "parseapi"
},
"parseapi": {
"applicationId": "%s",
"masterKey": "%s"
}
},
"global": {
"parseVersion": "1.1.16"
}
}
"""
class Game(Object):
pass
class GameScore(Object):
pass
class TestRelation(unittest.TestCase):
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)
self.score3 = GameScore(score=1337, player_name='Joan Doe', cheat_mode=False)
self.score4 = GameScore(score=1337, player_name='Jeff Doe', cheat_mode=False)
self.game = Game(name="foobar")
self.game.save()
self.rel = self.game.relation('scores')
def tearDown(self):
game_score = getattr(self.score1, 'score', None)
game_name = getattr(self.game, 'name', None)
if game_score:
ParseBatcher().batch_delete(GameScore.Query.filter(score=game_score))
if game_name:
ParseBatcher().batch_delete(Game.Query.filter(name=game_name))
def testRelationsAdd(self):
"""Add multiple objects to a relation."""
self.rel.add(self.score1)
scores = self.rel.query()
self.assertEqual(len(scores), 1)
self.assertEqual(scores[0].player_name, 'John Doe')
self.rel.add(self.score2)
scores = self.rel.query()
self.assertEqual(len(scores), 2)
self.rel.add([self.score3, self.score4])
scores = self.rel.query()
self.assertEqual(len(scores), 4)
def testRelationQueryLimitsToRelation(self):
"""Relational query limits results to objects in the relation."""
self.rel.add([self.score1, self.score2])
gamescore3 = GameScore(score=1337)
gamescore3.save()
# score saved but not associated with the game
q = self.rel.query()
scores = q.filter(score__gte=1337)
self.assertEqual(len(scores), 2)
def testRemoval(self):
"""Test if a specific object can be removed from a relation."""
self.rel.add([self.score1, self.score2, self.score3])
self.rel.remove(self.score1)
self.rel.remove(self.score2)
scores = self.rel.query()
self.assertEqual(scores[0].player_name, 'Joan Doe')
def testSchema(self):
"""Retrieve a schema for the class."""
schema = Game.schema()
self.assertEqual(schema['className'], 'Game')
fields = schema['fields']
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)
def testNoTypeSet(self):
"""Query can run before anything is added to the relation,
if the schema online has already defined the relation.
"""
scores = self.rel.query()
self.assertEqual(len(scores), 0)
def run_tests():
"""Run all tests in the parse_rest package"""
tests = unittest.TestLoader().loadTestsFromNames(['parse_rest.tests'])
t = unittest.TextTestRunner(verbosity=1)
t.run(tests)
if __name__ == "__main__":
# command line
unittest.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