Skip to content
Snippets Groups Projects
Commit 629d3534 authored by Calum Mackervoy's avatar Calum Mackervoy
Browse files

recursive solution supporting nested objects and intermediate relations

parent 137cebad
No related branches found
Tags v0.6.25
No related merge requests found
......@@ -143,6 +143,18 @@ class Model(models.Model):
path = "{}/".format(path)
return path
@classonlymethod
def get_subclass_with_rdf_type(cls, type):
'''returns Model subclass with Meta.rdf_type matching parameterised type, or None'''
if type == 'foaf:user':
return get_user_model()
for subcls in Model.__subclasses__():
if Model.get_meta(subcls, 'rdf_type') == type:
return subcls
return None
@classonlymethod
def get_permission_classes(cls, related_model, default_permissions_classes):
'''returns the permission_classes set in the models Meta class'''
......
......@@ -209,6 +209,7 @@ class CircleMember(Model):
anonymous_perms = ['view', 'add', 'delete', 'add', 'change', 'control']
authenticated_perms = ['inherit']
unique_together = ['user', 'circle']
rdf_type = 'hd:circlemember'
class Project(Model):
......
......@@ -12,11 +12,9 @@ class TestsInbox(APITestCase):
# project model has a direct many-to-many with User
def test_add_activity_project(self):
# a local user has joined a distant circle
# a local user has joined a distant project
user = get_user_model().objects.create(username='john', email='jlennon@beatles.com', password='glass onion')
UserProfile.objects.create(user=user)
# assumption that the backlinked project already exists
Project.objects.create(urlid="https://distant.com/projects/1/")
payload = {
"@context": [
......@@ -49,9 +47,6 @@ class TestsInbox(APITestCase):
projects = Project.objects.all()
user_projects = user.projects.all()
activities = Activity.objects.all()
print('projects = ' + str(projects))
print('user_projects = ' + str(user_projects))
print('activities = ' + str(activities))
self.assertEquals(len(projects), 1)
self.assertEquals(len(user_projects), 1)
self.assertEquals(len(activities), 1)
......@@ -60,11 +55,10 @@ class TestsInbox(APITestCase):
self.assertIn(response["Location"], activities.values_list('urlid', flat=True))
# circle model has a many-to-many with user, through an intermediate model
'''def test_add_activity_circle(self):
def test_add_activity_circle(self):
# a local user has joined a distant circle
user = get_user_model().objects.create(username='john', email='jlennon@beatles.com', password='glass onion')
# assumption that the backlinked circle already exists
Project.objects.create(urlid="https://distant.com/circles/1/")
UserProfile.objects.create(user=user)
payload = {
"@context": [
......@@ -75,36 +69,39 @@ class TestsInbox(APITestCase):
"type": "Add",
"actor": {
"type": "Person",
"@type": "foaf:user",
"name": user.get_full_name(),
"id": user.urlid
},
"object": {
"type": "Object",
"@type": "hd:circlemember",
"@id": "https://distant.com/circle-members/1/",
"user": {
"@type": "foaf:user",
"@id": user.urlid
},
"circle": {
"type": "Object",
"@type": "hd:circle",
"@id": "https://distant.com/circles/1/"
}
},
"target": {
"type": "Object",
"@type": "foaf:user",
"name": user.get_full_name(),
"@id": user.urlid + "circles/"
"@id": user.urlid
}
}
response = self.client.post('/users/{}/inbox/'.format(user.pk),
data=json.dumps(payload), content_type='application/ld+json;profile="https://www.w3.org/ns/activitystreams"')
self.assertEqual(response.status_code, 202)
self.assertEqual(response.status_code, 201)
# assert that the circle backlink was created
# assert that the circle backlink(s) & activity were created
circles = Circle.objects.all()
user_circles = user.circles.all()
print('circles = ' + str(circles))
print('user_circles = ' + str(user_circles))
activities = Activity.objects.all()
self.assertEquals(len(circles), 1)
self.assertEquals(len(user_circles), 1)
self.assertEquals(len(activities), 1)
self.assertIn("https://distant.com/circles/1/", circles.values_list('urlid', flat=True))
self.assertIn("https://distant.com/circle-members/1/", user_circles.values_list('urlid', flat=True))'''
self.assertIn("https://distant.com/circle-members/1/", user_circles.values_list('urlid', flat=True))
self.assertIn(response["Location"], activities.values_list('urlid', flat=True))
......@@ -72,11 +72,39 @@ class InboxModelMixin:
inbox_actions = {'post': 'receive_notification'}
parser_classes = (JSONLDParser,)
def get_or_create_backlink(self, model, urlid):
def get_or_create_backlink(self, model, urlid, **field_tuples):
try:
return model.objects.get(urlid=urlid)
except model.DoesNotExist:
return model.objects.create(urlid=urlid)
return model.objects.create(urlid=urlid, **field_tuples)
def get_or_create_nested_backlinks(self, object, object_model=None):
'''
recursively constructs a tree of nested objects, using get_or_create on each leaf/branch
:param: object. Dict representation of the object
:param: object_model. The Model class of the object. Will be discovered if set to None
'''
# store a list of the object's sub-items
if object_model is None:
object_model = Model.get_subclass_with_rdf_type(object['@type'])
if object_model is None:
raise Exception('unable to store type ' + object['@type'] + ', model with this rdf_type not found')
branches = {}
for item in object.items():
if isinstance(item[1], dict):
item_value = item[1]
item_model = Model.get_subclass_with_rdf_type(item_value['@type'])
if item_model is None:
raise Exception('unable to store type ' + item_value['@type'] + ', model with this rdf_type not found')
# push nested object tuple as a branch
backlink = self.get_or_create_nested_backlinks(item_value, item_model)
branches[item[0]] = backlink
# get or create the backlink
return_value = self.get_or_create_backlink(object_model, object['@id'], **branches)
return return_value
def handle_add_activity(self, activity, **kwargs):
'''
......@@ -84,21 +112,11 @@ class InboxModelMixin:
Indicates that the actor has added the object to the target
'''
# create the backlinked object
object_model = None
target_model = None
object_model = Model.get_subclass_with_rdf_type(activity.object['@type'])
if activity.target['@type'] == 'foaf:user':
target_model = get_user_model()
for subcls in Model.__subclasses__():
if Model.get_meta(subcls, 'rdf_type') == activity.object['@type']:
object_model = subcls
if Model.get_meta(subcls, 'rdf_type') == activity.target['@type']:
object_model = subcls
if object_model is not None and target_model is not None:
break
else:
target_model = Model.get_subclass_with_rdf_type(activity.target['@type'])
# TODO: a fallback here? Saving the backlink as Object or similar
if object_model is None:
......@@ -107,20 +125,24 @@ class InboxModelMixin:
if target_model is None:
raise Exception('unable to store type ' + activity.target['@type'] + ', model not found')
backlink = self.get_or_create_backlink(object_model, activity.object['@id'])
# store backlink(s) in database
# TODO: permissions
try:
target = target_model.objects.get(urlid=activity.target['@id'])
info = model_meta.get_field_info(target_model)
backlink = self.get_or_create_nested_backlinks(activity.object, object_model)
except KeyError:
return Response({'error': '@type or @id missing on passed object'}, status=status.HTTP_400_BAD_REQUEST)
for field_name, relation_info in info.relations.items():
# add object to target
try:
target_info = model_meta.get_field_info(target_model)
for field_name, relation_info in target_info.relations.items():
if relation_info.related_model == object_model:
target = target_model.objects.get(urlid=activity.target['@id'])
getattr(target, field_name).add(backlink)
except get_user_model().DoesNotExist:
except target_model.DoesNotExist:
return Response({}, status=status.HTTP_404_NOT_FOUND)
def receive_notification(self, request, pk=None, *args, **kwargs):
'''
receiver for inbox messages. See https://www.w3.org/TR/ldn/
......@@ -142,8 +164,6 @@ class InboxModelMixin:
obj.aid = Model.absolute_url(obj)
obj.save()
print(str(Activity.objects.all()))
response = Response({}, status=status.HTTP_201_CREATED)
response['Location'] = obj.aid
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment