{"action":"created","comment":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/38352729","id":38352729,"diff_hunk":"@@ -0,0 +1,433 @@\n+/*\n+ *  Knockout views for the Developer application pages: List and Create/Edit pages.\n+ */\n+\n+\n+'use strict';\n+\n+var $ = require('jquery');\n+var bootbox = require('bootbox');\n+var historyjs = require('exports?History!history');\n+\n+var ko = require('knockout');\n+require('knockout.validation');\n+var Raven = require('raven-js');\n+\n+var koHelpers = require('./koHelpers');  // URL validators etc\n+var $osf = require('./osfHelpers');\n+var oop = require('js/oop');\n+var language = require('js/osfLanguage');\n+\n+/*\n+ *  Store the data related to a single API application\n+ */\n+var ApplicationData = oop.defclass({\n+    constructor: function (data) {  // Read in API data and store as object\n+        data = data || {};\n+\n+        // User-editable fields\n+        this.name = ko.observable(data.name)\n+            .extend({required: true});\n+        this.description = ko.observable(data.description);\n+        this.homeUrl = ko.observable(data.home_url)\n+            .extend({\n+                url: true,\n+                ensureHttp: true,\n+                required: true\n+            });\n+        this.callbackUrl = ko.observable(data.callback_url)\n+            .extend({\n+                url: true,\n+                ensureHttp: true,\n+                required: true\n+            });\n+\n+        // Other fields. Owner and client ID should never change within this view.\n+        this.owner = data.owner;\n+        this.clientId = data.client_id;\n+        this.clientSecret = ko.observable(data.client_secret);\n+        this.webDetailUrl = data.links ? data.links.html : undefined;\n+        this.apiDetailUrl = data.links ? data.links.self : undefined;\n+\n+        // Enable value validation in form\n+        this.validated =  ko.validatedObservable(this);\n+\n+        this.isValid = ko.computed(function () {\n+            return this.validated.isValid();\n+        }.bind(this));\n+    },\n+\n+    serialize: function () {\n+        return { // Convert data to JSON-serializable format consistent with API\n+            name: this.name(),\n+            description: this.description(),\n+            home_url: this.homeUrl(),\n+            callback_url: this.callbackUrl(),\n+            client_id: this.clientId,\n+            client_secret: this.clientSecret(),\n+            owner: this.owner\n+        };\n+    }\n+});\n+\n+/*\n+ * Fetch data about applications\n+ */\n+var ApplicationDataClient = oop.defclass({\n+    /*\n+     * Create the client for server operations on ApplicationData objects.\n+     * @param {Object} apiListUrl: The api URL for application listing/creation\n+     */\n+    constructor: function (apiListUrl) {\n+        this.apiListUrl = apiListUrl;\n+    },\n+    _fetchData: function (url) {\n+        var ret = $.Deferred();\n+        var request = $osf.ajaxJSON('GET', url, {isCors: true});\n+\n+        request.done(function (data) {\n+            ret.resolve(this.unserialize(data));\n+        }.bind(this));\n+\n+        request.fail(function (xhr, status, error) {\n+            ret.reject(xhr, status, error);\n+        }.bind(this));\n+\n+        return ret.promise();\n+    },\n+    fetchList: function () {\n+        return this._fetchData(this.apiListUrl);\n+    },\n+    fetchOne: function (url) {\n+        return this._fetchData(url);\n+    },\n+    _sendData: function (appData, url, method) {\n+        var ret = $.Deferred();\n+\n+        var payload = appData.serialize();\n+        var request = $osf.ajaxJSON(method, url, {isCors: true, data: payload});\n+\n+        request.done(function (data) { // The server response will contain the newly created/updated record\n+            ret.resolve(this.unserialize(data));\n+        }.bind(this));\n+        request.fail(function (xhr, status, error) {\n+            ret.reject(xhr, status, error);\n+        });\n+        return ret.promise();\n+    },\n+    createOne: function (appData) {\n+        var url = this.apiListUrl;\n+        return this._sendData(appData, url, 'POST');\n+    },\n+    updateOne: function (appData) {\n+        var url = appData.apiDetailUrl;\n+        return this._sendData(appData, url, 'PATCH');\n+    },\n+    deleteOne: function (appData) {\n+        var url = appData.apiDetailUrl;\n+        return $osf.ajaxJSON('DELETE', url, {isCors: true});\n+    },\n+    unserialize: function (apiData) {\n+        var result;\n+        // Check return type: return one object (detail view) or list of objects (list view) as appropriate.\n+        if (Array.isArray(apiData.data)) {\n+            result = $.map(apiData.data, function (item) {\n+                return new ApplicationData(item);\n+            });\n+        } else if (apiData.data) {\n+            result = new ApplicationData(apiData.data);\n+        } else {\n+            result = null;\n+        }\n+        return result;\n+    }\n+});\n+\n+/*\n+  ViewModel for List views\n+ */\n+var ApplicationsListViewModel = oop.defclass({\n+    constructor: function (urls) {\n+        this.apiListUrl = urls.apiListUrl;\n+        this.webCreateUrl = urls.webCreateUrl;\n+        // Set up data storage\n+        this.appData = ko.observableArray();\n+        this.sortedByName = ko.pureComputed(function () {\n+            return this.appData().sort(function (a,b) {\n+                var an = a.name().toLowerCase();\n+                var bn = b.name().toLowerCase();\n+                return an === bn ? 0 : (an < bn ? -1 : 1);\n+            });\n+        }.bind(this));\n+\n+        // Set up data access client\n+        this.client = new ApplicationDataClient(this.apiListUrl);\n+    },\n+    init: function () {\n+        var request = this.client.fetchList();\n+        request.done(function (data) {\n+            this.appData(data);\n+        }.bind(this));\n+\n+        request.fail(function(xhr, status, error) {\n+            $osf.growl('Error',\n+                language.apiOauth2Application.dataFetchError,\n+                'danger');\n+\n+            Raven.captureMessage('Error fetching list of registered applications', {\n+                url: this.apiListUrl,\n+                status: status,\n+                error: error\n+            });\n+        }.bind(this));\n+    },\n+    deleteApplication: function (appData) {\n+        bootbox.confirm({\n+            title: 'Deactivate application?',\n+            message: language.apiOauth2Application.deactivateConfirm,\n+            callback: function (confirmed) {\n+                if (confirmed) {\n+                    var request = this.client.deleteOne(appData);\n+                    request.done(function () {\n+                            this.appData.destroy(appData);\n+                            var appName = $osf.htmlEscape(appData.name());\n+                            $osf.growl('Deletion', '\"' + appName + '\" has been deactivated', 'success');\n+                    }.bind(this));\n+                    request.fail(function () {\n+                            $osf.growl('Error',\n+                                       language.apiOauth2Application.deactivateError,\n+                                       'danger');\n+                    }.bind(this));\n+                }\n+            }.bind(this),\n+            buttons:{\n+                confirm:{\n+                    label:'Deactivate',\n+                    className:'btn-danger'\n+                }\n+            }\n+        });\n+    }\n+});\n+\n+\n+/*\n+  ViewModel for Detail views (create and update pages- related though distinct behaviors in a single ViewModel)\n+    Expects a urls object containing webListUrl, apiListUrl, and apiDetailUrl values. If apiDetailUrl is blank, it\n+    behaves like a create view.\n+ */\n+var ApplicationDetailViewModel = oop.defclass({\n+    constructor: function (urls) {\n+        var placeholder = new ApplicationData();\n+        this.appData = ko.observable(placeholder);\n+\n+        // Track whether data has changed, and whether user is allowed to leave page anyway\n+        this.originalValues = ko.observable(placeholder.serialize());\n+        this.dirty = ko.computed(function(){\n+            return JSON.stringify(this.originalValues()) !== JSON.stringify(this.appData().serialize());\n+        }.bind(this));\n+        this.allowExit = ko.observable(false);\n+\n+        // Set up data access client\n+        this.webListUrl = urls.webListUrl;\n+        this.client = new ApplicationDataClient(urls.apiListUrl);\n+\n+        // Control success/failure messages above submit buttons\n+        this.message = ko.observable();\n+        this.messageClass = ko.observable();\n+\n+        // Toggle hiding client secret (in detail view)\n+        this.showSecret = ko.observable(false);\n+        // Toggle display of validation messages\n+        this.showMessages = ko.observable(false);\n+\n+        // // If no detail url provided, render view as though it was a creation form. Otherwise, treat as READ/UPDATE.\n+        this.apiDetailUrl = ko.observable(urls.apiDetailUrl);\n+        this.isCreateView = ko.computed(function () {\n+            return !this.apiDetailUrl();\n+        }.bind(this));\n+    },\n+    init: function () {\n+        if (!this.isCreateView()) {\n+            // Prevent user from leaving page if there are unsaved changes\n+            $(window).on('beforeunload', function () {\n+                if (this.dirty() && !this.allowExit()) {\n+                    return 'There are unsaved changes on this page.';\n+                }\n+            }.bind(this));\n+\n+            var request = this.client.fetchOne(this.apiDetailUrl());\n+            request.done(function (dataObj) {\n+                this.appData(dataObj);\n+                this.originalValues(dataObj.serialize());\n+            }.bind(this));\n+            request.fail(function(xhr, status, error) {\n+                $osf.growl('Error',\n+                             language.apiOauth2Application.dataFetchError,\n+                            'danger');\n+\n+                Raven.captureMessage('Error fetching application data', {\n+                    url: this.apiDetailUrl(),\n+                    status: status,\n+                    error: error\n+                });\n+            }.bind(this));\n+        }\n+    },\n+    updateApplication: function () {\n+        var request = this.client.updateOne(this.appData());\n+        request.done(function (dataObj) {\n+            this.appData(dataObj);\n+            this.originalValues(dataObj.serialize());\n+            this.changeMessage(\n+            'Application data updated',\n+            'text-success',\n+            5000);\n+        }.bind(this));\n+\n+        request.fail(function (xhr, status, error) {\n+            $osf.growl('Error',\n+                       language.apiOauth2Application.dataSendError,\n+                       'danger');\n+\n+            Raven.captureMessage('Error updating instance', {\n+                url: this.apiDetailUrl,\n+                status: status,\n+                error: error\n+            });\n+        }.bind(this));\n+        return request;\n+    },\n+    createApplication: function () {\n+        var request = this.client.createOne(this.appData());\n+        request.done(function (dataObj) {\n+            this.appData(dataObj);\n+            this.originalValues(dataObj.serialize());\n+\n+            this.changeMessage(language.apiOauth2Application.creationSuccess, 'text-success', 5000);\n+            this.apiDetailUrl(dataObj.apiDetailUrl); // Toggle ViewModel --> act like a display view now.\n+            historyjs.replaceState({}, '', dataObj.webDetailUrl);  // Update address bar to show new detail page\n+        }.bind(this));\n+\n+        request.fail(function (xhr, status, error) {\n+            $osf.growl('Error',\n+                       language.apiOauth2Application.dataSendError,\n+                       'danger');\n+\n+            Raven.captureMessage('Error registering new OAuth2 application', {\n+                url: this.apiDetailUrl,\n+                status: status,\n+                error: error\n+            });\n+        }.bind(this));\n+    },\n+    submit: function () {\n+        // Validate and dispatch form to correct handler based on view type\n+        if (!this.appData().isValid()) {\n+            // Turn on display of validation messages\n+            this.showMessages(true);\n+        } else {\n+            this.showMessages(false);\n+            if (this.isCreateView()) {\n+                this.createApplication();\n+            } else {\n+                this.updateApplication();\n+            }\n+        }\n+    },\n+    deleteApplication: function () {\n+        var appData = this.appData();\n+        bootbox.confirm({\n+            title: 'Deactivate application?',\n+            message: language.apiOauth2Application.deactivateConfirm,\n+            callback: function (confirmed) {\n+                if (confirmed) {\n+                    var request = this.client.deleteOne(appData );\n+                    request.done(function () {\n+                        this.allowExit(true);\n+                        // Don't let user go back to a deleted application page\n+                        historyjs.replaceState({}, '', this.webListUrl);\n+                        this.visitList();\n+                    }.bind(this));\n+                    request.fail(function () {\n+                            $osf.growl('Error',\n+                                       language.apiOauth2Application.deactivateError,\n+                                       'danger');\n+                    }.bind(this));\n+                }\n+            }.bind(this),\n+            buttons:{\n+                confirm:{\n+                    label:'Deactivate',\n+                    className:'btn-danger'\n+                }\n+            }\n+        });\n+    },\n+    visitList: function () {\n+        window.location = this.webListUrl;\n+    },\n+    cancelChange: function () {\n+        if (!this.dirty()) {\n+            this.visitList();\n+        } else {\n+            bootbox.confirm({","path":"website/static/js/apiApplication.js","position":374,"original_position":374,"commit_id":"2dfa106360a9f51ad25978a8dfcff9dfdb34b1a8","original_commit_id":"2dfa106360a9f51ad25978a8dfcff9dfdb34b1a8","user":{"login":"sloria","id":2379650,"avatar_url":"https://avatars.githubusercontent.com/u/2379650?v=3","gravatar_id":"","url":"https://api.github.com/users/sloria","html_url":"https://github.com/sloria","followers_url":"https://api.github.com/users/sloria/followers","following_url":"https://api.github.com/users/sloria/following{/other_user}","gists_url":"https://api.github.com/users/sloria/gists{/gist_id}","starred_url":"https://api.github.com/users/sloria/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sloria/subscriptions","organizations_url":"https://api.github.com/users/sloria/orgs","repos_url":"https://api.github.com/users/sloria/repos","events_url":"https://api.github.com/users/sloria/events{/privacy}","received_events_url":"https://api.github.com/users/sloria/received_events","type":"User","site_admin":false},"body":"There seems to be (justifiable) ambiguity in the guidelines here. In this context, I think the modal should be removed.\r\n\r\nHowever, @abought rightfully points that the other profile pages use the confirmation modals for \"Discard\"/\"Cancel\". If we were to remove them here, they should be removed on those pages as well. Though it's a small change, it may be outside the scope of this PR.\r\n\r\nPinging @erinmayhood on how to proceed.","created_at":"2015-08-31T20:01:59Z","updated_at":"2015-08-31T20:01:59Z","html_url":"https://github.com/CenterForOpenScience/osf.io/pull/3082#discussion_r38352729","pull_request_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082","_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/comments/38352729"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/3082#discussion_r38352729"},"pull_request":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082"}}},"pull_request":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082","id":37495384,"html_url":"https://github.com/CenterForOpenScience/osf.io/pull/3082","diff_url":"https://github.com/CenterForOpenScience/osf.io/pull/3082.diff","patch_url":"https://github.com/CenterForOpenScience/osf.io/pull/3082.patch","issue_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/3082","number":3082,"state":"open","locked":false,"title":"[feature][Ready for review] UI and models to register third-party OAuth2 clients","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":"Moved from brianjgeiger/osf.io#86. Work in progress; opened for tracking purposes.\r\n\r\nPartially implements requirements of #3323.\r\n\r\n## Purpose\r\n1. Provide models and UI to allowing users to register new OAuth2 applications\r\n2. Demonstrate the first front-end views built on the new API. This entails adding support for cross-origin requests to the API and javascript calls.\r\n\r\n## Summary of changes\r\n1. Add a new database model (OAuth2App) to track information about user-registered OAuth2 applications.\r\n2. Add new APIv2 endpoints that allow users to request and change information about their API applications (default intended restriction: only registered owner can make queries about an application)\r\n    - `/v2/users/<user_id>/applications/` and `/v2/users/<user_id>/<client_id>/`\r\n3. Add new UI at `/settings/applications` and `/settings/applications/<client_id>` allowing users to see list of currently registered applications, view detailed information, or create new.\r\n4. Add (and configure) the django-cors-headers application, used by DRF to support OSF calls to the new API endpoints. (particularly for dev environment, where calls are explicitly cross-origin; may be of use later as more OSF-internal services need to coordinate with the API)\r\n\r\n## To be done\r\n- [x] UI / knockout code cleanup\r\n- [x] Add styling to display of client id/ client secret on detail page\r\n- [x] Verify that registered applications integrate with CAS server as expected\r\n- [x] Add unit tests for JS/ viewmodel (and clean up stubs)\r\n- [x] Write documentation for developers (how to connect using oauth) and find a place to host/ serve that documentation (see #4078)\r\n\r\nPossibly post-MVP: \r\n- Add ability for user to revoke applications\r\n- Revisit suppressing routes from swagger documentation\r\n- Allow resetting of client secret via UI. May also invalidate tokens as part of this reset.\r\n- Refactor Links field (using new `LinksFieldWithSelfLink` classes that will land in https://github.com/CenterForOpenScience/osf.io/pull/3683 )\r\n- Restrict views that are accessible via the API (see #4057)\r\n\r\n## References\r\n- Issue brianjgeiger/osf.io#30\r\n- Issue #3041 \r\n\r\n## Testing notes\r\nLocal testing of this feature requires realCAS (for deleting an application/deauthorizing tokens) and apiV2 (for operations on application data).\r\n\r\nAdditionally, settings page views are currently only activated in dev mode, controlled via the `dev_only` decorator.","created_at":"2015-06-11T18:44:06Z","updated_at":"2015-08-31T20:01:59Z","closed_at":null,"merged_at":null,"merge_commit_sha":"407651ad5c78894c466e21e896c0890d69ed1151","assignee":{"login":"sloria","id":2379650,"avatar_url":"https://avatars.githubusercontent.com/u/2379650?v=3","gravatar_id":"","url":"https://api.github.com/users/sloria","html_url":"https://github.com/sloria","followers_url":"https://api.github.com/users/sloria/followers","following_url":"https://api.github.com/users/sloria/following{/other_user}","gists_url":"https://api.github.com/users/sloria/gists{/gist_id}","starred_url":"https://api.github.com/users/sloria/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sloria/subscriptions","organizations_url":"https://api.github.com/users/sloria/orgs","repos_url":"https://api.github.com/users/sloria/repos","events_url":"https://api.github.com/users/sloria/events{/privacy}","received_events_url":"https://api.github.com/users/sloria/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/CenterForOpenScience/osf.io/milestones/47","html_url":"https://github.com/CenterForOpenScience/osf.io/milestones/0.48.0","labels_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/milestones/47/labels","id":1266839,"number":47,"title":"0.48.0","description":"","creator":{"login":"sloria","id":2379650,"avatar_url":"https://avatars.githubusercontent.com/u/2379650?v=3","gravatar_id":"","url":"https://api.github.com/users/sloria","html_url":"https://github.com/sloria","followers_url":"https://api.github.com/users/sloria/followers","following_url":"https://api.github.com/users/sloria/following{/other_user}","gists_url":"https://api.github.com/users/sloria/gists{/gist_id}","starred_url":"https://api.github.com/users/sloria/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sloria/subscriptions","organizations_url":"https://api.github.com/users/sloria/orgs","repos_url":"https://api.github.com/users/sloria/repos","events_url":"https://api.github.com/users/sloria/events{/privacy}","received_events_url":"https://api.github.com/users/sloria/received_events","type":"User","site_admin":false},"open_issues":9,"closed_issues":17,"state":"open","created_at":"2015-08-21T19:24:08Z","updated_at":"2015-08-31T19:12:17Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082/commits","review_comments_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082/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/3082/comments","statuses_url":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/2dfa106360a9f51ad25978a8dfcff9dfdb34b1a8","head":{"label":"abought:feature/oauth-app-reg-cors","ref":"feature/oauth-app-reg-cors","sha":"2dfa106360a9f51ad25978a8dfcff9dfdb34b1a8","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},"repo":{"id":31237649,"name":"osf.io","full_name":"abought/osf.io","owner":{"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},"private":false,"html_url":"https://github.com/abought/osf.io","description":"Facilitating Open Science","fork":true,"url":"https://api.github.com/repos/abought/osf.io","forks_url":"https://api.github.com/repos/abought/osf.io/forks","keys_url":"https://api.github.com/repos/abought/osf.io/keys{/key_id}","collaborators_url":"https://api.github.com/repos/abought/osf.io/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/abought/osf.io/teams","hooks_url":"https://api.github.com/repos/abought/osf.io/hooks","issue_events_url":"https://api.github.com/repos/abought/osf.io/issues/events{/number}","events_url":"https://api.github.com/repos/abought/osf.io/events","assignees_url":"https://api.github.com/repos/abought/osf.io/assignees{/user}","branches_url":"https://api.github.com/repos/abought/osf.io/branches{/branch}","tags_url":"https://api.github.com/repos/abought/osf.io/tags","blobs_url":"https://api.github.com/repos/abought/osf.io/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/abought/osf.io/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/abought/osf.io/git/refs{/sha}","trees_url":"https://api.github.com/repos/abought/osf.io/git/trees{/sha}","statuses_url":"https://api.github.com/repos/abought/osf.io/statuses/{sha}","languages_url":"https://api.github.com/repos/abought/osf.io/languages","stargazers_url":"https://api.github.com/repos/abought/osf.io/stargazers","contributors_url":"https://api.github.com/repos/abought/osf.io/contributors","subscribers_url":"https://api.github.com/repos/abought/osf.io/subscribers","subscription_url":"https://api.github.com/repos/abought/osf.io/subscription","commits_url":"https://api.github.com/repos/abought/osf.io/commits{/sha}","git_commits_url":"https://api.github.com/repos/abought/osf.io/git/commits{/sha}","comments_url":"https://api.github.com/repos/abought/osf.io/comments{/number}","issue_comment_url":"https://api.github.com/repos/abought/osf.io/issues/comments{/number}","contents_url":"https://api.github.com/repos/abought/osf.io/contents/{+path}","compare_url":"https://api.github.com/repos/abought/osf.io/compare/{base}...{head}","merges_url":"https://api.github.com/repos/abought/osf.io/merges","archive_url":"https://api.github.com/repos/abought/osf.io/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/abought/osf.io/downloads","issues_url":"https://api.github.com/repos/abought/osf.io/issues{/number}","pulls_url":"https://api.github.com/repos/abought/osf.io/pulls{/number}","milestones_url":"https://api.github.com/repos/abought/osf.io/milestones{/number}","notifications_url":"https://api.github.com/repos/abought/osf.io/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/abought/osf.io/labels{/name}","releases_url":"https://api.github.com/repos/abought/osf.io/releases{/id}","created_at":"2015-02-24T00:27:13Z","updated_at":"2015-05-06T13:18:01Z","pushed_at":"2015-08-31T14:38:41Z","git_url":"git://github.com/abought/osf.io.git","ssh_url":"git@github.com:abought/osf.io.git","clone_url":"https://github.com/abought/osf.io.git","svn_url":"https://github.com/abought/osf.io","homepage":"http://osf.io","size":99206,"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":"18b8b12daef3798c5d2ab723e50bc0e6fcd64f3a","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-08-31T19:07:58Z","pushed_at":"2015-08-31T20:01:55Z","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":228386,"stargazers_count":130,"watchers_count":130,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":113,"mirror_url":null,"open_issues_count":425,"forks":113,"open_issues":425,"watchers":130,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082"},"html":{"href":"https://github.com/CenterForOpenScience/osf.io/pull/3082"},"issue":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/3082"},"comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/issues/3082/comments"},"review_comments":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/pulls/3082/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/3082/commits"},"statuses":{"href":"https://api.github.com/repos/CenterForOpenScience/osf.io/statuses/2dfa106360a9f51ad25978a8dfcff9dfdb34b1a8"}}}}