{"action":"created","comment":{"url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/comments/73076680","id":73076680,"diff_hunk":"@@ -0,0 +1,583 @@\n+/**\n+ * Copyright ©2016. The Regents of the University of California (Regents). All Rights Reserved.\n+ *\n+ * Permission to use, copy, modify, and distribute this software and its documentation\n+ * for educational, research, and not-for-profit purposes, without fee and without a\n+ * signed licensing agreement, is hereby granted, provided that the above copyright\n+ * notice, this paragraph and the following two paragraphs appear in all copies,\n+ * modifications, and distributions.\n+ *\n+ * Contact The Office of Technology Licensing, UC Berkeley, 2150 Shattuck Avenue,\n+ * Suite 510, Berkeley, CA 94720-1620, (510) 643-7201, otl@berkeley.edu,\n+ * http://ipira.berkeley.edu/industry-info for commercial licensing opportunities.\n+ *\n+ * IN NO EVENT SHALL REGENTS BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL,\n+ * INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF\n+ * THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF REGENTS HAS BEEN ADVISED\n+ * OF THE POSSIBILITY OF SUCH DAMAGE.\n+ *\n+ * REGENTS SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\n+ * SOFTWARE AND ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED\n+ * \"AS IS\". REGENTS HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,\n+ * ENHANCEMENTS, OR MODIFICATIONS.\n+ */\n+\n+var _ = require('lodash');\n+var async = require('async');\n+var config = require('config');\n+var moment = require('moment-timezone');\n+\n+var ActivitiesApi = require('col-activities');\n+var CollabosphereConstants = require('col-core/lib/constants');\n+var CourseAPI = require('col-course');\n+var DB = require('col-core/lib/db');\n+var EmailUtil = require('col-core/lib/email');\n+var log = require('col-core/lib/logger')('col-activities/notifications');\n+var UsersAPI = require('col-users');\n+var UserConstants = require('col-users/lib/constants');\n+\n+ /**\n+ * Send weekly notification emails for all courses.\n+ *\n+ * @param  {Function}     callback    Standard callback function\n+ */\n+var collect = module.exports.collect = function(callback) {\n+  var start = Date.now();\n+\n+  // Get all active courses.\n+  CourseAPI.getCourses(function(err, courses) {\n+    if (err) {\n+      return log.error({'err': err}, 'Unable to retrieve courses for weekly notifications.');\n+    }\n+\n+    // Iterate through all courses.\n+    async.eachSeries(courses, collectCourse, function(err) {\n+      if (err) {\n+        return log.error({'err': err}, 'Unable to collect a course for weekly notifications.');\n+      }\n+      log.info({'duration': (Date.now() - start)}, 'Sent weekly notifications for all courses.');\n+\n+      return callback();\n+    });\n+  });\n+};\n+\n+/**\n+ * Send weekly notification emails for a given course.\n+ *\n+ * Note that this method is only made publicly available so it can be called from the integration\n+ * tests. Do not call this method directly.\n+ *\n+ * @param  {Course}         course          The course for which to send weekly notifications\n+ * @param  {Function}       callback        Standard callback function\n+ * @api private\n+ */\n+var collectCourse = module.exports.collectCourse = function(course, callback) {\n+  var start = Date.now();\n+\n+  // 1. Get users and activity data for the course.\n+  getCourseData(course, function(err, courseData) {\n+    // Don't pass errors up.\n+    if (err) {\n+      return callback();\n+    }\n+\n+    // Return early if no activity data was found for the course.\n+    if (!courseData) {\n+      return callback();\n+    }\n+\n+    // 2. Iterate through users in the course and send notifications to each.\n+    var userIterator = handleUser.bind(null, course, courseData.activitySummary);\n+    async.eachSeries(courseData.users, userIterator, function() {\n+      log.info({\n+        'course': course.id,\n+        'duration': (Date.now() - start)\n+      }, 'Sent weekly notifications for course.');\n+\n+      return callback();\n+    });\n+  });\n+};\n+\n+/**\n+ * Send notifications for a user in a course.\n+ *\n+ * Note that this function does not pass up errors.\n+ *\n+ * @param  {Course}     course              The course for which to send notifications\n+ * @param  {Object}     activitySummary     Weekly activity data for the course\n+ * @param  {User}       user                The user to notify\n+ * @param  {Function}   callback            Standard callback function\n+ * @api private\n+ */\n+var handleUser = function(course, activitySummary, user, callback) {\n+  // Ignore users without an email address.\n+  if (!user.canvas_email) {\n+    return callback();\n+  }\n+\n+  // Parse out activity data specific to this user.\n+  // TODO: Include ranks for the user: last week's and this week's both.\n+\n+  var userData = activitySummary.users[user.id] || {};\n+  if (!_.isEmpty(userData)) {\n+    var topAsset = sampleMaximum(userData.assets, function(asset) {\n+      // We define asset 'popularity' as a weighted sum of views, likes and comments.\n+      return asset.weeklyTotals.views + (2 * asset.weeklyTotals.likes) + (5 * asset.weeklyTotals.comments);\n+    });\n+    if (topAsset) {\n+      userData.topAsset = userData.assets[topAsset.id];\n+    }\n+  }\n+\n+  var subject = 'This week\\'s activity in ' + (course.name || 'The Asset Library and Whiteboards');\n+  var emailData = {\n+    'weekly': {\n+      'course': activitySummary.course,\n+      'user': userData\n+    }\n+  };\n+\n+  EmailUtil.sendEmail(subject, user, course, emailData, 'weekly', function(err) {\n+    // Errors are logged in the sendEmail implementation. Ignore them here, and continue to the next user.\n+    return callback();\n+  });\n+};\n+\n+\n+/* Data retrieval */\n+\n+\n+/**\n+ * Get activity and user data for a given course in the past week.\n+ *\n+ * @param  {Context}    course                                 The course for which to get activity data\n+ * @param  {Function}   callback                               Standard callback\n+ * @param  {Object}     callback.err                           An error object, if any\n+ * @param  {Object}     callback.courseData                    Course data for the weekly email\n+ * @param  {Object}     callback.courseData.activitySummary    Summary of the past week's activity in the course\n+ * @param  {User[]}     callback.data.users                    All active users in the course\n+ * @api private\n+ */\n+var getCourseData = function(course, callback) {\n+  var ctx = {\n+    'course': course,\n+    // Fake an admin user. At this point we can't reliably retrieve an admin from\n+    // the database as there might not be any yet\n+    'user': {\n+      'is_admin': true\n+    }\n+  };\n+\n+  // 1. Get all active users in the course.\n+  var options = {\n+    'enrollmentStates': CollabosphereConstants.ENROLLMENT_STATE.ACTIVE,\n+    'includeEmail': true\n+  };\n+  UsersAPI.getAllUsers(ctx, options, function(err, users) {\n+    if (err) {\n+      log.error({'err': err, 'course': ctx.course.id}, 'Failed to retrieve the users for a course');\n+      return callback(err);\n+    }\n+\n+    // Get user count, then index by id for quick lookup.\n+    var userCount = users.length;\n+    users = _.chain(users)\n+      .map(function(user) {\n+        return user.toJSON();\n+      })\n+      .indexBy('id')\n+      .value();\n+\n+    // 2. Get the points configuration for the course.\n+    ActivitiesApi.getActivityTypeConfiguration(ctx.course.id, function(err, activityTypeConfiguration) { \n+      if (err) {\n+        log.error({'err': err, 'course': ctx.course.id}, 'Failed to retrieve the points configuration for a course');\n+        return callback(err);\n+      }\n+\n+      // 3. Get all activities from the past week. We use a constant time cutoff for the date range so that we're\n+      // not affected by processing time during this job.\n+      var hour = config.get('email.weeklyHour');\n+      var dateRange = {\n+        'start': moment()\n+          .subtract(7, 'day')\n+          .hours(hour).minutes(0).seconds(0)\n+          .toDate(),\n+        'end': moment()\n+          .hours(hour).minutes(0).seconds(0)\n+          .toDate()\n+      };\n+\n+      getActivitiesForDateRange(ctx, dateRange, function(err, activities) {\n+        if (err) {\n+          log.error({'err': err, 'course': ctx.course.id}, 'Failed to retrieve activities for the past week');\n+          return callback(err);\n+        }\n+\n+        // Return early if no activities were found.\n+        if (_.isEmpty(activities)) {\n+          return callback();\n+        }\n+\n+        // 4. Calculate summary data for the week.\n+        var activitySummary = summarizeActivities(activities, activityTypeConfiguration, users, userCount);\n+        var courseData = {\n+          'activitySummary': activitySummary,\n+          'users': users\n+        };\n+\n+        return callback(null, courseData);\n+      });\n+    });\n+  });\n+};\n+\n+/**\n+ * Get recent activity data for a course.\n+ *\n+ * @param  {Context}    ctx                  Standard context containing the current user and the current course\n+ * @param  {Object}     dateRange            Timestamp range for activity creation\n+ * @param  {Date}       dateRange.start      Lower bound for activity creation timestamp\n+ * @param  {Date}       dateRange.end        Upper bound for activity creation timestamp\n+ * @param  {Function}   callback             Standard callback function\n+ * @param  {Object}     callback.err         An error object, if any\n+ * @param  {Activity[]} callback.activities  Activity data for the current course within the date range\n+ * @api private\n+ */\n+var getActivitiesForDateRange = function(ctx, dateRange, callback) {\n+  var options = {\n+    'where': {\n+      'course_id': ctx.course.id,\n+      'created_at': {'gt': dateRange.start, 'lt': dateRange.end}\n+    },\n+    'include': [{\n+      'model': DB.Asset,\n+      'include': [\n+        // All we need is the IDs of associated users, but Sequelize forces us to grab some user fields as well.\n+        // Get basic fields only.\n+        {\n+          'model': DB.User, \n+          'as': 'users', \n+          'required': true, \n+          'attributes': UserConstants.BASIC_USER_FIELDS\n+        },\n+        // Get new comments for this week only, for purposes of counting keywords.\n+        {\n+          'model': DB.Comment, \n+          'required': true, \n+          'where': {\n+            'created_at': {'gt': dateRange.start, 'lt': dateRange.end}\n+          }\n+        }\n+      ]\n+    }],\n+    'limit': null,\n+    'subQuery': false\n+  };\n+  DB.Activity.findAll(options).complete(function(err, activities) {\n+    if (err) {\n+      log.error({'err': err, 'course': ctx.course.id}, 'Unable to get recent activities for a course.');\n+      return callback(err);\n+    }\n+    return callback(null, activities);\n+  });\n+};\n+\n+\n+/* Summary data calculation */\n+\n+\n+/**\n+ * Calculate summary data from a list of activities.\n+ *\n+ * @param  {Activity[]}   activities                   Activities within a date range for a given course\n+ * @param  {Object}       activityTypeConfiguration    Activity type configuration for the course\n+ * @param  {Object}       users                        All active users in the course indexed by id\n+ * @param  {Number}       userCount                    Number of active users in the course\n+ * @return {Object}                                    Calculated summary data\n+ * @api private\n+ */\n+var summarizeActivities = function(activities, activityTypeConfiguration, users, userCount) {\n+  var courseTotals = new CourseTotals();\n+  var summary = {\n+    'course': {\n+      'totals': courseTotals\n+    },\n+    'assets': {},\n+    'users': {},\n+  };\n+\n+  var activityConfigurationByType = _.indexBy(activityTypeConfiguration, 'type');\n+\n+  // 1. Increment activities and points in all categories.\n+  _.forEach(activities, function(activity) {\n+    var type = activity.type;\n+    \n+    if (activityConfigurationByType[type].enabled) {\n+      var assetTotals = getAssetTotals(activity.asset, summary);\n+      var userTotals = getUserTotals(activity.user_id, summary);\n+\n+      // Before assigning points, increment activity counts for the associated user and asset.\n+      assetTotals.incrementActivities(type);\n+      userTotals.incrementActivities(type);\n+\n+      // Get the point value. \n+      var points = activityConfigurationByType[type].points;\n+\n+      // Increment points for the associated user and course under activity type. \n+      userTotals.incrementPoints(type, points);\n+      courseTotals.incrementPoints(type, points);\n+\n+      // Increment points for the associated user and course under generic totals.\n+      userTotals.incrementPoints('collected', points);\n+      courseTotals.incrementPoints('generated', points);\n+\n+      if (activity.actor_id && (activity.actor_id !== activity.user_id)) {\n+        // The actor and recipient are different users. Count points generated for the actor, and points\n+        // received for the recipient and course.\n+        var actorTotals = getUserTotals(activity.actor_id, summary);\n+        actorTotals.incrementPoints('generated', points);\n+        userTotals.incrementPoints('received', points);\n+        courseTotals.incrementPoints('received', points);\n+      } else {\n+        // There is no recipient separate from the actor. Count points generated only.\n+        userTotals.incrementPoints('generated', points);\n+      }\n+    }\n+  });\n+\n+  // 2. Divide course totals by the user count to get averages.\n+  summary.course.averages = _.mapValues(summary.course.totals, function(courseTotal) {\n+    return _.round(courseTotal / userCount);\n+  });\n+\n+  // 3. Find the top asset in each category.\n+  summary.course.topAssets = {};\n+  _.forEach(['comments', 'likes', 'views'], function(key) {\n+    var topAsset = sampleMaximum(summary.assets, _.property('weeklyTotals.' + key));\n+    if (topAsset) {\n+      summary.course.topAssets[key] = summary.assets[topAsset.id];\n+    }\n+  });\n+\n+  // 4. Find the top user in each category.\n+  summary.course.topUsers = {};\n+  _.forEach(['pointsGenerated', 'pointsReceived'], function(key) {\n+    var topUser = sampleMaximum(summary.users, _.property(key));\n+    if (topUser) {\n+      summary.course.topUsers[key] = {\n+        'total': topUser.value,\n+        'user': users[topUser.id]\n+      };\n+    }\n+  });\n+\n+  // TODO: Iterate through assets to get keyword frequencies from titles and descriptions (if created this\n+  // week), and from comments.\n+\n+  return summary;\n+};\n+\n+/**\n+ * Return or instantiate running totals for an asset.\n+ * \n+ * @param  {Number}    asset      The asset to get totals for\n+ * @param  {Object}    summary    The activities summary\n+ * @return {AssetTotals}          Running totals for the given asset\n+ * @api private\n+ *\n+ */\n+var getAssetTotals = function(asset, summary) {\n+  if (!asset) {\n+    return null;\n+  }\n+  if (!_.has(summary.assets, asset.id)) {","path":"node_modules/col-activities/lib/notifications/weekly.js","position":397,"original_position":397,"commit_id":"be576644f3c3432a2ea89e393454a46f92eab339","original_commit_id":"be576644f3c3432a2ea89e393454a46f92eab339","user":{"login":"nicolaasmatthijs","id":109850,"avatar_url":"https://avatars.githubusercontent.com/u/109850?v=3","gravatar_id":"","url":"https://api.github.com/users/nicolaasmatthijs","html_url":"https://github.com/nicolaasmatthijs","followers_url":"https://api.github.com/users/nicolaasmatthijs/followers","following_url":"https://api.github.com/users/nicolaasmatthijs/following{/other_user}","gists_url":"https://api.github.com/users/nicolaasmatthijs/gists{/gist_id}","starred_url":"https://api.github.com/users/nicolaasmatthijs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nicolaasmatthijs/subscriptions","organizations_url":"https://api.github.com/users/nicolaasmatthijs/orgs","repos_url":"https://api.github.com/users/nicolaasmatthijs/repos","events_url":"https://api.github.com/users/nicolaasmatthijs/events{/privacy}","received_events_url":"https://api.github.com/users/nicolaasmatthijs/received_events","type":"User","site_admin":false},"body":"`if (!summary.assets[asset.id]) {` would be fine here","created_at":"2016-08-02T00:21:40Z","updated_at":"2016-08-02T00:21:40Z","html_url":"https://github.com/ets-berkeley-edu/collabosphere/pull/145#discussion_r73076680","pull_request_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145","_links":{"self":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/comments/73076680"},"html":{"href":"https://github.com/ets-berkeley-edu/collabosphere/pull/145#discussion_r73076680"},"pull_request":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145"}}},"pull_request":{"url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145","id":78233032,"html_url":"https://github.com/ets-berkeley-edu/collabosphere/pull/145","diff_url":"https://github.com/ets-berkeley-edu/collabosphere/pull/145.diff","patch_url":"https://github.com/ets-berkeley-edu/collabosphere/pull/145.patch","issue_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/145","number":145,"state":"open","locked":false,"title":"COL-401 Collect weekly activity summaries for use in email template","user":{"login":"paulkerschen","id":2413467,"avatar_url":"https://avatars.githubusercontent.com/u/2413467?v=3","gravatar_id":"","url":"https://api.github.com/users/paulkerschen","html_url":"https://github.com/paulkerschen","followers_url":"https://api.github.com/users/paulkerschen/followers","following_url":"https://api.github.com/users/paulkerschen/following{/other_user}","gists_url":"https://api.github.com/users/paulkerschen/gists{/gist_id}","starred_url":"https://api.github.com/users/paulkerschen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paulkerschen/subscriptions","organizations_url":"https://api.github.com/users/paulkerschen/orgs","repos_url":"https://api.github.com/users/paulkerschen/repos","events_url":"https://api.github.com/users/paulkerschen/events{/privacy}","received_events_url":"https://api.github.com/users/paulkerschen/received_events","type":"User","site_admin":false},"body":"https://jira.ets.berkeley.edu/jira/browse/COL-401\r\n\r\nNotes:\r\n- This code has been exercised locally in the console, but there are no tests yet. Integration tests will follow once data is hooked up to the email template.\r\n- Two not-yet-implemented activity types are assumed: `whiteboard_add_asset` and `whiteboard_chat`.\r\n- Keyword counts for the week are to be added in a later task.\r\n- User leaderboard ranks, present and past, are to be added in a later task.\r\n- For now, weekly email scheduling is arbitrarily configured to Friday at noon. This configuration is used in setting the date range for the activities query, but is not yet added to the scheduling code in `col-activities/lib/notifications/api.js`.","created_at":"2016-07-20T22:51:41Z","updated_at":"2016-08-02T00:21:40Z","closed_at":null,"merged_at":null,"merge_commit_sha":"64443d61b702089280750ab0491e595e2c6e0743","assignee":{"login":"nicolaasmatthijs","id":109850,"avatar_url":"https://avatars.githubusercontent.com/u/109850?v=3","gravatar_id":"","url":"https://api.github.com/users/nicolaasmatthijs","html_url":"https://github.com/nicolaasmatthijs","followers_url":"https://api.github.com/users/nicolaasmatthijs/followers","following_url":"https://api.github.com/users/nicolaasmatthijs/following{/other_user}","gists_url":"https://api.github.com/users/nicolaasmatthijs/gists{/gist_id}","starred_url":"https://api.github.com/users/nicolaasmatthijs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nicolaasmatthijs/subscriptions","organizations_url":"https://api.github.com/users/nicolaasmatthijs/orgs","repos_url":"https://api.github.com/users/nicolaasmatthijs/repos","events_url":"https://api.github.com/users/nicolaasmatthijs/events{/privacy}","received_events_url":"https://api.github.com/users/nicolaasmatthijs/received_events","type":"User","site_admin":false},"assignees":[{"login":"nicolaasmatthijs","id":109850,"avatar_url":"https://avatars.githubusercontent.com/u/109850?v=3","gravatar_id":"","url":"https://api.github.com/users/nicolaasmatthijs","html_url":"https://github.com/nicolaasmatthijs","followers_url":"https://api.github.com/users/nicolaasmatthijs/followers","following_url":"https://api.github.com/users/nicolaasmatthijs/following{/other_user}","gists_url":"https://api.github.com/users/nicolaasmatthijs/gists{/gist_id}","starred_url":"https://api.github.com/users/nicolaasmatthijs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nicolaasmatthijs/subscriptions","organizations_url":"https://api.github.com/users/nicolaasmatthijs/orgs","repos_url":"https://api.github.com/users/nicolaasmatthijs/repos","events_url":"https://api.github.com/users/nicolaasmatthijs/events{/privacy}","received_events_url":"https://api.github.com/users/nicolaasmatthijs/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145/commits","review_comments_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145/comments","review_comment_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/145/comments","statuses_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/statuses/be576644f3c3432a2ea89e393454a46f92eab339","head":{"label":"paulkerschen:col-401","ref":"col-401","sha":"be576644f3c3432a2ea89e393454a46f92eab339","user":{"login":"paulkerschen","id":2413467,"avatar_url":"https://avatars.githubusercontent.com/u/2413467?v=3","gravatar_id":"","url":"https://api.github.com/users/paulkerschen","html_url":"https://github.com/paulkerschen","followers_url":"https://api.github.com/users/paulkerschen/followers","following_url":"https://api.github.com/users/paulkerschen/following{/other_user}","gists_url":"https://api.github.com/users/paulkerschen/gists{/gist_id}","starred_url":"https://api.github.com/users/paulkerschen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paulkerschen/subscriptions","organizations_url":"https://api.github.com/users/paulkerschen/orgs","repos_url":"https://api.github.com/users/paulkerschen/repos","events_url":"https://api.github.com/users/paulkerschen/events{/privacy}","received_events_url":"https://api.github.com/users/paulkerschen/received_events","type":"User","site_admin":false},"repo":{"id":59068710,"name":"collabosphere","full_name":"paulkerschen/collabosphere","owner":{"login":"paulkerschen","id":2413467,"avatar_url":"https://avatars.githubusercontent.com/u/2413467?v=3","gravatar_id":"","url":"https://api.github.com/users/paulkerschen","html_url":"https://github.com/paulkerschen","followers_url":"https://api.github.com/users/paulkerschen/followers","following_url":"https://api.github.com/users/paulkerschen/following{/other_user}","gists_url":"https://api.github.com/users/paulkerschen/gists{/gist_id}","starred_url":"https://api.github.com/users/paulkerschen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/paulkerschen/subscriptions","organizations_url":"https://api.github.com/users/paulkerschen/orgs","repos_url":"https://api.github.com/users/paulkerschen/repos","events_url":"https://api.github.com/users/paulkerschen/events{/privacy}","received_events_url":"https://api.github.com/users/paulkerschen/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/paulkerschen/collabosphere","description":"","fork":true,"url":"https://api.github.com/repos/paulkerschen/collabosphere","forks_url":"https://api.github.com/repos/paulkerschen/collabosphere/forks","keys_url":"https://api.github.com/repos/paulkerschen/collabosphere/keys{/key_id}","collaborators_url":"https://api.github.com/repos/paulkerschen/collabosphere/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/paulkerschen/collabosphere/teams","hooks_url":"https://api.github.com/repos/paulkerschen/collabosphere/hooks","issue_events_url":"https://api.github.com/repos/paulkerschen/collabosphere/issues/events{/number}","events_url":"https://api.github.com/repos/paulkerschen/collabosphere/events","assignees_url":"https://api.github.com/repos/paulkerschen/collabosphere/assignees{/user}","branches_url":"https://api.github.com/repos/paulkerschen/collabosphere/branches{/branch}","tags_url":"https://api.github.com/repos/paulkerschen/collabosphere/tags","blobs_url":"https://api.github.com/repos/paulkerschen/collabosphere/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/paulkerschen/collabosphere/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/paulkerschen/collabosphere/git/refs{/sha}","trees_url":"https://api.github.com/repos/paulkerschen/collabosphere/git/trees{/sha}","statuses_url":"https://api.github.com/repos/paulkerschen/collabosphere/statuses/{sha}","languages_url":"https://api.github.com/repos/paulkerschen/collabosphere/languages","stargazers_url":"https://api.github.com/repos/paulkerschen/collabosphere/stargazers","contributors_url":"https://api.github.com/repos/paulkerschen/collabosphere/contributors","subscribers_url":"https://api.github.com/repos/paulkerschen/collabosphere/subscribers","subscription_url":"https://api.github.com/repos/paulkerschen/collabosphere/subscription","commits_url":"https://api.github.com/repos/paulkerschen/collabosphere/commits{/sha}","git_commits_url":"https://api.github.com/repos/paulkerschen/collabosphere/git/commits{/sha}","comments_url":"https://api.github.com/repos/paulkerschen/collabosphere/comments{/number}","issue_comment_url":"https://api.github.com/repos/paulkerschen/collabosphere/issues/comments{/number}","contents_url":"https://api.github.com/repos/paulkerschen/collabosphere/contents/{+path}","compare_url":"https://api.github.com/repos/paulkerschen/collabosphere/compare/{base}...{head}","merges_url":"https://api.github.com/repos/paulkerschen/collabosphere/merges","archive_url":"https://api.github.com/repos/paulkerschen/collabosphere/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/paulkerschen/collabosphere/downloads","issues_url":"https://api.github.com/repos/paulkerschen/collabosphere/issues{/number}","pulls_url":"https://api.github.com/repos/paulkerschen/collabosphere/pulls{/number}","milestones_url":"https://api.github.com/repos/paulkerschen/collabosphere/milestones{/number}","notifications_url":"https://api.github.com/repos/paulkerschen/collabosphere/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/paulkerschen/collabosphere/labels{/name}","releases_url":"https://api.github.com/repos/paulkerschen/collabosphere/releases{/id}","deployments_url":"https://api.github.com/repos/paulkerschen/collabosphere/deployments","created_at":"2016-05-18T00:27:39Z","updated_at":"2016-05-18T00:27:40Z","pushed_at":"2016-07-25T17:44:27Z","git_url":"git://github.com/paulkerschen/collabosphere.git","ssh_url":"git@github.com:paulkerschen/collabosphere.git","clone_url":"https://github.com/paulkerschen/collabosphere.git","svn_url":"https://github.com/paulkerschen/collabosphere","homepage":null,"size":6395,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","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":"master"}},"base":{"label":"ets-berkeley-edu:master","ref":"master","sha":"687d332c06eb5d8da1cc5a7f21500894e9d126ff","user":{"login":"ets-berkeley-edu","id":283538,"avatar_url":"https://avatars.githubusercontent.com/u/283538?v=3","gravatar_id":"","url":"https://api.github.com/users/ets-berkeley-edu","html_url":"https://github.com/ets-berkeley-edu","followers_url":"https://api.github.com/users/ets-berkeley-edu/followers","following_url":"https://api.github.com/users/ets-berkeley-edu/following{/other_user}","gists_url":"https://api.github.com/users/ets-berkeley-edu/gists{/gist_id}","starred_url":"https://api.github.com/users/ets-berkeley-edu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ets-berkeley-edu/subscriptions","organizations_url":"https://api.github.com/users/ets-berkeley-edu/orgs","repos_url":"https://api.github.com/users/ets-berkeley-edu/repos","events_url":"https://api.github.com/users/ets-berkeley-edu/events{/privacy}","received_events_url":"https://api.github.com/users/ets-berkeley-edu/received_events","type":"Organization","site_admin":false},"repo":{"id":30094168,"name":"collabosphere","full_name":"ets-berkeley-edu/collabosphere","owner":{"login":"ets-berkeley-edu","id":283538,"avatar_url":"https://avatars.githubusercontent.com/u/283538?v=3","gravatar_id":"","url":"https://api.github.com/users/ets-berkeley-edu","html_url":"https://github.com/ets-berkeley-edu","followers_url":"https://api.github.com/users/ets-berkeley-edu/followers","following_url":"https://api.github.com/users/ets-berkeley-edu/following{/other_user}","gists_url":"https://api.github.com/users/ets-berkeley-edu/gists{/gist_id}","starred_url":"https://api.github.com/users/ets-berkeley-edu/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ets-berkeley-edu/subscriptions","organizations_url":"https://api.github.com/users/ets-berkeley-edu/orgs","repos_url":"https://api.github.com/users/ets-berkeley-edu/repos","events_url":"https://api.github.com/users/ets-berkeley-edu/events{/privacy}","received_events_url":"https://api.github.com/users/ets-berkeley-edu/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ets-berkeley-edu/collabosphere","description":"","fork":false,"url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere","forks_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/forks","keys_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/teams","hooks_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/hooks","issue_events_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/events{/number}","events_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/events","assignees_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/assignees{/user}","branches_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/branches{/branch}","tags_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/tags","blobs_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/git/refs{/sha}","trees_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/statuses/{sha}","languages_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/languages","stargazers_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/stargazers","contributors_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/contributors","subscribers_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/subscribers","subscription_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/subscription","commits_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/commits{/sha}","git_commits_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/git/commits{/sha}","comments_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/comments{/number}","issue_comment_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/comments{/number}","contents_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/contents/{+path}","compare_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/merges","archive_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/downloads","issues_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues{/number}","pulls_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls{/number}","milestones_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/milestones{/number}","notifications_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/labels{/name}","releases_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/releases{/id}","deployments_url":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/deployments","created_at":"2015-01-30T22:46:30Z","updated_at":"2016-04-22T18:56:45Z","pushed_at":"2016-07-28T16:55:28Z","git_url":"git://github.com/ets-berkeley-edu/collabosphere.git","ssh_url":"git@github.com:ets-berkeley-edu/collabosphere.git","clone_url":"https://github.com/ets-berkeley-edu/collabosphere.git","svn_url":"https://github.com/ets-berkeley-edu/collabosphere","homepage":null,"size":6507,"stargazers_count":3,"watchers_count":3,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":4,"forks":2,"open_issues":4,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145"},"html":{"href":"https://github.com/ets-berkeley-edu/collabosphere/pull/145"},"issue":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/145"},"comments":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/issues/145/comments"},"review_comments":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145/comments"},"review_comment":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/pulls/145/commits"},"statuses":{"href":"https://api.github.com/repos/ets-berkeley-edu/collabosphere/statuses/be576644f3c3432a2ea89e393454a46f92eab339"}}}}
{"actor":{"display_login":"nicolaasmatthijs"}}