Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
ParsePy
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
OpenEdx
ParsePy
Commits
9fce3b0d
Commit
9fce3b0d
authored
Jul 28, 2016
by
Miles Richardson
Committed by
GitHub
Jul 28, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #131 from johnkawakami/master
Adds additional support for Relations.
parents
31f23b19
bc210077
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
390 additions
and
2 deletions
+390
-2
README.mkd
+53
-0
parse_rest/datatypes.py
+133
-2
parse_rest/test_relations.py
+204
-0
No files found.
README.mkd
View file @
9fce3b0d
...
...
@@ -453,6 +453,59 @@ for post in posts_by_joe:
**TODO**
: Slicing of Querysets
Relations
---------
A Relation is field that contains references to multiple objects.
You can query this subset of objects.
(Note that Parse's relations are "one sided" and don't involve a join table.
[
See the docs.
](
https://parse.com/docs/js/guide#relations-many-to-many
)
)
For example, if we have Game and GameScore classes, and one game
can have multiple GameScores, you can use relations to associate
those GameScores with a Game.
~~~
~~ {python}
game = Game(name="3-way Battle")
game.save()
score1 = GameScore(player_name='Ronald', score=100)
score2 = GameScore(player_name='Rebecca', score=140)
score3 = GameScore(player_name='Sara', score=190)
relation = game.relation('scores')
relation.add([score1, score2, score3])
~~~
~~
A Game gets added, three GameScores get added, and three relations
are created associating the GameScores with the Game.
To retreive the related scores for a game, you use query() to get a
Queryset for the relation.
~~~
~~ {python}
scores = relation.query()
for gamescore in scores:
print gamescore.player_name, gamescore.score
~~~
~~
The query is limited to the objects previously added to the
relation.
~~~
~~ {python}
scores = relation.query().order_by('score', descending=True)
for gamescore in scores:
print gamescore.player_name, gamescore.score
~~~
~~
To remove objects from a relation, you use remove(). This example
removes all the related objects.
~~~
~~ {python}
scores = relation.query()
for gamescore in scores:
relation.remove(gamescore)
~~~
~~
Users
-----
...
...
parse_rest/datatypes.py
View file @
9fce3b0d
...
...
@@ -130,7 +130,109 @@ class EmbeddedObject(ParseType):
class
Relation
(
ParseType
):
@classmethod
def
from_native
(
cls
,
**
kw
):
pass
return
cls
(
**
kw
)
def
with_parent
(
self
,
**
kw
):
"""The parent calls this if the Relation already exists."""
if
'parentObject'
in
kw
:
self
.
parentObject
=
kw
[
'parentObject'
]
self
.
key
=
kw
[
'key'
]
return
self
def
__init__
(
self
,
**
kw
):
"""Called either via Relation(), or via from_native().
In both cases, the Relation object cannot perform
queries until we know what classes are on both sides
of the relation.
If it's called via from_native, then a later call to
with_parent() provides parent information.
If it's called as Relation(), the relatedClassName is
discovered either on the first added object, or
by querying the server to retrieve the schema.
"""
# Name of the key on the parent object.
self
.
key
=
None
self
.
parentObject
=
None
self
.
relatedClassName
=
None
# Called via from_native()
if
'className'
in
kw
:
self
.
relatedClassName
=
kw
[
'className'
]
# Called via Relation(...)
if
'parentObject'
in
kw
:
self
.
parentObject
=
kw
[
'parentObject'
]
self
.
key
=
kw
[
'key'
]
def
__repr__
(
self
):
className
=
objectId
=
None
if
self
.
parentObject
is
not
None
:
className
=
self
.
parentObject
.
className
objectId
=
self
.
parentObject
.
objectId
repr
=
u'<Relation where
%
s:
%
s for
%
s>'
%
\
(
className
,
objectId
,
self
.
relatedClassName
)
return
repr
def
_to_native
(
self
):
return
{
'__type'
:
'Relation'
,
'className'
:
self
.
relatedClassName
}
def
add
(
self
,
objs
):
"""Adds a Parse.Object or an array of Parse.Objects to the relation."""
if
type
(
objs
)
is
not
list
:
objs
=
[
objs
]
if
self
.
relatedClassName
is
None
:
# find the related class from the first object added
self
.
relatedClassName
=
objs
[
0
]
.
className
setattr
(
self
.
parentObject
,
self
.
key
,
self
)
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
):
"""Removes an array of, or one Parse.Object from this relation."""
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
):
"""Returns a Parse.Query limited to objects in this relation."""
if
self
.
relatedClassName
is
None
:
self
.
_probe_for_relation_class
()
key
=
'
%
s__relatedTo'
%
(
self
.
key
,)
kw
=
{
key
:
self
.
parentObject
}
relatedClass
=
Object
.
factory
(
self
.
relatedClassName
)
q
=
relatedClass
.
Query
.
all
()
.
filter
(
**
kw
)
return
q
def
_probe_for_relation_class
(
self
):
"""Retrive the schema from the server to find related class."""
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
,))
@complex_type
()
...
...
@@ -443,6 +545,27 @@ 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
@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
:
...
...
@@ -500,4 +623,12 @@ 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
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
,))
parse_rest/test_relations.py
0 → 100755
View file @
9fce3b0d
#!/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.core
import
ParseError
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
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
)
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
))
@classmethod
def
tearDownClass
(
cls
):
Game
.
schema_delete_field
(
'scores'
)
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
):
"""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
testNoTypeSetParseHasColumn
(
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
testWrongColumnForRelation
(
self
):
"""Should get a ParseError if we specify a relation on
a column that is not a relation.
"""
with
self
.
assertRaises
(
ParseError
):
rel
=
self
.
game
.
relation
(
"name"
)
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"
)
rel
.
query
()
def
testRepr
(
self
):
s
=
"***
%
s ***"
%
(
self
.
rel
)
self
.
assertRegex
(
s
,
'<Relation where .+ for .+>'
)
def
testWithParent
(
self
):
"""Rehydrating a relation from an instance on the server.
With_parent is called by relation() when the object was
retrieved from the server. This test is for code coverage.
"""
game2
=
Game
.
Query
.
get
(
objectId
=
self
.
game
.
objectId
)
self
.
assertTrue
(
hasattr
(
game2
,
'scores'
))
rel2
=
game2
.
relation
(
'scores'
)
self
.
assertIsInstance
(
rel2
,
Relation
)
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
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment