diff --git a/djangoldp/activities/services.py b/djangoldp/activities/services.py index 15cbe2e44d21236349aa7ce3a517f9ac1dc246fe..db02400cb41aa7280ca77ff9cdfb7eb0835158fa 100644 --- a/djangoldp/activities/services.py +++ b/djangoldp/activities/services.py @@ -239,6 +239,27 @@ class ActivityPubService(object): inboxes = set(Follower.objects.filter(object=object_urlid).values_list('inbox', flat=True)) return inboxes + @classmethod + def save_follower_for_target(cls, external_urlid, obj_id): + inbox = ActivityPubService.discover_inbox(external_urlid) + + if not Follower.objects.filter(object=obj_id, follower=external_urlid).exists(): + Follower.objects.create(object=obj_id, inbox=inbox, follower=external_urlid, + is_backlink=True) + + @classmethod + def save_followers_for_targets(cls, external_urlids, obj_id): + ''' + saves Follower objects for any external urlid which isn't already following the object in question + :param external_urlids: list of external urlids to populate the follower inbox + :param obj_id: object id to be followed + ''' + existing_followers = Follower.objects.filter(object=obj_id).values_list('follower', flat=True) + for urlid in external_urlids: + if urlid not in existing_followers: + Follower.objects.create(object=obj_id, inbox=ActivityPubService.discover_inbox(urlid), + follower=urlid, is_backlink=True) + @receiver([post_save]) def check_save_for_backlinks(sender, instance, created, **kwargs): @@ -262,11 +283,7 @@ def check_save_for_backlinks(sender, instance, created, **kwargs): ActivityPubService.send_update_activity(actor, obj, target) # create Followers to update external resources of changes in future - existing_followers = Follower.objects.filter(object=obj['@id']).values_list('follower', flat=True) - for urlid in external_urlids: - if urlid not in existing_followers: - Follower.objects.create(object=obj['@id'], inbox=ActivityPubService.discover_inbox(urlid), - follower=urlid, is_backlink=True) + ActivityPubService.save_followers_for_targets(external_urlids, obj['@id']) @receiver([post_delete]) @@ -340,10 +357,7 @@ def check_m2m_for_backlinks(sender, instance, action, *args, **kwargs): if action == 'post_add': for target in targets: ActivityPubService.send_add_activity(BACKLINKS_ACTOR, obj, target) - inbox = ActivityPubService.discover_inbox(target['@id']) - if not Follower.objects.filter(object=obj['@id'], follower=target['@id']).exists(): - Follower.objects.create(object=obj['@id'], inbox=inbox, follower=target['@id'], - is_backlink=True) + ActivityPubService.save_follower_for_target(target['@id'], obj['@id']) elif action == "post_remove" or action == "pre_clear": for target in targets: diff --git a/djangoldp/tests/tests_inbox.py b/djangoldp/tests/tests_inbox.py index 5e937d651874f7e8fed6ff3340d8d66c2ca39b9c..13f243390ff35a54efdb757613ee9579fdc2e632 100644 --- a/djangoldp/tests/tests_inbox.py +++ b/djangoldp/tests/tests_inbox.py @@ -52,9 +52,14 @@ class TestsInbox(APITestCase): self.assertEquals(len(activities), activity_len) self.assertIn(response["Location"], activities.values_list('urlid', flat=True)) + def _assert_follower_created(self, local_urlid, external_urlid): + existing_followers = Follower.objects.filter(object=local_urlid).values_list('follower', flat=True) + self.assertTrue(external_urlid in existing_followers) + # # CREATE ACTIVITY # + @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True) def test_create_activity_circle(self): obj = { "@type": "hd:circle", @@ -77,6 +82,10 @@ class TestsInbox(APITestCase): self.assertEqual(circles[0].owner, self.user) self._assert_activity_created(response) + # assert external circle member now following local user + self.assertEquals(Follower.objects.count(), 1) + self._assert_follower_created(self.user.urlid, "https://distant.com/circles/1/") + # sender has sent a circle with a local user that doesn't exist def test_create_activity_circle_local(self): urlid = '{}{}'.format(settings.SITE_URL, 'someonewhodoesntexist') @@ -104,6 +113,7 @@ class TestsInbox(APITestCase): # ADD ACTIVITIES # # project model has a direct many-to-many with User + @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True) def test_add_activity_project(self): obj = { "@type": "hd:project", @@ -124,18 +134,26 @@ class TestsInbox(APITestCase): self.assertIn("https://distant.com/projects/1/", user_projects.values_list('urlid', flat=True)) self._assert_activity_created(response) + # assert external circle member now following local user + self.assertEquals(Follower.objects.count(), 1) + self._assert_follower_created(self.user.urlid, "https://distant.com/projects/1/") + # circle model has a many-to-many with user, through an intermediate model + @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True) def test_add_activity_circle(self): + ext_circlemember_urlid = "https://distant.com/circle-members/1/" + ext_circle_urlid = "https://distant.com/circles/1/" + obj = { "@type": "hd:circlemember", - "@id": "https://distant.com/circle-members/1/", + "@id": ext_circlemember_urlid, "user": { "@type": "foaf:user", "@id": self.user.urlid }, "circle": { "@type": "hd:circle", - "@id": "https://distant.com/circles/1/" + "@id": ext_circle_urlid } } payload = self._get_activity_request_template("Add", obj, self._build_target_from_user(self.user)) @@ -149,15 +167,19 @@ class TestsInbox(APITestCase): user_circles = self.user.circles.all() self.assertEquals(len(circles), 1) self.assertEquals(len(user_circles), 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(ext_circle_urlid, circles.values_list('urlid', flat=True)) + self.assertIn(ext_circlemember_urlid, user_circles.values_list('urlid', flat=True)) self._assert_activity_created(response) + # assert external circle member now following local user + self.assertEquals(Follower.objects.count(), 1) + self._assert_follower_created(self.user.urlid, ext_circlemember_urlid) + # test sending an add activity when the backlink already exists @override_settings(SEND_BACKLINKS=True, DISABLE_OUTBOX=True) def test_add_activity_object_already_added(self): circle = Circle.objects.create(urlid="https://distant.com/circles/1/") - CircleMember.objects.create(urlid="https://distant.com/circle-members/1/", circle=circle, user=self.user) + cm = CircleMember.objects.create(urlid="https://distant.com/circle-members/1/", circle=circle, user=self.user) obj = { "@type": "hd:circlemember", @@ -189,6 +211,10 @@ class TestsInbox(APITestCase): self._assert_activity_created(response) self.assertEqual(Activity.objects.count(), prior_count + 1) + # assert that followers exist for the external urlids + self.assertEquals(Follower.objects.count(), 1) + self._assert_follower_created(self.user.urlid, cm.urlid) + # TODO: https://git.startinblox.com/djangoldp-packages/djangoldp/issues/250 def test_add_activity_str_parameter(self): payload = self._get_activity_request_template("Add", "https://distant.com/somethingunknown/1/", diff --git a/djangoldp/views.py b/djangoldp/views.py index 2837279d38055ee3a7e3081d908e568ce32f4e74..a110bfff07c0bf804b4c1ce8ab60314f9f2595d5 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -159,7 +159,24 @@ class InboxView(APIView): # get or create the backlink try: - return Model.get_or_create_external(object_model, obj['@id'], update=update, **branches) + external = Model.get_or_create_external(object_model, obj['@id'], update=update, **branches) + + # creating followers, to inform distant resource of changes to local connection + if Model.is_external(external): + # this is handled with Followers, where each local child of the branch is followed by its external parent + for item in obj.items(): + urlid = item[1] + if isinstance(item[1], dict): + urlid = urlid['@id'] + if not isinstance(urlid, str): + continue + + if not Model.is_external(urlid): + ActivityPubService.save_follower_for_target(external.urlid, urlid) + + return external + + # this will be raised when the object was local, but it didn't exist except ObjectDoesNotExist: raise Http404() @@ -194,6 +211,7 @@ class InboxView(APIView): attr = getattr(target, field_name) if not attr.filter(urlid=backlink.urlid).exists(): attr.add(backlink) + ActivityPubService.save_follower_for_target(backlink.urlid, target.urlid) def handle_remove_activity(self, activity, **kwargs): '''