{"action":"created","comment":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/39054713","id":39054713,"diff_hunk":"@@ -863,6 +865,512 @@ def test_filtering_on_invalid_field(self):\n         assert_equal(len(errors), 1)\n         assert_equal(errors[0]['detail'], 'Querystring contains an invalid filter.')\n \n+\n+class TestNodeContributorAdd(ApiTestCase):\n+    def setUp(self):\n+        super(TestNodeContributorAdd, self).setUp()\n+        self.user = AuthUserFactory()\n+        self.user_two = AuthUserFactory()\n+        self.user_three = AuthUserFactory()\n+\n+        self.private_project = ProjectFactory(creator=self.user, is_public=False)\n+        self.private_url = '/{}nodes/{}/contributors/'.format(API_BASE, self.private_project._id)\n+\n+        self.public_project = ProjectFactory(creator=self.user, is_public=True)\n+        self.public_url = '/{}nodes/{}/contributors/'.format(API_BASE, self.public_project._id)\n+\n+        self.data_user_two = {\n+            'id': self.user_two._id,\n+            'bibliographic': True\n+        }\n+\n+        self.data_user_three = {\n+            'id': self.user_three._id,\n+            'bibliographic': True\n+        }\n+\n+    def test_adds_bibliographic_contributor_public_project_admin(self):\n+        res = self.app.post_json(self.public_url, self.data_user_two, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.public_project.reload()\n+        assert_in(self.user_two, self.public_project.contributors)\n+\n+    def test_adds_non_bibliographic_contributor_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'bibliographic': False\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+        assert_false(self.private_project.get_visible(self.user_two))\n+\n+    def test_adds_contributor_public_project_non_admin(self):\n+        self.public_project.add_contributor(self.user_two, permissions=[permissions.READ, permissions.WRITE], auth=Auth(self.user), save=True)\n+        res = self.app.post_json(self.public_url, self.data_user_three,\n+                                 auth=self.user_two.auth, expect_errors=True)\n+        self.public_project.reload()\n+        assert_equal(res.status_code, 403)\n+        assert_not_in(self.user_three, self.public_project.contributors)\n+\n+    def test_adds_contributor_public_project_non_contributor(self):\n+        res = self.app.post_json(self.public_url, self.data_user_three,\n+                                 auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+        assert_not_in(self.user_three, self.public_project.contributors)\n+\n+    def test_adds_contributor_public_project_not_logged_in(self):\n+        res = self.app.post_json(self.public_url, self.data_user_two, expect_errors=True)\n+        assert_equal(res.status_code, 401)\n+        assert_not_in(self.user_two, self.public_project.contributors)\n+\n+    def test_adds_contributor_private_project_admin(self):\n+        res = self.app.post_json(self.private_url, self.data_user_two, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+\n+    def test_adds_contributor_without_bibliographic_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+\n+        self.private_project.reload()\n+        assert_not_in(self.user_two, self.private_project.contributors)\n+\n+    def test_adds_admin_contributor_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'permission': permissions.ADMIN,\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+        assert_equal(self.private_project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE, permissions.ADMIN])\n+\n+    def test_adds_write_contributor_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'permission': permissions.WRITE,\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+        assert_equal(self.private_project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+\n+    def test_adds_read_contributor_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'permission': permissions.READ,\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+        assert_equal(res.json['data']['id'], self.user_two._id)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+        assert_equal(self.private_project.get_permissions(self.user_two), [permissions.READ])\n+\n+    def test_adds_invalid_permission_contributor_private_project_admin(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'permission': 'invalid',\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+\n+        self.private_project.reload()\n+        assert_not_in(self.user_two, self.private_project.contributors)\n+\n+    def test_adds_none_permission_contributor_private_project_admin_uses_default_permissions(self):\n+        data = {\n+            'id': self.user_two._id,\n+            'permission': None,\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 201)\n+\n+        self.private_project.reload()\n+        assert_in(self.user_two, self.private_project.contributors)\n+        for permission in permissions.DEFAULT_CONTRIBUTOR_PERMISSIONS:\n+            assert_true(self.private_project.has_permission(self.user_two, permission))\n+\n+    def test_adds_already_existing_contributor_private_project_admin(self):\n+        self.private_project.add_contributor(self.user_two, auth=Auth(self.user), save=True)\n+        self.private_project.reload()\n+\n+        res = self.app.post_json(self.private_url, self.data_user_two,\n+                                 auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+\n+    def test_adds_non_existing_user_private_project_admin(self):\n+        data = {\n+            'id': 'Fake',\n+            'bibliographic': True\n+        }\n+        res = self.app.post_json(self.private_url, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 404)\n+\n+        self.private_project.reload()\n+        assert_equal(len(self.private_project.contributors), 1)\n+\n+    def test_adds_contributor_private_project_non_admin(self):\n+        self.private_project.add_contributor(self.user_two, permissions=[permissions.READ, permissions.WRITE], auth=Auth(self.user))\n+        res = self.app.post_json(self.private_url, self.data_user_three,\n+                                 auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+\n+        self.private_project.reload()\n+        assert_not_in(self.user_three, self.private_project.contributors)\n+\n+    def test_adds_contributor_private_project_non_contributor(self):\n+        res = self.app.post_json(self.private_url, self.data_user_three,\n+                                 auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+\n+        self.private_project.reload()\n+        assert_not_in(self.user_three, self.private_project.contributors)\n+\n+    def test_adds_contributor_private_project_not_logged_in(self):\n+        res = self.app.post_json(self.private_url, self.data_user_two, expect_errors=True)\n+        assert_equal(res.status_code, 401)\n+\n+        self.private_project.reload()\n+        assert_not_in(self.user_two, self.private_project.contributors)\n+\n+\n+class TestContributorDetail(ApiTestCase):\n+    def setUp(self):\n+        super(TestContributorDetail, self).setUp()\n+        self.user = AuthUserFactory()\n+        self.user_two = AuthUserFactory()\n+\n+        self.public_project = ProjectFactory(creator=self.user, is_public=True)\n+        self.public_url = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.public_project, self.user._id)\n+\n+        self.private_project = ProjectFactory(creator=self.user, is_public=False)\n+        self.private_url_base = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.private_project._id, '{}')\n+        self.private_url = self.private_url_base.format(self.user._id)\n+\n+    def test_get_public_contributor_detail(self):\n+        res = self.app.get(self.public_url)\n+        assert_equal(res.status_code, 200)\n+        assert_equal(res.json['data']['id'], self.user._id)\n+\n+    def test_get_private_node_contributor_detail_contributor_auth(self):\n+        res = self.app.get(self.private_url, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        assert_equal(res.json['data']['id'], self.user._id)\n+\n+    def test_get_private_node_contributor_detail_non_contributor(self):\n+        res = self.app.get(self.private_url, auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+\n+    def test_get_private_node_contributor_detail_not_logged_in(self):\n+        res = self.app.get(self.private_url, expect_errors=True)\n+        assert_equal(res.status_code, 401)\n+\n+    def test_get_private_node_non_contributor_detail_contributor_auth(self):\n+        res = self.app.get(self.private_url_base.format(self.user_two._id), auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 404)\n+\n+    def test_get_private_node_invalid_user_detail_contributor_auth(self):\n+        res = self.app.get(self.private_url_base.format('invalid'), auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 404)\n+\n+\n+class TestNodeContributorUpdate(ApiTestCase):\n+    def setUp(self):\n+        super(TestNodeContributorUpdate, self).setUp()\n+        self.user = AuthUserFactory()\n+        self.user_two = AuthUserFactory()\n+\n+        self.project = ProjectFactory(creator=self.user)\n+        self.project.add_contributor(self.user_two, permissions=[permissions.READ, permissions.WRITE], visible=True, save=True)\n+        self.project.reload()\n+\n+        self.url_creator = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.project._id, self.user._id)\n+        self.url_contributor = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.project._id, self.user_two._id)\n+\n+    def test_change_contributor_permissions(self):\n+        data = {\n+            'permission': permissions.ADMIN,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.ADMIN)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE, permissions.ADMIN])\n+\n+        data = {\n+            'permission': permissions.WRITE,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.WRITE)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+\n+        data = {\n+            'permission': permissions.READ,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.READ)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ])\n+\n+    def test_change_contributor_bibliographic(self):\n+        data = {\n+            'bibliographic': False\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['bibliographic'], False)\n+\n+        self.project.reload()\n+        assert_false(self.project.get_visible(self.user_two))\n+\n+        data = {\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)        \n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['bibliographic'], True)\n+\n+        self.project.reload()\n+        assert_true(self.project.get_visible(self.user_two))\n+\n+    def test_change_contributor_permission_and_bibliographic(self):\n+        data = {\n+            'permission': permissions.READ,\n+            'bibliographic': False\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.READ)\n+        assert_equal(attributes['bibliographic'], False)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ])\n+        assert_false(self.project.get_visible(self.user_two))\n+\n+    def test_not_change_contributor(self):\n+        data = {\n+            'permission': None,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.WRITE)\n+        assert_equal(attributes['bibliographic'], True)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+        assert_true(self.project.get_visible(self.user_two))\n+\n+    def test_invalid_change_inputs_contributor(self):\n+        data = {\n+            'permission': 'invalid',\n+            'bibliographic': 'invalid'\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+        assert_true(self.project.get_visible(self.user_two))\n+\n+    def test_change_admin_self_with_other_admin(self):\n+        self.project.add_permission(self.user_two, permissions.ADMIN, save=True)\n+        self.project.reload()\n+\n+        data = {\n+            'permission': permissions.WRITE,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_creator, data, auth=self.user.auth)\n+        assert_equal(res.status_code, 200)\n+        attributes = res.json['data']['attributes']\n+        assert_equal(attributes['permission'], permissions.WRITE)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user), [permissions.READ, permissions.WRITE])\n+\n+    def test_change_admin_self_without_other_admin(self):\n+        data = {\n+            'permission': permissions.WRITE,\n+            'bibliographic': True\n+        }\n+        res = self.app.put_json(self.url_creator, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user), [permissions.READ, permissions.WRITE, permissions.ADMIN])\n+\n+    def test_remove_all_bibliographic_statuses_contributors(self):\n+        self.project.set_visible(self.user_two, False, save=True)\n+        self.project.reload()\n+        \n+        data = {\n+            'bibliographic': False\n+        }\n+        res = self.app.put_json(self.url_creator, data, auth=self.user.auth, expect_errors=True)\n+        assert_equal(res.status_code, 400)\n+\n+        self.project.reload()\n+        assert_true(self.project.get_visible(self.user))\n+\n+    def test_change_contributor_non_admin_auth(self):\n+        data = {\n+            'permission': permissions.READ,\n+            'bibliographic': False\n+        }\n+        res = self.app.put_json(self.url_contributor, data, auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+        assert_true(self.project.get_visible(self.user_two))\n+\n+    def test_change_contributor_not_logged_in(self):\n+        data = {\n+            'permission': permissions.READ,\n+            'bibliographic': False\n+        }\n+        res = self.app.put_json(self.url_contributor, data, expect_errors=True)\n+        assert_equal(res.status_code, 401)\n+\n+        self.project.reload()\n+        assert_equal(self.project.get_permissions(self.user_two), [permissions.READ, permissions.WRITE])\n+        assert_true(self.project.get_visible(self.user_two))\n+\n+\n+class TestNodeContributorDelete(ApiTestCase):\n+    def setUp(self):\n+        super(TestNodeContributorDelete, self).setUp()\n+        self.user = AuthUserFactory()\n+        self.user_two = AuthUserFactory()\n+        self.user_three = AuthUserFactory()\n+\n+        self.project = ProjectFactory(creator=self.user)\n+        self.project.add_contributor(self.user_two, permissions=[permissions.READ, permissions.WRITE], visible=True, save=True)\n+        self.project.reload()\n+\n+        self.url_user = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.project._id, self.user._id)\n+        self.url_user_two = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.project._id, self.user_two._id)\n+        self.url_user_three = '/{}nodes/{}/contributors/{}/'.format(API_BASE, self.project._id, self.user_three._id)\n+\n+    def test_remove_contributor_admin(self):\n+        res = self.app.delete(self.url_user_two, auth=self.user.auth)\n+        assert_equal(res.status_code, 204)\n+\n+        self.project.reload()\n+        assert_not_in(self.user_two, self.project.contributors)\n+\n+    def test_remove_contributor_non_admin_is_forbidden(self):\n+        self.project.add_contributor(self.user_three, permissions=[permissions.READ, permissions.WRITE], visible=True, save=True)\n+\n+        res = self.app.delete(self.url_user_three, auth=self.user_two.auth, expect_errors=True)\n+        assert_equal(res.status_code, 403)\n+\n+        self.project.reload()\n+        assert_in(self.user_three, self.project.contributors)\n+\n+    def test_remove_self_non_admin(self):","path":"tests/api_tests/nodes/test_views.py","position":465,"original_position":465,"commit_id":"cfb3783745970c4847c2891b8581554ba5a064d1","original_commit_id":"cfb3783745970c4847c2891b8581554ba5a064d1","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":"Thanks for adding this test. Can we also get an assertion checking the status code from the request? (204, I think)","created_at":"2015-09-09T15:08:41Z","updated_at":"2015-09-09T15:08:41Z","html_url":"https://github.com/CenterForOpenScience/osf.io/pull/4235#discussion_r39054713","pull_request_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235","_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/39054713"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/4235#discussion_r39054713"},"pull_request":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235"}}},"pull_request":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235","id":44032982,"html_url":"https://github.com/CenterForOpenScience/osf.io/pull/4235","diff_url":"https://github.com/CenterForOpenScience/osf.io/pull/4235.diff","patch_url":"https://github.com/CenterForOpenScience/osf.io/pull/4235.patch","issue_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4235","number":4235,"state":"open","locked":false,"title":"[WIP] Feature/api/node contrib crud","user":{"login":"samchrisinger","id":2207561,"avatar_url":"https://avatars.githubusercontent.com/u/2207561?v=3","gravatar_id":"","url":"https://api.github.com/users/samchrisinger","html_url":"https://github.com/samchrisinger","followers_url":"https://api.github.com/users/samchrisinger/followers","following_url":"https://api.github.com/users/samchrisinger/following{/other_user}","gists_url":"https://api.github.com/users/samchrisinger/gists{/gist_id}","starred_url":"https://api.github.com/users/samchrisinger/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/samchrisinger/subscriptions","organizations_url":"https://api.github.com/users/samchrisinger/orgs","repos_url":"https://api.github.com/users/samchrisinger/repos","events_url":"https://api.github.com/users/samchrisinger/events{/privacy}","received_events_url":"https://api.github.com/users/samchrisinger/received_events","type":"User","site_admin":false},"body":"# Purpose\r\nBring https://github.com/CenterForOpenScience/osf.io/pull/3990/ up to date with develop. \r\n\r\nAllows user to add, edit, and remove users through API requests.\r\n\r\n# Changes\r\n\r\nAdded new serializer for contributor, removing old one. Created ability to add a contributor to a node in node contributor list. Added serializer for contributor detail where user can edit contributor bibliographic and permissions.\r\n\r\n# Side Effects\r\n\r\nAPI user can now add, remove, and update contributors as well as view contributor permission level and view contributor details separate from list\r\n\r\n```json\r\n\r\n{\r\n    \"data\": [\r\n        {\r\n            \"id\": \"j84ad\",\r\n            \"type\": \"users\",\r\n            \"attributes\": {\r\n                \"fullname\": \"Samuel Chrisinger\",\r\n                \"given_name\": \"Samuel\",\r\n                \"middle_name\": \"\",\r\n                \"family_name\": \"Chrisinger\",\r\n                \"suffix\": \"\",\r\n                \"date_registered\": \"2015-08-12T15:34:00.814000\",\r\n                \"gravatar_url\": \"https://secure.gravatar.com/avatar/474539e4281a2712b321cd5c433fe2f9?d=identicon&size=40\",\r\n                \"social_accounts\": {},\r\n                \"bibliographic\": true,\r\n                \"permission\": \"admin\"\r\n            },\r\n            \"links\": {\r\n                \"nodes\": {\r\n                    \"relation\": \"http://localhost:8000/v2/users/j84ad/nodes/\"\r\n                },\r\n                \"html\": \"http://localhost:5000/j84ad/\",\r\n                \"detail\": \"http://localhost:8000/v2/nodes/23ehy/contributors/j84ad/\",\r\n                \"self\": \"http://localhost:8000/v2/users/j84ad/\"\r\n            }\r\n        }\r\n    ],\r\n    \"links\": {\r\n        \"first\": null,\r\n        \"last\": null,\r\n        \"prev\": null,\r\n        \"next\": null,\r\n        \"meta\": {\r\n            \"total\": 1,\r\n            \"per_page\": 10\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n![screen shot 2015-09-03 at 10 10 31 am](https://cloud.githubusercontent.com/assets/2207561/9660578/4d4ed7fc-5224-11e5-8b89-a7236e13d6a7.png)\r\n\r\n![screen shot 2015-09-03 at 10 11 56 am](https://cloud.githubusercontent.com/assets/2207561/9660567/3e20aeea-5224-11e5-8d9a-628371cf9a12.png)\r\n\r\n![screen shot 2015-09-03 at 10 12 52 am](https://cloud.githubusercontent.com/assets/2207561/9660589/5d12b9c4-5224-11e5-83a2-30dd6cff283e.png)\r\n\r\n![screen shot 2015-09-03 at 10 13 10 am](https://cloud.githubusercontent.com/assets/2207561/9660595/6f18735c-5224-11e5-88cf-1ec112ee72f3.png)\r\n\r\n![screen shot 2015-09-03 at 10 13 17 am](https://cloud.githubusercontent.com/assets/2207561/9660602/7663b464-5224-11e5-9428-73ccdd3e650b.png)\r\n","created_at":"2015-09-02T19:37:04Z","updated_at":"2015-09-09T15:08:41Z","closed_at":null,"merged_at":null,"merge_commit_sha":"049e1576114c91e7698427eaf0688c8f54ca20a9","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235/commits","review_comments_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235/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/4235/comments","statuses_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/cfb3783745970c4847c2891b8581554ba5a064d1","head":{"label":"samchrisinger:feature/api/node-contrib-crud","ref":"feature/api/node-contrib-crud","sha":"cfb3783745970c4847c2891b8581554ba5a064d1","user":{"login":"samchrisinger","id":2207561,"avatar_url":"https://avatars.githubusercontent.com/u/2207561?v=3","gravatar_id":"","url":"https://api.github.com/users/samchrisinger","html_url":"https://github.com/samchrisinger","followers_url":"https://api.github.com/users/samchrisinger/followers","following_url":"https://api.github.com/users/samchrisinger/following{/other_user}","gists_url":"https://api.github.com/users/samchrisinger/gists{/gist_id}","starred_url":"https://api.github.com/users/samchrisinger/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/samchrisinger/subscriptions","organizations_url":"https://api.github.com/users/samchrisinger/orgs","repos_url":"https://api.github.com/users/samchrisinger/repos","events_url":"https://api.github.com/users/samchrisinger/events{/privacy}","received_events_url":"https://api.github.com/users/samchrisinger/received_events","type":"User","site_admin":false},"repo":{"id":30772672,"name":"osf.io","full_name":"samchrisinger/osf.io","owner":{"login":"samchrisinger","id":2207561,"avatar_url":"https://avatars.githubusercontent.com/u/2207561?v=3","gravatar_id":"","url":"https://api.github.com/users/samchrisinger","html_url":"https://github.com/samchrisinger","followers_url":"https://api.github.com/users/samchrisinger/followers","following_url":"https://api.github.com/users/samchrisinger/following{/other_user}","gists_url":"https://api.github.com/users/samchrisinger/gists{/gist_id}","starred_url":"https://api.github.com/users/samchrisinger/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/samchrisinger/subscriptions","organizations_url":"https://api.github.com/users/samchrisinger/orgs","repos_url":"https://api.github.com/users/samchrisinger/repos","events_url":"https://api.github.com/users/samchrisinger/events{/privacy}","received_events_url":"https://api.github.com/users/samchrisinger/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/samchrisinger/osf.io","description":"Facilitating Open Science","fork":true,"url":"https://api.github.com/repos/samchrisinger/osf.io","forks_url":"https://api.github.com/repos/samchrisinger/osf.io/forks","keys_url":"https://api.github.com/repos/samchrisinger/osf.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/samchrisinger/osf.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/samchrisinger/osf.io/teams","hooks_url":"https://api.github.com/repos/samchrisinger/osf.io/hooks","issue_events_url":"https://api.github.com/repos/samchrisinger/osf.io/issues/events{/number}","events_url":"https://api.github.com/repos/samchrisinger/osf.io/events","assignees_url":"https://api.github.com/repos/samchrisinger/osf.io/assignees{/user}","branches_url":"https://api.github.com/repos/samchrisinger/osf.io/branches{/branch}","tags_url":"https://api.github.com/repos/samchrisinger/osf.io/tags","blobs_url":"https://api.github.com/repos/samchrisinger/osf.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/samchrisinger/osf.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/samchrisinger/osf.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/samchrisinger/osf.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/samchrisinger/osf.io/statuses/{sha}","languages_url":"https://api.github.com/repos/samchrisinger/osf.io/languages","stargazers_url":"https://api.github.com/repos/samchrisinger/osf.io/stargazers","contributors_url":"https://api.github.com/repos/samchrisinger/osf.io/contributors","subscribers_url":"https://api.github.com/repos/samchrisinger/osf.io/subscribers","subscription_url":"https://api.github.com/repos/samchrisinger/osf.io/subscription","commits_url":"https://api.github.com/repos/samchrisinger/osf.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/samchrisinger/osf.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/samchrisinger/osf.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/samchrisinger/osf.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/samchrisinger/osf.io/contents/{+path}","compare_url":"https://api.github.com/repos/samchrisinger/osf.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/samchrisinger/osf.io/merges","archive_url":"https://api.github.com/repos/samchrisinger/osf.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/samchrisinger/osf.io/downloads","issues_url":"https://api.github.com/repos/samchrisinger/osf.io/issues{/number}","pulls_url":"https://api.github.com/repos/samchrisinger/osf.io/pulls{/number}","milestones_url":"https://api.github.com/repos/samchrisinger/osf.io/milestones{/number}","notifications_url":"https://api.github.com/repos/samchrisinger/osf.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/samchrisinger/osf.io/labels{/name}","releases_url":"https://api.github.com/repos/samchrisinger/osf.io/releases{/id}","created_at":"2015-02-13T19:40:14Z","updated_at":"2015-02-27T18:03:37Z","pushed_at":"2015-09-09T13:22:13Z","git_url":"git://github.com/samchrisinger/osf.io.git","ssh_url":"git@github.com:samchrisinger/osf.io.git","clone_url":"https://github.com/samchrisinger/osf.io.git","svn_url":"https://github.com/samchrisinger/osf.io","homepage":"http://osf.io","size":106597,"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":3,"forks":0,"open_issues":3,"watchers":0,"default_branch":"develop"}},"base":{"label":"CenterForOpenScience:develop","ref":"develop","sha":"68c04fb05c1c8b5415ad4d61f35ff6b9bdf9d6c8","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-08T23:20:40Z","pushed_at":"2015-09-09T14:48:48Z","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":228867,"stargazers_count":134,"watchers_count":134,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":115,"mirror_url":null,"open_issues_count":416,"forks":115,"open_issues":416,"watchers":134,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/4235"},"issue":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4235"},"comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/4235/comments"},"review_comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/4235/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/4235/commits"},"statuses":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/cfb3783745970c4847c2891b8581554ba5a064d1"}}}}