diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py new file mode 100644 index 0000000..28ed200 --- /dev/null +++ b/tests/functional/test_actions.py @@ -0,0 +1,17 @@ +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +from tests.functional import test_base + +class TestActionss(test_base.TestBase): + testcase_name = "action API" + + # TODO: Enable this test (and write more) once the Actions API is working. + # Currently always returns: + # "Could not find route /action/create.json from /action/create.json" + @unittest.expectedFailure + def test_create_delete(self): + """ Create an action on a photo, then delete it """ + action = self.client.action.create(target=self.photos[0]) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py new file mode 100644 index 0000000..6dd7ece --- /dev/null +++ b/tests/unit/test_actions.py @@ -0,0 +1,142 @@ +from __future__ import unicode_literals +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import trovebox + +class TestActions(unittest.TestCase): + test_host = "test.example.com" + test_photos_dict = [{"id": "photo1"}, + {"id": "photo2"}] + test_actions_dict = [{"id": "1", + "target": test_photos_dict[0], + "target_type": "photo", + "totalRows": 2}, + {"id": "2", + "target": test_photos_dict[1], + "target_type": "photo", + "totalRows": 2}] + + def setUp(self): + self.client = trovebox.Trovebox(host=self.test_host) + self.test_photos = [trovebox.objects.photo.Photo(self.client, photo) + for photo in self.test_photos_dict] + self.test_actions = [trovebox.objects.action.Action(self.client, action) + for action in self.test_actions_dict] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + +class TestActionCreate(TestActions): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create(self, mock_post): + """Check that an action can be created on a photo object""" + mock_post.return_value = self._return_value(self.test_actions_dict[0]) + result = self.client.action.create(target=self.test_photos[0], foo="bar") + mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, + target_type="photo", + foo="bar") + self.assertEqual(result.id, "1") + self.assertEqual(result.target.id, "photo1") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create_id(self, mock_post): + """Check that an action can be created using a photo id""" + mock_post.return_value = self._return_value(self.test_actions_dict[0]) + result = self.client.action.create(target=self.test_photos[0].id, + target_type="photo", foo="bar") + mock_post.assert_called_with("/action/create.json", target=self.test_photos[0].id, + target_type="photo", + foo="bar") + self.assertEqual(result.id, "1") + self.assertEqual(result.target.id, "photo1") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_create_invalid_type(self, mock_post): + """Check that an exception is raised if an action is created on a non photo object""" + with self.assertRaises(NotImplementedError): + self.client.action.create(target=object(), foo="bar") + +class TestActionDelete(TestActions): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete(self, mock_post): + """Check that an action can be deleted""" + mock_post.return_value = self._return_value(True) + result = self.client.action.delete(self.test_actions[0]) + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete_id(self, mock_post): + """Check that an action can be deleted using its ID""" + mock_post.return_value = self._return_value(True) + result = self.client.action.delete("1") + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_delete_failure(self, mock_post): + """Check that an exception is raised if an action cannot be deleted""" + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + self.client.action.delete(self.test_actions[0]) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_object_delete(self, mock_post): + """Check that an action can be deleted using the action object directly""" + mock_post.return_value = self._return_value(True) + action = self.test_actions[0] + result = action.delete() + mock_post.assert_called_with("/action/1/delete.json") + self.assertEqual(result, True) + self.assertEqual(action.get_fields(), {}) + self.assertEqual(action.id, None) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_action_object_delete_failure(self, mock_post): + """ + Check that an exception is raised if an action cannot be deleted + when using the action object directly + """ + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + self.test_actions[0].delete() + +class TestActionView(TestActions): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_view(self, mock_get): + """Check that an action can be viewed""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + result = self.client.action.view(self.test_actions[0], name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.target.id, "photo2") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_view_id(self, mock_get): + """Check that an action can be viewed using its ID""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + result = self.client.action.view("1", name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(result.id, "2") + self.assertEqual(result.target.id, "photo2") + self.assertEqual(result.target_type, "photo") + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_action_object_view(self, mock_get): + """Check that an action can be viewed using the action object directly""" + mock_get.return_value = self._return_value(self.test_actions_dict[1]) + action = self.test_actions[0] + action.view(name="Test") + mock_get.assert_called_with("/action/1/view.json", name="Test") + self.assertEqual(action.id, "2") + self.assertEqual(action.target.id, "photo2") + self.assertEqual(action.target_type, "photo") + diff --git a/trovebox/api/api_action.py b/trovebox/api/api_action.py new file mode 100644 index 0000000..1712a8a --- /dev/null +++ b/trovebox/api/api_action.py @@ -0,0 +1,56 @@ +""" +api_action.py : Trovebox Action API Classes +""" +from trovebox.objects.action import Action +from trovebox.objects.photo import Photo + +class ApiAction(object): + """ Definitions of /action/ API endpoints """ + def __init__(self, client): + self._client = client + + def create(self, target, target_type=None, **kwds): + """ + Create a new action and return it. + If the target_type parameter isn't specified, it is automatically + generated. + """ + if target_type is None: + # Determine the target type + if isinstance(target, Photo): + target_type = "photo" + else: + raise NotImplementedError("Actions can only be assigned to " + "Photos when target_type isn't " + "specified") + # Extract the ID from the target + try: + target_id = target.id + except AttributeError: + # Assume the ID was passed in directly + target_id = target + + result = self._client.post("/action/create.json", + target=target_id, target_type=target_type, + **kwds)["result"] + return Action(self._client, result) + + def delete(self, action, **kwds): + """ + Delete an action. + Returns True if successful. + Raises a TroveboxError if not. + """ + if not isinstance(action, Action): + action = Action(self._client, {"id": action}) + return action.delete(**kwds) + + def view(self, action, **kwds): + """ + View an action's contents. + Returns the requested action object. + """ + if not isinstance(action, Action): + action = Action(self._client, {"id": action}) + action.view(**kwds) + return action diff --git a/trovebox/objects/action.py b/trovebox/objects/action.py new file mode 100644 index 0000000..c15af37 --- /dev/null +++ b/trovebox/objects/action.py @@ -0,0 +1,47 @@ +""" +Representation of an Action object +""" +from trovebox.errors import TroveboxError +from .trovebox_object import TroveboxObject +from .photo import Photo + +class Action(TroveboxObject): + """ Representation of an Action object """ + def __init__(self, trovebox, json_dict): + self.target = None + self.target_type = None + TroveboxObject.__init__(self, trovebox, json_dict) + self._update_fields_with_objects() + + def _update_fields_with_objects(self): + """ Convert dict fields into objects, where appropriate """ + # Update the photo target with photo objects + if self.target is not None: + if self.target_type == "photo": + self.target = Photo(self._trovebox, self.target) + else: + raise NotImplementedError("Actions can only be assigned to " + "Photos") + + def delete(self, **kwds): + """ + Delete this action. + Returns True if successful. + Raises a TroveboxError if not. + """ + result = self._trovebox.post("/action/%s/delete.json" % + self.id, **kwds)["result"] + if not result: + raise TroveboxError("Delete response returned False") + self._delete_fields() + return result + + def view(self, **kwds): + """ + Requests the full contents of the action. + Updates the action's fields with the response. + """ + result = self._trovebox.get("/action/%s/view.json" % + self.id, **kwds)["result"] + self._replace_fields(result) + self._update_fields_with_objects()