diff --git a/djangoldp/activities/verbs.py b/djangoldp/activities/verbs.py index 65bfe09f0db269f77f1bf6008fa3f4f68d182e11..313b738f799021162e7f1208e78b4f886af6c404 100644 --- a/djangoldp/activities/verbs.py +++ b/djangoldp/activities/verbs.py @@ -1,5 +1,6 @@ from copy import copy +from django.conf import settings from djangoldp.activities import errors from djangoldp.activities.objects import ALLOWED_TYPES, Object, Actor @@ -7,6 +8,11 @@ from djangoldp.activities.objects import ALLOWED_TYPES, Object, Actor class Activity(Object): attributes = Object.attributes + ["actor", "object"] type = "Activity" + # dictionary defining required attributes -> tuple of acceptable types + required_attributes = { + "actor": (Actor, str), + "object": dict + } def get_audience(self): audience = [] @@ -29,30 +35,16 @@ class Activity(Object): return new def validate(self): - pass + for attr in self.required_attributes.keys(): + if not isinstance(getattr(self, attr, None), self.required_attributes[attr]): + raise errors.ActivityStreamValidationError("required attribute " + attr + " of type " + + str(self.required_attributes[attr])) class Add(Activity): type = "Add" attributes = Activity.attributes + ["target"] - - def validate(self): - msg = None - if not getattr(self, "actor", None): - msg = "Invalid activity, actor is missing" - elif not getattr(self, "object", None): - msg = "Invalid activity, object is missing" - elif not getattr(self, "target", None): - msg = "Invalid activity, target is missing" - elif not isinstance(self.actor, Actor) and not isinstance(self.actor, str): - msg = "Invalid actor type, must be an Actor or a string" - elif not isinstance(self.object, dict): - msg = "Invalid object type, must be a dict" - elif not isinstance(self.target, dict): - msg = "Invalid target type, must be a dict" - - if msg: - raise errors.ActivityStreamValidationError(msg) + required_attributes = {**Activity.required_attributes, "target": dict} class Remove(Activity): @@ -60,49 +52,24 @@ class Remove(Activity): attributes = Activity.attributes + ["target", "origin"] def validate(self): - msg = None - if not getattr(self, "actor", None): - msg = "Invalid activity, actor is missing" - elif not getattr(self, "object", None): - msg = "Invalid activity, object is missing" - elif not getattr(self, "target", None) and not getattr(self, "origin", None): - msg = "Invalid activity, no target or origin given" - elif not isinstance(self.actor, Actor) and not isinstance(self.actor, str): - msg = "Invalid actor type, must be an Actor or a string" - elif not isinstance(self.object, dict): - msg = "Invalid object type, must be a dict" + super().validate() + + if not getattr(self, "target", None) and not getattr(self, "origin", None): + raise errors.ActivityStreamValidationError("Invalid activity, no target or origin given") if getattr(self, "target", None) is not None: if not isinstance(self.target, dict): - msg = "Invalid target type, must be a dict" + raise errors.ActivityStreamValidationError("Invalid target type, must be a dict") if getattr(self, "origin", None) is not None: if not isinstance(self.origin, dict): - msg = "Invalid origin type, must be a dict" - - if msg: - raise errors.ActivityStreamValidationError(msg) + raise errors.ActivityStreamValidationError("Invalid origin type, must be a dict") class Create(Activity): type = "Create" - def validate(self): - msg = None - - if not getattr(self, "actor", None): - msg = "Invalid activity, actor is missing" - elif not getattr(self, "object", None): - msg = "Invalid activity, object is missing" - elif not isinstance(self.actor, Actor) and not isinstance(self.actor, str): - msg = "Invalid actor type, must be an Actor or a string" - elif not isinstance(self.object, dict): - msg = "Invalid object type, must be a dict" - if msg: - raise errors.ActivityStreamValidationError(msg) - - -class Update(Create): +class Update(Activity): type = "Update" @@ -110,40 +77,15 @@ class Delete(Activity): type = "Delete" attributes = Activity.attributes + ["origin"] - def validate(self): - msg = None - if not getattr(self, "actor", None): - msg = "Invalid activity, actor is missing" - elif not getattr(self, "object", None): - msg = "Invalid activity, object is missing" - elif not isinstance(self.actor, Actor) and not isinstance(self.actor, str): - msg = "Invalid actor type, must be an Actor or a string" - elif not isinstance(self.object, dict): - msg = "Invalid object type, must be a dict" - - if msg: - raise errors.ActivityStreamValidationError(msg) - class Follow(Activity): type = "Follow" def validate(self): - msg = None - - if not getattr(self, "actor", None): - msg = "Invalid activity, actor is missing" - elif not getattr(self, "object", None): - msg = "Invalid activity, object is missing" - elif not isinstance(self.actor, Actor) and not isinstance(self.actor, str): - msg = "Invalid actor type, must be an Actor or a string" - elif isinstance(self.actor, Actor) and (self.actor.inbox is None and self.actor.id is None): - msg = "Must pass inbox or id with the actor to follow" - elif not isinstance(self.object, dict): - msg = "Invalid object type, must be a dict" - - if msg: - raise errors.ActivityStreamValidationError(msg) + super().validate() + + if isinstance(self.actor, Actor) and (self.actor.inbox is None and self.actor.id is None): + raise errors.ActivityStreamValidationError("Must pass inbox or id with the actor to follow") ALLOWED_TYPES.update({ diff --git a/djangoldp/tests/tests_inbox.py b/djangoldp/tests/tests_inbox.py index d29a18e39d218a537e463eee2ff8729b71764512..21df3c5eaac3e0c38a61bc6602767c02ecceac97 100644 --- a/djangoldp/tests/tests_inbox.py +++ b/djangoldp/tests/tests_inbox.py @@ -205,7 +205,33 @@ class TestsInbox(APITestCase): 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)) - # TODO: adding to a model which has multiple relationships with this RDF type + # TODO: https://git.startinblox.com/djangoldp-packages/djangoldp/issues/250 + def test_add_activity_str_parameter(self): + user = get_user_model().objects.create(username='john', email='jlennon@beatles.com', password='glass onion') + UserProfile.objects.create(user=user) + + payload = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + {"hd": "http://happy-dev.fr/owl/#"} + ], + "summary": "Test was added to Test Circle", + "type": "Add", + "actor": { + "type": "Service", + "name": "Backlinks Service" + }, + "object": "https://distant.com/somethingunknown/1/", + "target": { + "@type": "foaf:user", + "@id": user.urlid + } + } + + response = self.client.post('/inbox/', + data=json.dumps(payload), + content_type='application/ld+json;profile="https://www.w3.org/ns/activitystreams"') + self.assertEqual(response.status_code, 400) # error behaviour - unknown model def test_add_activity_unknown(self): @@ -239,6 +265,32 @@ class TestsInbox(APITestCase): data=json.dumps(payload), content_type='application/ld+json;profile="https://www.w3.org/ns/activitystreams"') self.assertEqual(response.status_code, 404) + def test_invalid_activity_missing_actor(self): + user = get_user_model().objects.create(username='john', email='jlennon@beatles.com', password='glass onion') + UserProfile.objects.create(user=user) + + payload = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + {"hd": "http://happy-dev.fr/owl/#"} + ], + "summary": "Test was added to Test Circle", + "type": "Add", + "object": { + "@type": "hd:somethingunknown", + "@id": "https://distant.com/somethingunknown/1/" + }, + "target": { + "@type": "foaf:user", + "@id": user.urlid + } + } + + response = self.client.post('/inbox/', + data=json.dumps(payload), + content_type='application/ld+json;profile="https://www.w3.org/ns/activitystreams"') + self.assertEqual(response.status_code, 400) + # # REMOVE & DELETE ACTIVITIES # @@ -288,7 +340,49 @@ class TestsInbox(APITestCase): self.assertIn(response["Location"], activities.values_list('urlid', flat=True)) # TODO: test_remove_activity_project_using_target - # TODO: error behaviour - project does not exist on user + + # error behaviour - project does not exist on user + def test_remove_activity_nonexistent_project(self): + user = get_user_model().objects.create(username='john', email='jlennon@beatles.com', password='glass onion') + UserProfile.objects.create(user=user) + Project.objects.create(urlid="https://distant.com/projects/1/") + + payload = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + {"hd": "http://happy-dev.fr/owl/#"} + ], + "summary": user.get_full_name() + " removed Test Project", + "type": "Remove", + "actor": { + "type": "Service", + "name": "Backlinks Service" + }, + "object": { + "@type": "hd:project", + "@id": "https://distant.com/projects/1/" + }, + "origin": { + "@type": "foaf:user", + "name": user.get_full_name(), + "@id": user.urlid + } + } + + response = self.client.post('/inbox/', + data=json.dumps(payload), + content_type='application/ld+json;profile="https://www.w3.org/ns/activitystreams"') + self.assertEqual(response.status_code, 201) + + # assert that the circle backlink(s) were removed & activity were created + projects = Project.objects.all() + user_projects = user.projects.all() + activities = Activity.objects.all() + self.assertEquals(len(projects), 1) + self.assertEquals(len(user_projects), 0) + self.assertEquals(len(activities), 1) + self.assertIn("https://distant.com/projects/1/", projects.values_list('urlid', flat=True)) + self.assertIn(response["Location"], activities.values_list('urlid', flat=True)) # Delete CircleMember def test_delete_activity_circle_using_origin(self): diff --git a/djangoldp/views.py b/djangoldp/views.py index 1dd61ecfba0d2d020a316293e5ffbe866fccdcc6..70a71ee5dd779520b45e6e1cb4146b58ecedf680 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -26,7 +26,7 @@ from djangoldp.models import LDPSource, Model, Activity, Follower from djangoldp.permissions import LDPPermissions from djangoldp.filters import LocalObjectOnContainerPathBackend from djangoldp.activities import ActivityPubService, as_activitystream -from djangoldp.activities.errors import ActivityStreamDecodeError +from djangoldp.activities.errors import ActivityStreamDecodeError, ActivityStreamValidationError get_user_model()._meta.rdf_context = {"get_full_name": "rdfs:label"} @@ -82,6 +82,8 @@ class InboxView(APIView): activity.validate() except ActivityStreamDecodeError: return Response('Activity type unsupported', status=status.HTTP_405_METHOD_NOT_ALLOWED) + except ActivityStreamValidationError as e: + return Response(str(e), status=status.HTTP_400_BAD_REQUEST) if activity.type == 'Add': self.handle_add_activity(activity, **kwargs) @@ -107,7 +109,7 @@ class InboxView(APIView): def get_or_create_nested_backlinks(self, object, object_model=None, update=False): ''' - recursively constructs a tree of nested objects, using get_or_create on each leaf/branch + recursively deconstructs 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 :param update: if True will update retrieved objects with new data @@ -149,19 +151,20 @@ class InboxView(APIView): object_model = self._get_subclass_with_rdf_type_or_404(activity.object['@type']) target_model = self._get_subclass_with_rdf_type_or_404(activity.target['@type']) + try: + target = target_model.objects.get(urlid=activity.target['@id']) + except target_model.DoesNotExist: + return Response({}, status=status.HTTP_404_NOT_FOUND) + # store backlink(s) in database backlink = self.get_or_create_nested_backlinks(activity.object, object_model) # add object to target - try: - target_info = model_meta.get_field_info(target_model) - target = target_model.objects.get(urlid=activity.target['@id']) + 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: - getattr(target, field_name).add(backlink) - except target_model.DoesNotExist: - return Response({}, status=status.HTTP_404_NOT_FOUND) + for field_name, relation_info in target_info.relations.items(): + if relation_info.related_model == object_model: + getattr(target, field_name).add(backlink) def handle_remove_activity(self, activity, **kwargs): ''' @@ -174,21 +177,19 @@ class InboxView(APIView): # get the model reference to saved object try: + origin = origin_model.objects.get(urlid=activity.origin['@id']) object_instance = object_model.objects.get(urlid=activity.object['@id']) + except origin_model.DoesNotExist: + raise Http404() except object_model.DoesNotExist: return # remove object from origin - try: - origin_info = model_meta.get_field_info(origin_model) - origin = origin_model.objects.get(urlid=activity.origin['@id']) + origin_info = model_meta.get_field_info(origin_model) - for field_name, relation_info in origin_info.relations.items(): - if relation_info.related_model == object_model: - getattr(origin, field_name).remove(object_instance) - # TODO: decipher from history if the resource has been moved? - except origin_model.DoesNotExist: - raise Http404() + for field_name, relation_info in origin_info.relations.items(): + if relation_info.related_model == object_model: + getattr(origin, field_name).remove(object_instance) def handle_create_or_update_activity(self, activity, **kwargs): '''