{"action":"created","comment":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/41282885","id":41282885,"diff_hunk":"@@ -7,313 +7,366 @@\n from dateutil import relativedelta\n \n from framework.auth import Auth\n+from framework.exceptions import PermissionsError\n from tests.base import OsfTestCase\n-from tests.factories import UserFactory, ProjectFactory\n+from tests.factories import UserFactory, ProjectFactory, ExternalAccountFactory\n from website.addons.base import exceptions\n \n+from website.addons.googledrive import model\n from website.addons.googledrive.client import GoogleAuthClient\n-from website.addons.googledrive.model import (\n-    GoogleDriveUserSettings,\n-    GoogleDriveNodeSettings,\n-    GoogleDriveOAuthSettings,\n-)\n from website.addons.googledrive.tests.factories import (\n     GoogleDriveNodeSettingsFactory,\n     GoogleDriveUserSettingsFactory,\n-    GoogleDriveOAuthSettingsFactory,\n )\n \n+class TestGoogleDriveProvider(OsfTestCase):\n+    def setUp(self):\n+        super(TestGoogleDriveProvider, self).setUp()\n+        self.provider = model.GoogleDriveProvider()\n \n-class TestGoogleDriveUserSettingsModel(OsfTestCase):\n+    @mock.patch.object(GoogleAuthClient, 'userinfo')\n+    def test_handle_callback(self, mock_client):\n+        fake_response = {'access_token': 'abc123'}\n+        fake_info = {'sub': '12345', 'name': 'fakename', 'profile': 'fakeUrl'}\n+        mock_client.return_value = fake_info\n+        res = self.provider.handle_callback(fake_response)\n+        assert_equal(res['provider_id'], '12345')\n+        assert_equal(res['display_name'], 'fakename')\n+        assert_equal(res['profile_url'], 'fakeUrl')\n \n-    def setUp(self):\n-        super(TestGoogleDriveUserSettingsModel, self).setUp()\n-        self.user = UserFactory()\n \n-    def test_fields(self):\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        retrieved = GoogleDriveUserSettings.load(user_settings._primary_key)\n+    @mock.patch.object(GoogleAuthClient, 'refresh')\n+    def test_refresh_token(self, mock_refresh):\n+        fake_access_token = 'abc123'\n+        fake_refresh_token = 'xyz456'\n+        mock_refresh.return_value = 'faketoken'\n \n-        assert_true(retrieved.owner)\n-        assert_true(retrieved.username)\n-        assert_true(retrieved.expires_at)\n-        assert_true(retrieved.access_token)\n+        res = self.provider._refresh_token(fake_access_token, fake_refresh_token)\n+        assert_equal(res, 'faketoken')\n \n-    def test_has_auth(self):\n-        user_settings = GoogleDriveUserSettingsFactory(access_token=None)\n-        assert_false(user_settings.has_auth)\n-        user_settings.access_token = '12345'\n-        user_settings.save()\n-        assert_true(user_settings.has_auth)\n \n-    @mock.patch.object(GoogleDriveOAuthSettings, '_needs_refresh')\n-    def test_access_token_checks(self, mock_needs_refresh):\n-        mock_needs_refresh.return_value = False\n-        user_settings = GoogleDriveUserSettingsFactory()\n+    @mock.patch.object(GoogleAuthClient, 'refresh')\n+    def test_refresh_token_without_refresh_token(self, mock_refresh):\n+        fake_access_token = 'abc123'\n+        fake_refresh_token = None\n+        mock_refresh.return_value = 'faketoken'\n \n-        user_settings.access_token\n+        res = self.provider._refresh_token(fake_access_token, fake_refresh_token)\n+        assert_false(res)\n \n-        mock_needs_refresh.assert_called_once()\n+class TestGoogleDriveUserSettings(OsfTestCase):\n+    def setUp(self):\n+        super(TestGoogleDriveUserSettings, self).setUp()\n+        self.node = ProjectFactory()\n+        self.user = self.node.creator\n \n-    @mock.patch.object(GoogleAuthClient, 'refresh')\n-    @mock.patch.object(GoogleDriveOAuthSettings, '_needs_refresh')\n-    def test_access_token_refreshes(self, mock_needs_refresh, mock_refresh):\n-        mock_refresh.return_value = {\n-            'access_token': 'abc',\n-            'refresh_token': '123',\n-            'expires_at': time.time(),\n-        }\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        user_settings.expires_at = datetime.now()\n-        user_settings.access_token\n-        mock_refresh.assert_called_once()\n+        self.external_account = ExternalAccountFactory()\n \n-    @mock.patch.object(GoogleAuthClient, 'refresh')\n-    def test_access_token_refreshes_timeout(self, mock_refresh):\n-        mock_refresh.return_value = {\n-            'access_token': 'abc',\n-            'refresh_token': '123',\n-            'expires_at': time.time(),\n-        }\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        user_settings.expires_at = (datetime.utcnow() + relativedelta.relativedelta(seconds=5))\n+        self.user.external_accounts.append(self.external_account)\n+        self.user.save()\n \n-        user_settings.access_token\n+        self.user_settings = self.user.get_or_add_addon('googledrive')\n \n-        mock_refresh.assert_called_once()\n+    def tearDown(self):\n+        super(TestGoogleDriveUserSettings, self).tearDown()\n+        self.user_settings.remove()\n+        self.external_account.remove()\n+        self.node.remove()\n+        self.user.remove()\n \n-    @mock.patch.object(GoogleAuthClient, 'refresh')\n-    def test_access_token_refreshes_timeout_longer(self, mock_refresh):\n-        mock_refresh.return_value = {\n-            'access_token': 'abc',\n-            'refresh_token': '123',\n-            'expires_at': time.time(),\n-        }\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        user_settings.expires_at = datetime.utcnow() + relativedelta.relativedelta(minutes=4)\n-        user_settings.access_token\n-        mock_refresh.assert_called_once()\n+    def test_grant_oauth_access_no_metadata(self):\n+        self.user_settings.grant_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+        )\n+        self.user_settings.save()\n \n-    @mock.patch.object(GoogleAuthClient, 'refresh')\n-    def test_access_token_doesnt_refresh(self, mock_refresh):\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        user_settings.save()\n-        user_settings.access_token\n-        assert_false(mock_refresh.called)\n-\n-    def test_clear_clears_associated_node_settings(self):\n-        node_settings = GoogleDriveNodeSettingsFactory.build()\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        node_settings.user_settings = user_settings\n-        node_settings.save()\n-        user_settings.clear()\n-        user_settings.save()\n-\n-        # Node settings no longer associated with user settings\n-        assert_is(node_settings.folder_id, None)\n-        assert_is(node_settings.user_settings, None)\n-\n-    def test_clear(self):\n-        node_settings = GoogleDriveNodeSettingsFactory.build()\n-        user_settings = GoogleDriveUserSettingsFactory(access_token='abcde')\n-        node_settings.user_settings = user_settings\n-        node_settings.save()\n-\n-        assert_true(user_settings.access_token)\n-        user_settings.clear()\n-        user_settings.save()\n-        assert_false(user_settings.access_token)\n-\n-    @mock.patch.object(GoogleDriveOAuthSettings, 'revoke_access_token')\n-    def test_clear_wo_oauth_settings(self, mock_revoke_access_token):\n-        user_settings = GoogleDriveUserSettingsFactory(access_token='abcde')\n-        user_settings.oauth_settings = None\n-        user_settings.save()\n-        node_settings = GoogleDriveNodeSettingsFactory.build()\n-        node_settings.user_settings = user_settings\n-        node_settings.save()\n-\n-        assert_false(user_settings.oauth_settings)\n-        user_settings.clear()\n-        user_settings.save()\n-\n-        assert_false(mock_revoke_access_token.called)\n-\n-    def test_delete(self):\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        assert_true(user_settings.has_auth)\n-        user_settings.delete()\n-        user_settings.save()\n-        assert_false(user_settings.access_token)\n-        assert_true(user_settings.deleted)\n-\n-    def test_delete_clears_associated_node_settings(self):\n-        node_settings = GoogleDriveNodeSettingsFactory.build()\n-        user_settings = GoogleDriveUserSettingsFactory()\n-        node_settings.user_settings = user_settings\n-        node_settings.save()\n-\n-        user_settings.delete()\n-        user_settings.save()\n-\n-        # Node settings no longer associated with user settings\n-        assert_false(node_settings.deleted)\n-        assert_is(node_settings.folder_id, None)\n-        assert_is(node_settings.user_settings, None)\n-\n-\n-class TestGoogleDriveNodeSettingsModel(OsfTestCase):\n+        assert_equal(\n+            self.user_settings.oauth_grants,\n+            {self.node._id: {self.external_account._id: {}}},\n+        )\n \n-    def setUp(self):\n-        super(TestGoogleDriveNodeSettingsModel, self).setUp()\n-        self.user = UserFactory()\n-        self.user.add_addon('googledrive')\n-        self.user.save()\n-        self.user_settings = self.user.get_addon('googledrive')\n-        oauth_settings = GoogleDriveOAuthSettingsFactory()\n-        oauth_settings.save()\n-        self.user_settings.oauth_settings = oauth_settings\n+    def test_grant_oauth_access_metadata(self):\n+        self.user_settings.grant_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+            metadata={'folder': 'fake_folder_id'}\n+        )\n+        self.user_settings.save()\n+\n+        assert_equal(\n+            self.user_settings.oauth_grants,\n+            {\n+                self.node._id: {\n+                    self.external_account._id: {'folder': 'fake_folder_id'}\n+                },\n+            }\n+        )\n+\n+    def test_verify_oauth_access_no_metadata(self):\n+        self.user_settings.grant_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+        )\n         self.user_settings.save()\n-        self.project = ProjectFactory()\n-        self.node_settings = GoogleDriveNodeSettingsFactory(\n-            user_settings=self.user_settings,\n-            owner=self.project,\n+\n+        account_has_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account\n         )\n \n-    def test_fields(self):\n-        node_settings = GoogleDriveNodeSettings(user_settings=self.user_settings)\n-        node_settings.save()\n+        factory_account_has_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=ExternalAccountFactory()\n+        )\n \n-        assert_true(node_settings.user_settings)\n-        assert_true(hasattr(node_settings, 'folder_id'))\n-        assert_true(hasattr(node_settings, 'folder_path'))\n-        assert_true(hasattr(node_settings, 'folder_name'))\n-        assert_equal(node_settings.user_settings.owner, self.user)\n+        assert_true(account_has_access)\n+        assert_false(factory_account_has_access)\n \n-    def test_complete_true(self):\n-        assert_true(self.node_settings.has_auth)\n-        assert_true(self.node_settings.complete)\n+    def test_verify_oauth_access_metadata(self):\n+        self.user_settings.grant_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+            metadata={'folder': 'fake_folder_id'}\n+        )\n+        self.user_settings.save()\n \n-    def test_complete_false(self):\n-        self.node_settings.folder_id = None\n+        correct_meta_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+            metadata={'folder': 'fake_folder_id'}\n+        )\n \n-        assert_true(self.node_settings.has_auth)\n-        assert_false(self.node_settings.complete)\n+        incorrect_meta_no_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=self.external_account,\n+            metadata={'folder': 'another_folder_id'}\n+        )\n \n-    def test_complete_auth_false(self):\n-        self.node_settings.user_settings = None\n+        assert_true(correct_meta_access)\n+        assert_false(incorrect_meta_no_access)\n \n-        assert_false(self.node_settings.has_auth)\n-        assert_false(self.node_settings.complete)\n \n-    def test_folder_defaults_to_none(self):\n-        node_settings = GoogleDriveNodeSettings(user_settings=self.user_settings)\n-        node_settings.save()\n+class TestGoogleDriveNodeSettings(OsfTestCase):\n+\n+    def setUp(self):\n+        super(TestGoogleDriveNodeSettings, self).setUp()\n+        self.node = ProjectFactory()\n+        self.node_settings = model.GoogleDriveNodeSettings(owner=self.node)\n+        self.node_settings.save()\n+        self.user = self.node.creator\n+        self.user_settings = self.user.get_or_add_addon('googledrive')\n+\n+    def tearDown(self):\n+        super(TestGoogleDriveNodeSettings, self).tearDown()\n+        self.user_settings.remove()\n+        self.node_settings.remove()\n+        self.node.remove()\n+        self.user.remove()\n+\n+    @mock.patch('website.addons.googledrive.model.GoogleDriveProvider')\n+    def test_api_not_cached(self, mock_gdp):\n+        # The first call to .api returns a new object\n+        api = self.node_settings.api\n+        mock_gdp.assert_called_once()\n+        assert_equal(api, mock_gdp())\n+\n+    @mock.patch('website.addons.googledrive.model.GoogleDriveProvider')\n+    def test_api_cached(self, mock_gdp):\n+        # Repeated calls to .api returns the same object\n+        self.node_settings._api = 'testapi'\n+        api = self.node_settings.api\n+        assert_false(mock_gdp.called)\n+        assert_equal(api, 'testapi')\n+\n+    def test_set_auth(self):\n+        external_account = ExternalAccountFactory()\n+        self.user.external_accounts.append(external_account)\n+        self.user.save()\n \n-        assert_is_none(node_settings.folder_id)\n-        assert_is_none(node_settings.folder_path)\n+        # this should be reset after the call\n+        self.node_settings.drive_folder_id = 'anything'\n \n-    def test_has_auth(self):\n-        settings = GoogleDriveNodeSettings(user_settings=self.user_settings)\n-        settings.user_settings.access_token = None\n-        settings.save()\n+        self.node_settings.set_auth(\n+            external_account=external_account,\n+            user=self.user\n+        )\n \n-        assert_false(settings.has_auth)\n+        # this instance is updated\n+        assert_equal(\n+            self.node_settings.external_account,\n+            external_account\n+        )\n+        assert_equal(\n+            self.node_settings.user_settings,\n+            self.user_settings\n+        )\n+        assert_is_none(\n+            self.node_settings.drive_folder_id\n+        )\n \n-        settings.user_settings.access_token = '123abc'\n-        settings.user_settings.save()\n+        set_auth_gives_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=external_account,\n+        )\n \n-        assert_true(settings.has_auth)\n+        assert_true(set_auth_gives_access)\n \n-    # TODO use this test if delete function is used in googledrive/model\n-    # def test_delete(self):\n-    #     assert_true(self.node_settings.user_settings)\n-    #     assert_true(self.node_settings.folder)\n-    #     old_logs = self.project.logs\n-    #     self.node_settings.delete()\n-    #     self.node_settings.save()\n-    #     assert_is(self.node_settings.user_settings, None)\n-    #     assert_is(self.node_settings.folder, None)\n-    #     assert_true(self.node_settings.deleted)\n-    #     assert_equal(self.project.logs, old_logs)\n+    def test_set_auth_wrong_user(self):\n+        external_account = ExternalAccountFactory()\n+        self.user.external_accounts.append(external_account)\n+        self.user.save()\n \n-    def test_deauthorize(self):\n-        assert_true(self.node_settings.folder_id)\n-        assert_true(self.node_settings.user_settings)\n+        with assert_raises(PermissionsError):\n+            self.node_settings.set_auth(\n+                external_account=external_account,\n+                user=UserFactory()\n+            )\n \n-        self.node_settings.deauthorize(auth=Auth(self.user))\n+    def test_clear_auth(self):\n+        self.node_settings.external_account = ExternalAccountFactory()\n+        self.node_settings.drive_folder_id = 'something'\n+        self.node_settings.user_settings = self.user_settings\n         self.node_settings.save()\n \n-        assert_is(self.node_settings.folder_id, None)\n-        assert_is(self.node_settings.user_settings, None)\n+        self.node_settings.clear_auth()\n \n-        last_log = self.project.logs[-1]\n-        params = last_log.params\n+        assert_is_none(self.node_settings.external_account)\n+        assert_is_none(self.node_settings.drive_folder_id)\n+        assert_is_none(self.node_settings.user_settings)\n+        assert_is_none(self.node_settings.folder_path)\n+        assert_is_none(self.node_settings.drive_folder_name)\n \n-        assert_in('node', params)\n-        assert_in('folder', params)\n-        assert_in('project', params)\n-        assert_equal(last_log.action, 'googledrive_node_deauthorized')\n+    def test_set_target_folder(self):\n \n-    def test_set_folder(self):\n-        folder_name = {\n-            'id': '1234',\n-            'name': 'freddie',\n-            'path': 'queen/freddie',\n+        folder = {\n+            'id': 'fake-folder-id',\n+            'name': 'fake-folder-name',\n+            'path': 'fake_path'\n         }\n \n-        self.node_settings.set_folder(folder_name, auth=Auth(self.user))\n+\n+        external_account = ExternalAccountFactory()\n+        self.user.external_accounts.append(external_account)\n+        self.user.save()\n+\n+        self.node_settings.set_auth(\n+            external_account=external_account,\n+            user=self.user,\n+            )\n+\n+        assert_is_none(self.node_settings.drive_folder_id)\n+\n+        self.node_settings.set_target_folder(\n+            folder,\n+            auth=Auth(user=self.user),\n+            )\n+\n+        # instance was updated\n+        assert_equal(\n+            self.node_settings.drive_folder_id,\n+            'fake-folder-id',\n+            )\n+\n+        has_access = self.user_settings.verify_oauth_access(\n+            node=self.node,\n+            external_account=external_account,\n+            metadata={'folder': 'fake-folder-id'}\n+        )\n+\n+        # user_settings was updated\n+        assert_true(has_access)\n+\n+        log = self.node.logs[-1]\n+        assert_equal(log.action, 'googledrive_folder_selected')\n+        assert_equal(log.params['folder_id'], folder['id'])\n+        assert_equal(log.params['folder_name'], folder['path'])\n+\n+    def test_has_auth_false(self):\n+        external_account = ExternalAccountFactory()\n+\n+        assert_false(self.node_settings.has_auth)\n+\n+        # both external_account and user_settings must be set to have auth\n+        self.node_settings.external_account = external_account\n+        assert_false(self.node_settings.has_auth)\n+\n+        self.node_settings.external_account = None\n+        self.node_settings.user_settings = self.user_settings\n+        assert_false(self.node_settings.has_auth)\n+\n+        # set_auth must be called to have auth\n+        self.node_settings.external_account = external_account\n+        self.node_settings.user_settings = self.user_settings\n+        assert_false(self.node_settings.has_auth)\n+\n+    def test_has_auth_true(self):\n+        external_account = ExternalAccountFactory()\n+        self.user.external_accounts.append(external_account)\n+\n+        self.node_settings.set_auth(external_account, self.user)\n+\n+        # mendeley_list_id should have no effect","path":"website/addons/googledrive/tests/test_models.py","position":510,"original_position":510,"commit_id":"6733bd69b7ea4842be53c20c808a87f9c915a8d6","original_commit_id":"6733bd69b7ea4842be53c20c808a87f9c915a8d6","user":{"login":"abought","id":2957073,"avatar_url":"https://avatars.githubusercontent.com/u/2957073?v=3","gravatar_id":"","url":"https://api.github.com/users/abought","html_url":"https://github.com/abought","followers_url":"https://api.github.com/users/abought/followers","following_url":"https://api.github.com/users/abought/following{/other_user}","gists_url":"https://api.github.com/users/abought/gists{/gist_id}","starred_url":"https://api.github.com/users/abought/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/abought/subscriptions","organizations_url":"https://api.github.com/users/abought/orgs","repos_url":"https://api.github.com/users/abought/repos","events_url":"https://api.github.com/users/abought/events{/privacy}","received_events_url":"https://api.github.com/users/abought/received_events","type":"User","site_admin":false},"body":">         # mendeley_list_id should have no effect \r\n\r\nGiven that this is the Google drive addon, I concur. Worth glancing at this section in case there are any other copy/paste issues.","created_at":"2015-10-06T15:51:46Z","updated_at":"2015-10-06T15:51:46Z","html_url":"https://github.com/CenterForOpenScience/osf.io/pull/4261#discussion_r41282885","pull_request_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261","_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/41282885"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/4261#discussion_r41282885"},"pull_request":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261"}}},"pull_request":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261","id":44473275,"html_url":"https://github.com/CenterForOpenScience/osf.io/pull/4261","diff_url":"https://github.com/CenterForOpenScience/osf.io/pull/4261.diff","patch_url":"https://github.com/CenterForOpenScience/osf.io/pull/4261.patch","issue_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4261","number":4261,"state":"open","locked":false,"title":"[Feature][Ready for Review] Update GDrive Oauth Refactor PR","user":{"login":"mfraezz","id":5659262,"avatar_url":"https://avatars.githubusercontent.com/u/5659262?v=3","gravatar_id":"","url":"https://api.github.com/users/mfraezz","html_url":"https://github.com/mfraezz","followers_url":"https://api.github.com/users/mfraezz/followers","following_url":"https://api.github.com/users/mfraezz/following{/other_user}","gists_url":"https://api.github.com/users/mfraezz/gists{/gist_id}","starred_url":"https://api.github.com/users/mfraezz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mfraezz/subscriptions","organizations_url":"https://api.github.com/users/mfraezz/orgs","repos_url":"https://api.github.com/users/mfraezz/repos","events_url":"https://api.github.com/users/mfraezz/events{/privacy}","received_events_url":"https://api.github.com/users/mfraezz/received_events","type":"User","site_admin":false},"body":"Purpose\r\n=======\r\nUpdates/replaces https://github.com/CenterForOpenScience/osf.io/pull/2231\r\n\r\nChanges\r\n=======\r\nNo user-facing changes, strictly back-end: mostly oauth flow, also static files associated with GDrive\r\n\r\nSide Effects\r\n=========\r\nFrom old PR: \r\nAny dependencies on node/user configurations files specific to google drive might be affected.\r\n\r\nNote\r\n====\r\nBefore merge, #4396 must go in and be run to migrate old account info\r\n\r\n[#OSF-2272]","created_at":"2015-09-08T19:33:28Z","updated_at":"2015-10-06T15:51:46Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f3e2a3dd07dec49bfd9ab38fa2ab0ad1f351eb19","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261/commits","review_comments_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261/comments","review_comment_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments{/number}","comments_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4261/comments","statuses_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/6733bd69b7ea4842be53c20c808a87f9c915a8d6","head":{"label":"mfraezz:feature/update-gdrive-oauth-pr","ref":"feature/update-gdrive-oauth-pr","sha":"6733bd69b7ea4842be53c20c808a87f9c915a8d6","user":{"login":"mfraezz","id":5659262,"avatar_url":"https://avatars.githubusercontent.com/u/5659262?v=3","gravatar_id":"","url":"https://api.github.com/users/mfraezz","html_url":"https://github.com/mfraezz","followers_url":"https://api.github.com/users/mfraezz/followers","following_url":"https://api.github.com/users/mfraezz/following{/other_user}","gists_url":"https://api.github.com/users/mfraezz/gists{/gist_id}","starred_url":"https://api.github.com/users/mfraezz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mfraezz/subscriptions","organizations_url":"https://api.github.com/users/mfraezz/orgs","repos_url":"https://api.github.com/users/mfraezz/repos","events_url":"https://api.github.com/users/mfraezz/events{/privacy}","received_events_url":"https://api.github.com/users/mfraezz/received_events","type":"User","site_admin":false},"repo":{"id":26929858,"name":"osf.io","full_name":"mfraezz/osf.io","owner":{"login":"mfraezz","id":5659262,"avatar_url":"https://avatars.githubusercontent.com/u/5659262?v=3","gravatar_id":"","url":"https://api.github.com/users/mfraezz","html_url":"https://github.com/mfraezz","followers_url":"https://api.github.com/users/mfraezz/followers","following_url":"https://api.github.com/users/mfraezz/following{/other_user}","gists_url":"https://api.github.com/users/mfraezz/gists{/gist_id}","starred_url":"https://api.github.com/users/mfraezz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mfraezz/subscriptions","organizations_url":"https://api.github.com/users/mfraezz/orgs","repos_url":"https://api.github.com/users/mfraezz/repos","events_url":"https://api.github.com/users/mfraezz/events{/privacy}","received_events_url":"https://api.github.com/users/mfraezz/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mfraezz/osf.io","description":"Facilitating Open Science","fork":true,"url":"https://api.github.com/repos/mfraezz/osf.io","forks_url":"https://api.github.com/repos/mfraezz/osf.io/forks","keys_url":"https://api.github.com/repos/mfraezz/osf.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mfraezz/osf.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mfraezz/osf.io/teams","hooks_url":"https://api.github.com/repos/mfraezz/osf.io/hooks","issue_events_url":"https://api.github.com/repos/mfraezz/osf.io/issues/events{/number}","events_url":"https://api.github.com/repos/mfraezz/osf.io/events","assignees_url":"https://api.github.com/repos/mfraezz/osf.io/assignees{/user}","branches_url":"https://api.github.com/repos/mfraezz/osf.io/branches{/branch}","tags_url":"https://api.github.com/repos/mfraezz/osf.io/tags","blobs_url":"https://api.github.com/repos/mfraezz/osf.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mfraezz/osf.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mfraezz/osf.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/mfraezz/osf.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mfraezz/osf.io/statuses/{sha}","languages_url":"https://api.github.com/repos/mfraezz/osf.io/languages","stargazers_url":"https://api.github.com/repos/mfraezz/osf.io/stargazers","contributors_url":"https://api.github.com/repos/mfraezz/osf.io/contributors","subscribers_url":"https://api.github.com/repos/mfraezz/osf.io/subscribers","subscription_url":"https://api.github.com/repos/mfraezz/osf.io/subscription","commits_url":"https://api.github.com/repos/mfraezz/osf.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/mfraezz/osf.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/mfraezz/osf.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/mfraezz/osf.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/mfraezz/osf.io/contents/{+path}","compare_url":"https://api.github.com/repos/mfraezz/osf.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mfraezz/osf.io/merges","archive_url":"https://api.github.com/repos/mfraezz/osf.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mfraezz/osf.io/downloads","issues_url":"https://api.github.com/repos/mfraezz/osf.io/issues{/number}","pulls_url":"https://api.github.com/repos/mfraezz/osf.io/pulls{/number}","milestones_url":"https://api.github.com/repos/mfraezz/osf.io/milestones{/number}","notifications_url":"https://api.github.com/repos/mfraezz/osf.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mfraezz/osf.io/labels{/name}","releases_url":"https://api.github.com/repos/mfraezz/osf.io/releases{/id}","created_at":"2014-11-20T20:12:10Z","updated_at":"2015-01-28T20:39:13Z","pushed_at":"2015-10-06T15:33:57Z","git_url":"git://github.com/mfraezz/osf.io.git","ssh_url":"git@github.com:mfraezz/osf.io.git","clone_url":"https://github.com/mfraezz/osf.io.git","svn_url":"https://github.com/mfraezz/osf.io","homepage":"http://osf.io","size":101749,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"develop"}},"base":{"label":"CenterForOpenScience:develop","ref":"develop","sha":"18a7a05ffa91d70759083f665cd39bfe7e0bcb27","user":{"login":"CenterForOpenScience","id":3344584,"avatar_url":"https://avatars.githubusercontent.com/u/3344584?v=3","gravatar_id":"","url":"https://api.github.com/users/CenterForOpenScience","html_url":"https://github.com/CenterForOpenScience","followers_url":"https://api.github.com/users/CenterForOpenScience/followers","following_url":"https://api.github.com/users/CenterForOpenScience/following{/other_user}","gists_url":"https://api.github.com/users/CenterForOpenScience/gists{/gist_id}","starred_url":"https://api.github.com/users/CenterForOpenScience/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CenterForOpenScience/subscriptions","organizations_url":"https://api.github.com/users/CenterForOpenScience/orgs","repos_url":"https://api.github.com/users/CenterForOpenScience/repos","events_url":"https://api.github.com/users/CenterForOpenScience/events{/privacy}","received_events_url":"https://api.github.com/users/CenterForOpenScience/received_events","type":"Organization","site_admin":false},"repo":{"id":10199599,"name":"osf.io","full_name":"CenterForOpenScience/osf.io","owner":{"login":"CenterForOpenScience","id":3344584,"avatar_url":"https://avatars.githubusercontent.com/u/3344584?v=3","gravatar_id":"","url":"https://api.github.com/users/CenterForOpenScience","html_url":"https://github.com/CenterForOpenScience","followers_url":"https://api.github.com/users/CenterForOpenScience/followers","following_url":"https://api.github.com/users/CenterForOpenScience/following{/other_user}","gists_url":"https://api.github.com/users/CenterForOpenScience/gists{/gist_id}","starred_url":"https://api.github.com/users/CenterForOpenScience/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CenterForOpenScience/subscriptions","organizations_url":"https://api.github.com/users/CenterForOpenScience/orgs","repos_url":"https://api.github.com/users/CenterForOpenScience/repos","events_url":"https://api.github.com/users/CenterForOpenScience/events{/privacy}","received_events_url":"https://api.github.com/users/CenterForOpenScience/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CenterForOpenScience/osf.io","description":"Facilitating Open Science","fork":false,"url":"https://api.github.com/repos/CenterForOpenScience/osf.io","forks_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/forks","keys_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/teams","hooks_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/hooks","issue_events_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/events{/number}","events_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/events","assignees_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/assignees{/user}","branches_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/branches{/branch}","tags_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/tags","blobs_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/{sha}","languages_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/languages","stargazers_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/stargazers","contributors_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/contributors","subscribers_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/subscribers","subscription_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/subscription","commits_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/contents/{+path}","compare_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/merges","archive_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/downloads","issues_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues{/number}","pulls_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls{/number}","milestones_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/milestones{/number}","notifications_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/labels{/name}","releases_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/releases{/id}","created_at":"2013-05-21T15:53:37Z","updated_at":"2015-09-28T06:19:31Z","pushed_at":"2015-10-06T15:45:01Z","git_url":"git://github.com/CenterForOpenScience/osf.io.git","ssh_url":"git@github.com:CenterForOpenScience/osf.io.git","clone_url":"https://github.com/CenterForOpenScience/osf.io.git","svn_url":"https://github.com/CenterForOpenScience/osf.io","homepage":"http://osf.io","size":232760,"stargazers_count":140,"watchers_count":140,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":120,"mirror_url":null,"open_issues_count":414,"forks":120,"open_issues":414,"watchers":140,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/4261"},"issue":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4261"},"comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4261/comments"},"review_comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261/comments"},"review_comment":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4261/commits"},"statuses":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/6733bd69b7ea4842be53c20c808a87f9c915a8d6"}}}}