{"action":"created","comment":{"url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/comments/30398616","id":30398616,"diff_hunk":"@@ -0,0 +1,806 @@\n+/**\n+ * Copyright (c) 2014 \"Fronteer LTD\"\n+ * Grasshopper Event Engine\n+ *\n+ * This program is free software: you can redistribute it and/or modify\n+ * it under the terms of the GNU Affero General Public License as\n+ * published by the Free Software Foundation, either version 3 of the\n+ * License, or (at your option) any later version.\n+ *\n+ * This program is distributed in the hope that it will be useful,\n+ * but WITHOUT ANY WARRANTY; without even the implied warranty of\n+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+ * GNU Affero General Public License for more details.\n+ *\n+ * You should have received a copy of the GNU Affero General Public License\n+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n+ */\n+\n+var _ = require('lodash');\n+var assert = require('assert');\n+var util = require('util');\n+\n+var AuthTestsUtil = require('gh-auth/tests/util');\n+var ConfigTestsUtil = require('gh-config/tests/util');\n+var EventsTestsUtil = require('gh-events/tests/util');\n+var GroupsTestsUtil = require('gh-groups/tests/util');\n+var SeriesTestsUtil = require('gh-series/tests/util');\n+var TestsUtil = require('gh-tests');\n+var UsersTestsUtil = require('gh-users/tests/util');\n+\n+var OrgUnitDAO = require('gh-orgunit/lib/internal/dao');\n+var OrgUnitImport = require('gh-orgunit/lib/internal/import');\n+var OrgUnitTestsUtil = require('./util');\n+\n+describe('Organisational units - import', function() {\n+\n+    /**\n+     * Returns a test tree from the `./data` directory\n+     *\n+     * @param  {String}     name    The name of the file where the tree is stored\n+     * @return {Object}             The test tree\n+     * @api private\n+     */\n+    var _getTestTree = function(name) {\n+        var tree = require(util.format('./data/%s', name));\n+        return _.cloneDeep(tree);\n+    };\n+\n+    /**\n+     * Recursively search through a tree and look for an organisational unit, series or event\n+     * with a given external id. When the node with the given external id is found, it will be\n+     * returned in its entirety (i.e., including it's child nodes)\n+     *\n+     * @param  {Object}     tree            The tree to search through\n+     * @param  {String}     externalId      The external id to search for\n+     * @return {Object}                     The matching node or null if no tree could be found\n+     * @api private\n+     */\n+    var _getNode = function(tree, externalId) {\n+        if (tree.externalId === externalId) {\n+            return tree;\n+        }\n+\n+        // Search through the children (including series and events)\n+        var allThings = _.union(tree.children, tree.series, tree.events);\n+        for (var i = 0; i < allThings.length; i++) {\n+            var foundThing = _getNode(allThings[i], externalId);\n+            if (foundThing) {\n+                return foundThing;\n+            }\n+        }\n+\n+        return null;\n+    };\n+\n+    /**\n+     * Generate a node structure\n+     *\n+     * @param  {String}         type            The type of the root node\n+     * @param  {...String[]}    childTypes      The types of the children of the root node\n+     * @return {Object}                         A node structure that can be imported\n+     * @api private\n+     */\n+    var _generateStructure = function(type, childTypes) {\n+        // Construct the root node\n+        var root = {\n+            'externalId': util.format('eid-%d', _.random(100000)),\n+            'type': type,\n+            'displayName': TestsUtil.generateString(32),\n+            'description': TestsUtil.generateString(128),\n+            'children': [],\n+            'series': []\n+        };\n+\n+        // Recursively construct the children, if any\n+        if (!_.isEmpty(childTypes)) {\n+            // Get the remainder of the arguments in case we need to generate grand-children\n+            var args = _.values(arguments);\n+            args.splice(0, 2);\n+\n+            // If an argument was provided as a simple string, we only need to generate 1 child.\n+            // If it was provided as an array, we generate a child per string in the array\n+            if (!_.isArray(childTypes)) {\n+                childTypes = [childTypes];\n+            }\n+            _.each(childTypes, function(childType) {\n+                // Shift the child type in the arguments array as the first element and recursively\n+                // generate its tree structure\n+                args.unshift(childType);\n+                root.children.push(_generateStructure.apply(null, args));\n+            });\n+        }\n+\n+        return root;\n+    };\n+\n+    describe('Validation', function() {\n+\n+        /**\n+         * Test that verifies basic validation when importing data\n+         */\n+        it('verify validation when importing data', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part', 'part', {}, function(part) {\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+\n+                // Invalid or missing id\n+                OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.cam2013.client, 'Not a number', importData, false, 400, function() {\n+                    OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.cam2013.client, -1, importData, false, 404, function() {\n+                        OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.cam2013.client, 23424234234, importData, false, 404, function() {\n+\n+                            // Invalid data\n+                            OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.cam2013.client, part.id, 'oh lawdy', false, 400, function() {\n+                                OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.cam2013.client, part.id, 123, false, 400, function() {\n+\n+                                    return callback();\n+                                });\n+                            });\n+                        });\n+                    });\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies authorization when importing data\n+         */\n+        it('verify authorization when importing data', function(callback) {\n+            // Create an organisational unit\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test org unit', 'part', null, function(part) {\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+\n+                // Anonymous users cannot import organisational units\n+                TestsUtil.getAnonymousAppUserClient(global.tests.apps.cam2013, function(anonymousClient) {\n+                    OrgUnitTestsUtil.assertImportOrgUnitFails(anonymousClient, part.id, importData, false, 401, function() {\n+                        TestsUtil.getAnonymousGlobalAdminRestClient(function(anonymousClient) {\n+                            OrgUnitTestsUtil.assertImportOrgUnitFails(anonymousClient, part.id, importData, false, 401, function() {\n+\n+                                // Authenticated users cannot update organisational units\n+                                TestsUtil.generateTestUsers(global.tests.apps.cam2013, 1, false, function(simon) {\n+                                    OrgUnitTestsUtil.assertImportOrgUnitFails(simon.client, part.id, importData, false, 401, function() {\n+\n+                                        // Application administrators from another application cannot update the organisational unit\n+                                        OrgUnitTestsUtil.assertImportOrgUnitFails(global.tests.admins.oxford2014.client, part.id, importData, false, 401, function() {\n+\n+                                            // Members of the organisational unit's group can import data\n+                                            var memberUpdate = {};\n+                                            memberUpdate[simon.profile.id] = true;\n+                                            GroupsTestsUtil.assertUpdateGroupMembers(global.tests.admins.cam2013.client, part.GroupId, memberUpdate, function() {\n+                                                OrgUnitTestsUtil.assertImportOrgUnit(simon.client, part.id, importData, false, function() {\n+\n+                                                    // Application administrators can import data\n+                                                    OrgUnitTestsUtil.assertImportOrgUnit(global.tests.admins.cam2013.client, part.id, importData, false, function() {\n+                                                        return callback();\n+                                                    });\n+                                                });\n+                                            });\n+                                        });\n+                                    });\n+                                });\n+                            });\n+                        });\n+                    });\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that external ids have to be unique\n+         */\n+        it('verify external ids have to be unique within the tree', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part', 'part', {}, function(part) {\n+\n+                // Trying to import 2 organisatioanl units with the same external id should fail\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+                importData.children[1].externalId = importData.children[0].externalId;\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Duplicate externalId'], function() {\n+\n+                    // Trying to import an organisational unit with the same external id as a series should fail\n+                    importData = _getTestTree('1part-1module-1series-1event');\n+                    importData.children[0].externalId = importData.children[0].series[0].externalId;\n+                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Duplicate externalId'], function() {\n+\n+                        // Trying to import an organisational unit with the same external id as an event should fail\n+                        importData = _getTestTree('1part-1module-1series-1event');\n+                        importData.children[0].externalId = importData.children[0].series[0].events[0].externalId;\n+                        OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Duplicate externalId'], function() {\n+\n+                            // Trying to import a series with the same external id as an event should fail\n+                            importData = _getTestTree('1part-1module-1series-1event');\n+                            importData.children[0].series[0].externalId = importData.children[0].series[0].events[0].externalId;\n+                            OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Duplicate externalId'], function() {\n+\n+                                // Import the structure into the part\n+                                importData = _getTestTree('1part-2module-1series-1event');\n+                                OrgUnitTestsUtil.assertImportOrgUnit(global.tests.admins.cam2013.client, part.id, importData, false, function() {\n+\n+                                    // Verify we can import the SAME structure into ANOTHER part. This is necessary\n+                                    // as multiple consumers can import data, but they don't necessarily know about\n+                                    // each other's external ids.\n+                                    OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part 2', 'part', {}, function(part2) {\n+                                        OrgUnitTestsUtil.assertImportOrgUnit(global.tests.admins.cam2013.client, part2.id, importData, false, function() {\n+\n+                                            // Sanity-check the structure was imported into both organisational units\n+                                            OrgUnitTestsUtil.assertExportOrgUnit(global.tests.admins.cam2013.client, part.id, null, function(exportPart1) {\n+                                                OrgUnitTestsUtil.assertExportOrgUnit(global.tests.admins.cam2013.client, part2.id, null, function(exportPart2) {\n+                                                    assert.ok(exportPart1.children[0].series[0].events[0]);\n+                                                    assert.ok(exportPart2.children[0].series[0].events[0]);\n+                                                    return callback();\n+                                                });\n+                                            });\n+                                        });\n+                                    });\n+                                });\n+                            });\n+                        });\n+                    });\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that organisational unit types have to be on the same level\n+         */\n+        it('verify organisational unit types have to be on the same level', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part', 'part', {}, function(part) {\n+\n+                // Importing a tree where the parent has the same type as the child is not allowed\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+                importData.type = 'module';\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['was used on a different level'], function() {\n+\n+                    // Importing a tree where the child has the same type as the parent is not allowed\n+                    importData = _getTestTree('1part-2module-1series-1event');\n+                    importData.children[0].type = 'part';\n+                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['was used on a different level'], function() {\n+                        return callback();\n+                    });\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that importing only works on course, subject or part organisational units\n+         */\n+        it('verify importing only works on courses, subjects or parts', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test module', 'module', {}, function(part) {\n+\n+                // Try to import just a module\n+                var tree = _getTestTree('1part-1module-1series-1event');\n+                var importData = tree.children[0];\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['only support importing data on courses, subjects or parts'], function() {\n+                    return callback();\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that only timetable types can be imported\n+         */\n+        it('verify only timetable types can be imported', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part', 'part', {}, function(part) {\n+\n+                // Importing a tree where the parent has the same type as the child is not allowed\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+                importData.type = 'wrong-type';\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['A valid type must be one of'], function() {\n+                    return callback();\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that only valid timetable structures can be imported\n+         */\n+        it('verify only valid timetable structures can be imported', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test course', 'course', {}, function(part) {\n+\n+                // Assert nesting organisational units under a course\n+                var importData = _generateStructure('course', 'module');\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a module under a course is invalid'], function() {\n+                    importData = _generateStructure('course', 'course');\n+                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a course under a course is invalid'], function() {\n+\n+                        // Assert nesting organisational units under a subject\n+                        importData = _generateStructure('course', 'subject', 'module');\n+                        OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a module under a subject is invalid'], function() {\n+                            importData = _generateStructure('course', 'subject', 'course');\n+                            OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a course under a subject is invalid'], function() {\n+                                importData = _generateStructure('course', 'subject', 'subject');\n+                                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a subject under a subject is invalid'], function() {\n+\n+                                    // Assert nesting organisational units under a part\n+                                    importData = _generateStructure('course', 'part', 'subject');\n+                                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a subject under a part is invalid'], function() {\n+                                        importData = _generateStructure('course', 'part', 'course');\n+                                        OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a course under a part is invalid'], function() {\n+                                            importData = _generateStructure('course', 'part', 'part');\n+                                            OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a part under a part is invalid'], function() {\n+\n+                                                // Assert nesting organisational units under a module\n+                                                importData = _generateStructure('course', 'part', 'module', 'part');\n+                                                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a part under a module is invalid'], function() {\n+                                                    importData = _generateStructure('course', 'part', 'module', 'subject');\n+                                                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a subject under a module is invalid'], function() {\n+                                                        importData = _generateStructure('course', 'part', 'module', 'course');\n+                                                        OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a course under a module is invalid'], function() {\n+                                                            importData = _generateStructure('course', 'part', 'module', 'module');\n+                                                            OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['Nesting a module under a module is invalid'], function() {\n+                                                                return callback();\n+                                                            });\n+                                                        });\n+                                                    });\n+                                                });\n+                                            });\n+                                        });\n+                                    });\n+                                });\n+                            });\n+                        });\n+                    });\n+                });\n+            });\n+        });\n+\n+        /**\n+         * Test that verifies that organisational units get validated\n+         */\n+        it('verify basic organisational unit validation', function(callback) {\n+            OrgUnitTestsUtil.assertCreateOrgUnit(global.tests.admins.cam2013.client, 'Test part', 'part', {}, function(part) {\n+\n+                // Too long displayName\n+                var importData = _getTestTree('1part-2module-1series-1event');\n+                importData.children[0].displayName = TestsUtil.generateString(257);\n+                OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['displayName'], function() {\n+\n+                    // Too long type\n+                    importData = _getTestTree('1part-2module-1series-1event');\n+                    importData.children[0].type = TestsUtil.generateString(33);\n+                    OrgUnitTestsUtil.assertImportOrgUnitImportFails(global.tests.admins.cam2013.client, part.id, importData, false, ['type'], function() {","path":"node_modules/gh-orgunit/tests/test-import.js","position":359,"original_position":359,"commit_id":"9de320968c1c9dcde0540a55d6e4959f9bb332d4","original_commit_id":"9de320968c1c9dcde0540a55d6e4959f9bb332d4","user":{"login":"simong","id":92180,"avatar_url":"https://avatars.githubusercontent.com/u/92180?v=3","gravatar_id":"","url":"https://api.github.com/users/simong","html_url":"https://github.com/simong","followers_url":"https://api.github.com/users/simong/followers","following_url":"https://api.github.com/users/simong/following{/other_user}","gists_url":"https://api.github.com/users/simong/gists{/gist_id}","starred_url":"https://api.github.com/users/simong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/simong/subscriptions","organizations_url":"https://api.github.com/users/simong/orgs","repos_url":"https://api.github.com/users/simong/repos","events_url":"https://api.github.com/users/simong/events{/privacy}","received_events_url":"https://api.github.com/users/simong/received_events","type":"User","site_admin":false},"body":"The import API is scoped to Timetable types. This means only `course`, `subject`, `part` and `module` are valid types. The `verify only timetable types can be imported` test verifies this","created_at":"2015-05-15T10:23:43Z","updated_at":"2015-05-15T10:23:43Z","html_url":"https://github.com/fronteerio/grasshopper/pull/107#discussion_r30398616","pull_request_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107","_links":{"self":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/comments/30398616"},"html":{"href":"https://github.com/fronteerio/grasshopper/pull/107#discussion_r30398616"},"pull_request":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107"}}},"pull_request":{"url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107","id":34606974,"html_url":"https://github.com/fronteerio/grasshopper/pull/107","diff_url":"https://github.com/fronteerio/grasshopper/pull/107.diff","patch_url":"https://github.com/fronteerio/grasshopper/pull/107.patch","issue_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/107","number":107,"state":"open","locked":false,"title":"Import data for an organisational unit","user":{"login":"simong","id":92180,"avatar_url":"https://avatars.githubusercontent.com/u/92180?v=3","gravatar_id":"","url":"https://api.github.com/users/simong","html_url":"https://github.com/simong","followers_url":"https://api.github.com/users/simong/followers","following_url":"https://api.github.com/users/simong/following{/other_user}","gists_url":"https://api.github.com/users/simong/gists{/gist_id}","starred_url":"https://api.github.com/users/simong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/simong/subscriptions","organizations_url":"https://api.github.com/users/simong/orgs","repos_url":"https://api.github.com/users/simong/repos","events_url":"https://api.github.com/users/simong/events{/privacy}","received_events_url":"https://api.github.com/users/simong/received_events","type":"User","site_admin":false},"body":"Builds on the export branch. The idea is that this will be limited to just Timetable applications","created_at":"2015-05-03T20:16:11Z","updated_at":"2015-05-15T10:23:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"1c44163d7e53b13dd50c791664fe5fa2f4ba4770","assignee":{"login":"simong","id":92180,"avatar_url":"https://avatars.githubusercontent.com/u/92180?v=3","gravatar_id":"","url":"https://api.github.com/users/simong","html_url":"https://github.com/simong","followers_url":"https://api.github.com/users/simong/followers","following_url":"https://api.github.com/users/simong/following{/other_user}","gists_url":"https://api.github.com/users/simong/gists{/gist_id}","starred_url":"https://api.github.com/users/simong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/simong/subscriptions","organizations_url":"https://api.github.com/users/simong/orgs","repos_url":"https://api.github.com/users/simong/repos","events_url":"https://api.github.com/users/simong/events{/privacy}","received_events_url":"https://api.github.com/users/simong/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107/commits","review_comments_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107/comments","review_comment_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls/comments{/number}","comments_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/107/comments","statuses_url":"https://api.github.com/repos/fronteerio/grasshopper/statuses/9de320968c1c9dcde0540a55d6e4959f9bb332d4","head":{"label":"fronteerio:import-new","ref":"import-new","sha":"9de320968c1c9dcde0540a55d6e4959f9bb332d4","user":{"login":"fronteerio","id":9116505,"avatar_url":"https://avatars.githubusercontent.com/u/9116505?v=3","gravatar_id":"","url":"https://api.github.com/users/fronteerio","html_url":"https://github.com/fronteerio","followers_url":"https://api.github.com/users/fronteerio/followers","following_url":"https://api.github.com/users/fronteerio/following{/other_user}","gists_url":"https://api.github.com/users/fronteerio/gists{/gist_id}","starred_url":"https://api.github.com/users/fronteerio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fronteerio/subscriptions","organizations_url":"https://api.github.com/users/fronteerio/orgs","repos_url":"https://api.github.com/users/fronteerio/repos","events_url":"https://api.github.com/users/fronteerio/events{/privacy}","received_events_url":"https://api.github.com/users/fronteerio/received_events","type":"Organization","site_admin":false},"repo":{"id":24999124,"name":"grasshopper","full_name":"fronteerio/grasshopper","owner":{"login":"fronteerio","id":9116505,"avatar_url":"https://avatars.githubusercontent.com/u/9116505?v=3","gravatar_id":"","url":"https://api.github.com/users/fronteerio","html_url":"https://github.com/fronteerio","followers_url":"https://api.github.com/users/fronteerio/followers","following_url":"https://api.github.com/users/fronteerio/following{/other_user}","gists_url":"https://api.github.com/users/fronteerio/gists{/gist_id}","starred_url":"https://api.github.com/users/fronteerio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fronteerio/subscriptions","organizations_url":"https://api.github.com/users/fronteerio/orgs","repos_url":"https://api.github.com/users/fronteerio/repos","events_url":"https://api.github.com/users/fronteerio/events{/privacy}","received_events_url":"https://api.github.com/users/fronteerio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/fronteerio/grasshopper","description":"Grasshopper Event Engine","fork":false,"url":"https://api.github.com/repos/fronteerio/grasshopper","forks_url":"https://api.github.com/repos/fronteerio/grasshopper/forks","keys_url":"https://api.github.com/repos/fronteerio/grasshopper/keys{/key_id}","collaborators_url":"https://api.github.com/repos/fronteerio/grasshopper/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/fronteerio/grasshopper/teams","hooks_url":"https://api.github.com/repos/fronteerio/grasshopper/hooks","issue_events_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/events{/number}","events_url":"https://api.github.com/repos/fronteerio/grasshopper/events","assignees_url":"https://api.github.com/repos/fronteerio/grasshopper/assignees{/user}","branches_url":"https://api.github.com/repos/fronteerio/grasshopper/branches{/branch}","tags_url":"https://api.github.com/repos/fronteerio/grasshopper/tags","blobs_url":"https://api.github.com/repos/fronteerio/grasshopper/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/fronteerio/grasshopper/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/fronteerio/grasshopper/git/refs{/sha}","trees_url":"https://api.github.com/repos/fronteerio/grasshopper/git/trees{/sha}","statuses_url":"https://api.github.com/repos/fronteerio/grasshopper/statuses/{sha}","languages_url":"https://api.github.com/repos/fronteerio/grasshopper/languages","stargazers_url":"https://api.github.com/repos/fronteerio/grasshopper/stargazers","contributors_url":"https://api.github.com/repos/fronteerio/grasshopper/contributors","subscribers_url":"https://api.github.com/repos/fronteerio/grasshopper/subscribers","subscription_url":"https://api.github.com/repos/fronteerio/grasshopper/subscription","commits_url":"https://api.github.com/repos/fronteerio/grasshopper/commits{/sha}","git_commits_url":"https://api.github.com/repos/fronteerio/grasshopper/git/commits{/sha}","comments_url":"https://api.github.com/repos/fronteerio/grasshopper/comments{/number}","issue_comment_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/comments{/number}","contents_url":"https://api.github.com/repos/fronteerio/grasshopper/contents/{+path}","compare_url":"https://api.github.com/repos/fronteerio/grasshopper/compare/{base}...{head}","merges_url":"https://api.github.com/repos/fronteerio/grasshopper/merges","archive_url":"https://api.github.com/repos/fronteerio/grasshopper/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/fronteerio/grasshopper/downloads","issues_url":"https://api.github.com/repos/fronteerio/grasshopper/issues{/number}","pulls_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls{/number}","milestones_url":"https://api.github.com/repos/fronteerio/grasshopper/milestones{/number}","notifications_url":"https://api.github.com/repos/fronteerio/grasshopper/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/fronteerio/grasshopper/labels{/name}","releases_url":"https://api.github.com/repos/fronteerio/grasshopper/releases{/id}","created_at":"2014-10-09T16:51:39Z","updated_at":"2015-05-01T14:17:34Z","pushed_at":"2015-05-14T21:51:39Z","git_url":"git://github.com/fronteerio/grasshopper.git","ssh_url":"git@github.com:fronteerio/grasshopper.git","clone_url":"https://github.com/fronteerio/grasshopper.git","svn_url":"https://github.com/fronteerio/grasshopper","homepage":null,"size":5693,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":12,"forks":4,"open_issues":12,"watchers":0,"default_branch":"master"}},"base":{"label":"fronteerio:master","ref":"master","sha":"f70d1a0ab1bffe322d6c1b28424689340cb94be2","user":{"login":"fronteerio","id":9116505,"avatar_url":"https://avatars.githubusercontent.com/u/9116505?v=3","gravatar_id":"","url":"https://api.github.com/users/fronteerio","html_url":"https://github.com/fronteerio","followers_url":"https://api.github.com/users/fronteerio/followers","following_url":"https://api.github.com/users/fronteerio/following{/other_user}","gists_url":"https://api.github.com/users/fronteerio/gists{/gist_id}","starred_url":"https://api.github.com/users/fronteerio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fronteerio/subscriptions","organizations_url":"https://api.github.com/users/fronteerio/orgs","repos_url":"https://api.github.com/users/fronteerio/repos","events_url":"https://api.github.com/users/fronteerio/events{/privacy}","received_events_url":"https://api.github.com/users/fronteerio/received_events","type":"Organization","site_admin":false},"repo":{"id":24999124,"name":"grasshopper","full_name":"fronteerio/grasshopper","owner":{"login":"fronteerio","id":9116505,"avatar_url":"https://avatars.githubusercontent.com/u/9116505?v=3","gravatar_id":"","url":"https://api.github.com/users/fronteerio","html_url":"https://github.com/fronteerio","followers_url":"https://api.github.com/users/fronteerio/followers","following_url":"https://api.github.com/users/fronteerio/following{/other_user}","gists_url":"https://api.github.com/users/fronteerio/gists{/gist_id}","starred_url":"https://api.github.com/users/fronteerio/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fronteerio/subscriptions","organizations_url":"https://api.github.com/users/fronteerio/orgs","repos_url":"https://api.github.com/users/fronteerio/repos","events_url":"https://api.github.com/users/fronteerio/events{/privacy}","received_events_url":"https://api.github.com/users/fronteerio/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/fronteerio/grasshopper","description":"Grasshopper Event Engine","fork":false,"url":"https://api.github.com/repos/fronteerio/grasshopper","forks_url":"https://api.github.com/repos/fronteerio/grasshopper/forks","keys_url":"https://api.github.com/repos/fronteerio/grasshopper/keys{/key_id}","collaborators_url":"https://api.github.com/repos/fronteerio/grasshopper/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/fronteerio/grasshopper/teams","hooks_url":"https://api.github.com/repos/fronteerio/grasshopper/hooks","issue_events_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/events{/number}","events_url":"https://api.github.com/repos/fronteerio/grasshopper/events","assignees_url":"https://api.github.com/repos/fronteerio/grasshopper/assignees{/user}","branches_url":"https://api.github.com/repos/fronteerio/grasshopper/branches{/branch}","tags_url":"https://api.github.com/repos/fronteerio/grasshopper/tags","blobs_url":"https://api.github.com/repos/fronteerio/grasshopper/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/fronteerio/grasshopper/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/fronteerio/grasshopper/git/refs{/sha}","trees_url":"https://api.github.com/repos/fronteerio/grasshopper/git/trees{/sha}","statuses_url":"https://api.github.com/repos/fronteerio/grasshopper/statuses/{sha}","languages_url":"https://api.github.com/repos/fronteerio/grasshopper/languages","stargazers_url":"https://api.github.com/repos/fronteerio/grasshopper/stargazers","contributors_url":"https://api.github.com/repos/fronteerio/grasshopper/contributors","subscribers_url":"https://api.github.com/repos/fronteerio/grasshopper/subscribers","subscription_url":"https://api.github.com/repos/fronteerio/grasshopper/subscription","commits_url":"https://api.github.com/repos/fronteerio/grasshopper/commits{/sha}","git_commits_url":"https://api.github.com/repos/fronteerio/grasshopper/git/commits{/sha}","comments_url":"https://api.github.com/repos/fronteerio/grasshopper/comments{/number}","issue_comment_url":"https://api.github.com/repos/fronteerio/grasshopper/issues/comments{/number}","contents_url":"https://api.github.com/repos/fronteerio/grasshopper/contents/{+path}","compare_url":"https://api.github.com/repos/fronteerio/grasshopper/compare/{base}...{head}","merges_url":"https://api.github.com/repos/fronteerio/grasshopper/merges","archive_url":"https://api.github.com/repos/fronteerio/grasshopper/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/fronteerio/grasshopper/downloads","issues_url":"https://api.github.com/repos/fronteerio/grasshopper/issues{/number}","pulls_url":"https://api.github.com/repos/fronteerio/grasshopper/pulls{/number}","milestones_url":"https://api.github.com/repos/fronteerio/grasshopper/milestones{/number}","notifications_url":"https://api.github.com/repos/fronteerio/grasshopper/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/fronteerio/grasshopper/labels{/name}","releases_url":"https://api.github.com/repos/fronteerio/grasshopper/releases{/id}","created_at":"2014-10-09T16:51:39Z","updated_at":"2015-05-01T14:17:34Z","pushed_at":"2015-05-14T21:51:39Z","git_url":"git://github.com/fronteerio/grasshopper.git","ssh_url":"git@github.com:fronteerio/grasshopper.git","clone_url":"https://github.com/fronteerio/grasshopper.git","svn_url":"https://github.com/fronteerio/grasshopper","homepage":null,"size":5693,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":12,"forks":4,"open_issues":12,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107"},"html":{"href":"https://github.com/fronteerio/grasshopper/pull/107"},"issue":{"href":"https://api.github.com/repos/fronteerio/grasshopper/issues/107"},"comments":{"href":"https://api.github.com/repos/fronteerio/grasshopper/issues/107/comments"},"review_comments":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107/comments"},"review_comment":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/fronteerio/grasshopper/pulls/107/commits"},"statuses":{"href":"https://api.github.com/repos/fronteerio/grasshopper/statuses/9de320968c1c9dcde0540a55d6e4959f9bb332d4"}}}}