diff --git a/tests/functional/test_actions.py b/tests/functional/test_actions.py index 28ed200..fce9b89 100644 --- a/tests/functional/test_actions.py +++ b/tests/functional/test_actions.py @@ -5,7 +5,7 @@ except ImportError: from tests.functional import test_base -class TestActionss(test_base.TestBase): +class TestActions(test_base.TestBase): testcase_name = "action API" # TODO: Enable this test (and write more) once the Actions API is working. diff --git a/tests/functional/test_activities.py b/tests/functional/test_activities.py new file mode 100644 index 0000000..3c3f68f --- /dev/null +++ b/tests/functional/test_activities.py @@ -0,0 +1,50 @@ +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +from tests.functional import test_base + +class TestActivities(test_base.TestBase): + testcase_name = "activity API" + + def test_list(self): + """ + Upload three photos, and check that three corresponding activities + are created. + """ + self._delete_all() + self._create_test_photos(tag=False) + photos = self.client.photos.list() + + # Check that each activity is for a valid test photo + activities = self.client.activities.list() + self.assertEqual(len(activities), len(self.photos)) + for activity in activities: + self.assertIn(activity.data.id, [photo.id for photo in photos]) + + # The purge endpoint currently reports a 500: Internal Server Error + @unittest.expectedFailure + def test_purge(self): + """ Test that the purge endpoint deletes all activities """ + activities = self.client.activities.list() + self.assertNotEqual(activities, []) + self.client.activities.purge() + self.assertEqual(activities, []) + + def test_view(self): + """ Test that the view endpoint is working correctly """ + activity = self.client.activities.list()[0] + fields = activity.get_fields().copy() + + # Check that the view method returns the same data as the list + activity.view() + self.assertEqual(fields, activity.get_fields()) + + # Check using the Trovebox class + activity = self.client.activity.view(activity) + self.assertEqual(fields, activity.get_fields()) + + # Check passing the activity ID to the Trovebox class + activity = self.client.activity.view(activity.id) + self.assertEqual(fields, activity.get_fields()) diff --git a/tests/functional/test_base.py b/tests/functional/test_base.py index 9c7056c..8a67d0b 100644 --- a/tests/functional/test_base.py +++ b/tests/functional/test_base.py @@ -124,7 +124,7 @@ class TestBase(unittest.TestCase): logging.info("Finished %s\n", self.id()) @classmethod - def _create_test_photos(cls): + def _create_test_photos(cls, tag=True): """ Upload three test photos """ album = cls.client.album.create(cls.TEST_ALBUM) photos = [ @@ -139,8 +139,9 @@ class TestBase(unittest.TestCase): albums=album.id), ] # Add the test tag, removing any autogenerated tags - for photo in photos: - photo.update(tags=cls.TEST_TAG) + if tag: + for photo in photos: + photo.update(tags=cls.TEST_TAG) @classmethod def _delete_all(cls): diff --git a/tests/functional/test_photos.py b/tests/functional/test_photos.py index f153b49..6a7577e 100644 --- a/tests/functional/test_photos.py +++ b/tests/functional/test_photos.py @@ -18,7 +18,7 @@ class TestPhotos(test_base.TestBase): # Check that they're gone self.assertEqual(self.client.photos.list(), []) - # Re-upload the photos, one of them using Bas64 encoding + # Re-upload the photos, one of them using Base64 encoding ret_val = self.client.photo.upload("tests/data/test_photo1.jpg", title=self.TEST_TITLE) self.client.photo.upload("tests/data/test_photo2.jpg", diff --git a/tests/unit/test_activities.py b/tests/unit/test_activities.py new file mode 100644 index 0000000..55a89ee --- /dev/null +++ b/tests/unit/test_activities.py @@ -0,0 +1,103 @@ +from __future__ import unicode_literals +import json +import mock +try: + import unittest2 as unittest # Python2.6 +except ImportError: + import unittest + +import trovebox + +class TestActivities(unittest.TestCase): + test_host = "test.example.com" + test_photos_dict = [{"id": "photo1"}, + {"id": "photo2"}] + test_activities_dict = [{"id": "1", + "data": test_photos_dict[0], + "type": "photo_upload"}, + {"id": "2", + "data": test_photos_dict[1], + "type": "photo_update"}] + + 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_activities = [trovebox.objects.activity.Activity(self.client, activity) + for activity in self.test_activities_dict] + + @staticmethod + def _return_value(result, message="", code=200): + return {"message": message, "code": code, "result": result} + + @staticmethod + def _view_wrapper(result): + """ The view method returns data enclosed in a dict and JSON encoded """ + result["data"] = json.dumps(result["data"]) + return {"0": result} + +class TestActivitiesList(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activities_list(self, mock_get): + """Check that the activity list is returned correctly""" + mock_get.return_value = self._return_value(self.test_activities_dict) + + result = self.client.activities.list() + mock_get.assert_called_with("/activities/list.json") + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "1") + self.assertEqual(result[0].type, "photo_upload") + self.assertEqual(result[0].data.id, "photo1") + self.assertEqual(result[1].id, "2") + self.assertEqual(result[1].type, "photo_update") + self.assertEqual(result[1].data.id, "photo2") + +class TestActivitiesPurge(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'post') + def test_activity_purge(self, mock_get): + """Test activity purging """ + mock_get.return_value = self._return_value(True) + + result = self.client.activities.purge(foo="bar") + mock_get.assert_called_with("/activities/purge.json", foo="bar") + self.assertEqual(result, True) + + @mock.patch.object(trovebox.Trovebox, 'post') + def test_activity_purge_failure(self, mock_post): + """Test activity purging """ + mock_post.return_value = self._return_value(False) + with self.assertRaises(trovebox.TroveboxError): + result = self.client.activities.purge(foo="bar") + +class TestActivityView(TestActivities): + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_view(self, mock_get): + """Check that a activity can be viewed""" + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + result = self.client.activity.view(self.test_activities[0], + foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(result.get_fields(), self.test_activities_dict[1]) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_view_id(self, mock_get): + """Check that a activity can be viewed using its ID""" + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + result = self.client.activity.view("1", foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(result.get_fields(), self.test_activities_dict[1]) + + @mock.patch.object(trovebox.Trovebox, 'get') + def test_activity_object_view(self, mock_get): + """ + Check that a activity can be viewed + when using the activity object directly + """ + mock_get.return_value = self._return_value(self._view_wrapper( + self.test_activities_dict[1])) + activity = self.test_activities[0] + activity.view(foo="bar") + mock_get.assert_called_with("/activity/1/view.json", foo="bar") + self.assertEqual(activity.get_fields(), self.test_activities_dict[1]) diff --git a/trovebox/.pylint-ignores.patch b/trovebox/.pylint-ignores.patch index 3aa911d..a4d8f5d 100644 --- a/trovebox/.pylint-ignores.patch +++ b/trovebox/.pylint-ignores.patch @@ -1,6 +1,18 @@ +diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_activity.py patched/api/api_activity.py +--- original/api/api_activity.py 2013-08-19 17:59:15.592149000 +0100 ++++ patched/api/api_activity.py 2013-08-19 18:08:39.950947589 +0100 +@@ -22,7 +22,7 @@ + raise TroveboxError("Purge response returned False") + return True + +-class ApiActivity(object): ++class ApiActivity(object): # pylint: disable=R0903 + """ Definitions of /activity/ API endpoints """ + def __init__(self, client): + self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_album.py patched/api/api_album.py ---- original/api/api_album.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/api/api_album.py 2013-08-19 16:09:30.263494209 +0100 +--- original/api/api_album.py 2013-08-19 16:09:53.539609000 +0100 ++++ patched/api/api_album.py 2013-08-19 18:08:20.118849270 +0100 @@ -3,7 +3,7 @@ """ from trovebox.objects.album import Album @@ -11,8 +23,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_al def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_tag.py patched/api/api_tag.py ---- original/api/api_tag.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/api/api_tag.py 2013-08-19 16:09:30.263494209 +0100 +--- original/api/api_tag.py 2013-08-19 16:09:53.539609000 +0100 ++++ patched/api/api_tag.py 2013-08-19 18:08:20.118849270 +0100 @@ -3,7 +3,7 @@ """ from trovebox.objects.tag import Tag @@ -23,8 +35,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/api/api_ta def __init__(self, client): self._client = client diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py patched/auth.py ---- original/auth.py 2013-08-19 16:08:00.231047000 +0100 -+++ patched/auth.py 2013-08-19 16:09:30.263494209 +0100 +--- original/auth.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/auth.py 2013-08-19 18:08:20.118849270 +0100 @@ -4,7 +4,7 @@ from __future__ import unicode_literals import os @@ -56,8 +68,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/auth.py pa parser.readfp(buf) # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py patched/http.py ---- original/http.py 2013-08-19 16:09:27.459480000 +0100 -+++ patched/http.py 2013-08-19 16:09:46.311573793 +0100 +--- original/http.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/http.py 2013-08-19 18:08:20.118849270 +0100 @@ -7,18 +7,18 @@ import requests_oauthlib import logging @@ -91,8 +103,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/http.py pa token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.py patched/__init__.py ---- original/__init__.py 2013-08-19 16:09:12.971408000 +0100 -+++ patched/__init__.py 2013-08-19 16:09:30.263494209 +0100 +--- original/__init__.py 2013-08-19 17:02:22.951226000 +0100 ++++ patched/__init__.py 2013-08-19 18:08:36.194928993 +0100 @@ -2,7 +2,7 @@ __init__.py : Trovebox package top level """ @@ -102,7 +114,16 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p from ._version import __version__ from trovebox.api import api_photo from trovebox.api import api_tag -@@ -23,7 +23,7 @@ +@@ -12,7 +12,7 @@ + + LATEST_API_VERSION = 2 + +-class Trovebox(Http): ++class Trovebox(Http): # pylint: disable=R0902 + """ + Client library for Trovebox + If no parameters are specified, config is loaded from the default +@@ -24,7 +24,7 @@ This should be used to ensure that your application will continue to work even if the Trovebox API is updated to a new revision. """ @@ -112,8 +133,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/__init__.p token='', token_secret='', api_version=None): diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py patched/main.py ---- original/main.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/main.py 2013-08-19 16:09:30.263494209 +0100 +--- original/main.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/main.py 2013-08-19 18:08:20.118849270 +0100 @@ -26,7 +26,7 @@ ################################################################# @@ -141,8 +162,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/main.py pa if options.verbose: diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tag.py patched/objects/tag.py ---- original/objects/tag.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/objects/tag.py 2013-08-19 16:09:30.263494209 +0100 +--- original/objects/tag.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/objects/tag.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,8 +1,8 @@ -""" +""" # pylint: disable=R0801 @@ -155,8 +176,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/ta from urllib import quote # Python2 diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/trovebox_object.py patched/objects/trovebox_object.py ---- original/objects/trovebox_object.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/objects/trovebox_object.py 2013-08-19 16:09:30.263494209 +0100 +--- original/objects/trovebox_object.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/objects/trovebox_object.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,10 +1,10 @@ """ Base object supporting the storage of custom fields as attributes @@ -171,8 +192,8 @@ diff --unified --recursive '--exclude=.pylint-ignores.patch' original/objects/tr self._trovebox = trovebox self._json_dict = json_dict diff --unified --recursive '--exclude=.pylint-ignores.patch' original/_version.py patched/_version.py ---- original/_version.py 2013-08-19 16:08:00.235047000 +0100 -+++ patched/_version.py 2013-08-19 16:09:30.263494209 +0100 +--- original/_version.py 2013-08-19 16:09:53.543609000 +0100 ++++ patched/_version.py 2013-08-19 18:08:20.118849270 +0100 @@ -1,2 +1,2 @@ - + # pylint: disable=C0111 diff --git a/trovebox/__init__.py b/trovebox/__init__.py index adb7ec1..9189d35 100644 --- a/trovebox/__init__.py +++ b/trovebox/__init__.py @@ -8,6 +8,7 @@ from trovebox.api import api_photo from trovebox.api import api_tag from trovebox.api import api_album from trovebox.api import api_action +from trovebox.api import api_activity LATEST_API_VERSION = 2 @@ -38,3 +39,5 @@ class Trovebox(Http): self.albums = api_album.ApiAlbums(self) self.album = api_album.ApiAlbum(self) self.action = api_action.ApiAction(self) + self.activities = api_activity.ApiActivities(self) + self.activity = api_activity.ApiActivity(self) diff --git a/trovebox/api/api_activity.py b/trovebox/api/api_activity.py new file mode 100644 index 0000000..d3f4c42 --- /dev/null +++ b/trovebox/api/api_activity.py @@ -0,0 +1,36 @@ +""" +api_activity.py : Trovebox Activity API Classes +""" +from trovebox.errors import TroveboxError +from trovebox.objects.activity import Activity + +class ApiActivities(object): + """ Definitions of /activities/ API endpoints """ + def __init__(self, client): + self._client = client + + def list(self, **kwds): + """ Returns a list of Activity objects """ + activities = self._client.get("/activities/list.json", **kwds)["result"] + return [Activity(self._client, activity) for activity in activities] + + def purge(self, **kwds): + """ Purge all activities """ + if not self._client.post("/activities/purge.json", **kwds)["result"]: + raise TroveboxError("Purge response returned False") + return True + +class ApiActivity(object): + """ Definitions of /activity/ API endpoints """ + def __init__(self, client): + self._client = client + + def view(self, activity, **kwds): + """ + View an activity's contents. + Returns the requested activity object. + """ + if not isinstance(activity, Activity): + activity = Activity(self._client, {"id": activity}) + activity.view(**kwds) + return activity diff --git a/trovebox/objects/activity.py b/trovebox/objects/activity.py new file mode 100644 index 0000000..fe57013 --- /dev/null +++ b/trovebox/objects/activity.py @@ -0,0 +1,40 @@ +""" +Representation of an Activity object +""" +import json + +from .trovebox_object import TroveboxObject +from .photo import Photo + +class Activity(TroveboxObject): + """ Representation of an Activity object """ + def __init__(self, trovebox, json_dict): + self.data = None + self.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 data with photo objects + if self.type is not None: + if self.type.startswith("photo"): + self.data = Photo(self._trovebox, self.data) + else: + raise NotImplementedError("Unrecognised activity type: %s" + % self.type) + + def view(self, **kwds): + """ + Requests the full contents of the activity. + Updates the activity's fields with the response. + """ + result = self._trovebox.get("/activity/%s/view.json" % + self.id, **kwds)["result"] + + # TBD: Why is the result enclosed/encoded like this? + result = result["0"] + result["data"] = json.loads(result["data"]) + + self._replace_fields(result) + self._update_fields_with_objects()