{"action":"created","comment":{"url":"https://api.github.com/repos/thecartercenter/elmo/pulls/comments/38927357","id":38927357,"diff_hunk":"@@ -35,79 +36,247 @@ def create_users(mission)\n     end\n \n     # parse the input file as a spreadsheet\n-    data = Roo::Spreadsheet.open(file)\n+    @data = Roo::Spreadsheet.open(file)\n \n+    validate_headers\n+\n+    if !@validation_error\n+\n+      # create the users in a transaction in case of validation error\n+      User.transaction do\n+\n+        row_start = @data.first_row\n+        @import_num = last_import_num_on_users\n+\n+        user_batch_attributes = parse_rows(row_start)\n+\n+        (0..number_of_iterations).each do |i|\n+          current_attributes_batch = user_batch_attributes[row_start-1, BATCH_SIZE]\n+\n+          users_batch = create_users_instances(current_attributes_batch, mission)\n+          create_hash_table_with_fields_and_indexes(users_batch)\n+\n+          validate_users_batch(users_batch)\n+\n+          check_uniqueness_on_objects(users_batch, 'email')\n+          check_uniqueness_on_objects(users_batch, 'login')\n+          check_uniqueness_on_objects(users_batch, 'phone')\n+          check_uniqueness_on_objects(users_batch, 'phone2')\n+\n+          check_uniqueness_on_db(users_batch, 'users', 'email', row_start)\n+          check_uniqueness_on_db(users_batch, 'users', 'login', row_start)\n+          check_uniqueness_on_db(users_batch, 'users', 'phone', row_start, ['phone','phone2'])\n+          check_uniqueness_on_db(users_batch, 'users', 'phone2', row_start, ['phone2','phone'])\n+\n+          check_validation_errors(users_batch, row_start)\n+\n+          break if errors_reached_limit\n+\n+          DirectDBConn.insert_on_db(users_batch, 'users')\n+          insert_assignments_on_db(users_batch)\n+\n+          users.concat users_batch\n+\n+          p \"Batch: #{i}\"\n+\n+          # Set row to start on the next batch\n+          row_start = ((i+1) * BATCH_SIZE)+1\n+        end\n+\n+        # now if there was a validation error with any user, rollback the transaction\n+        raise ActiveRecord::Rollback if @validation_error\n+\n+      end # transaction\n+    end\n+\n+    return succeeded?\n+  end\n+\n+  private\n+\n+  def validate_headers\n     # assume the first row is the header row\n-    headers = data.row(data.first_row)\n+    headers = @data.row(@data.first_row)\n \n     expected_headers = Hash[*%i{name phone phone2 email notes}.map do |field|\n       [User.human_attribute_name(field), field]\n     end.flatten]\n \n-    fields = Hash[*headers.map.with_index do |header,index|\n+    @fields = Hash[*headers.map.with_index do |header,index|\n       [index, expected_headers[header]]\n     end.flatten]\n \n     # validate headers\n-    if fields.values.any?(&:nil?)\n+    if @fields.values.any?(&:nil?)\n       @validation_error = true\n       errors.add(:file, :invalid_headers)\n       return succeeded?\n     end\n+  end\n \n-    # create the users in a transaction in case of validation error\n-    User.transaction do\n-\n-      # iterate over each row, creating users as we go\n-      data.each_row_streaming(offset: data.first_row).with_index do |row,row_index|\n-        # excel row numbers are 1-indexed\n-        row_number = 1 + data.first_row + row_index\n-\n-        # stop processing rows after a certain number of errors\n-        # (we do this at the beginning of the loop to avoid adding a redundant\n-        # :too_many_errors error on the last loop iteration)\n-        if errors.count >= IMPORT_ERROR_CUTOFF\n-          # we pass the previous row number to the error message formatting\n-          # since the error count cutoff was surpassed by the previous rows,\n-          # not this row\n-          errors.add(:users, :too_many_errors, row: row_number - 1)\n-          break\n-        end\n+  def number_of_iterations\n+    users_rows_count = @data.count - 1\n+    (users_rows_count/BATCH_SIZE.to_f).ceil - 1\n+  end\n+\n+  def parse_rows(offset)\n+    user_batch_attributes = []\n+\n+    @data.each_row_streaming(offset: offset).with_index do |row,row_index|\n+      # excel row numbers are 1-indexed\n+      row_number = 1 + @data.first_row + row_index\n+\n+      # skip blank rows\n+      next if row.all?(&:blank?)\n+\n+      # turn the row into an attribute hash\n+      attributes = Hash[*row.map do |cell|\n+        field = @fields[cell.coordinate.column - 1]\n+        [field, cell.value.presence]\n+      end.flatten]\n+\n+      user_batch_attributes << attributes\n+    end\n+\n+    user_batch_attributes\n+  end\n+\n+  def create_users_instances(attribs, mission)\n+    attribs.map{ |attrib| create_new_user(attrib, mission) }\n+  end\n+\n+  def create_hash_table_with_fields_and_indexes(objects)\n+    @fields_hash_table = {\n+      'email' => {},\n+      'login' => {},\n+      'phone' => {}\n+    }\n+\n+    columns = ['email', 'login', 'phone', 'phone2']\n+\n+    columns.each{ |field| populate_hash_with_field_and_occurrences(field, objects) }\n+  end\n+\n+  def populate_hash_with_field_and_occurrences(field, objects)\n+    key = correct_field_key(field)\n+\n+    objects.map{|o| o.send(field)}.each_with_index do |value, index|\n+      (@fields_hash_table[key][value] ||= []) << index unless value.nil?\n+    end\n+  end\n+\n+  def validate_users_batch(users_batch)\n+    users_batch.each{|u| u.valid? }\n+  end\n+\n+  def check_validation_errors(users, row_start)\n+    users.each_with_index do |user, index|\n+      row_number = actual_row_number(row_start, index)\n+      add_validation_error_messages(user, row_number)\n+\n+      if errors_reached_limit\n+        add_too_many_errors(row_number)\n+        break\n+      end\n+    end\n+  end\n+\n+  def create_new_user(attributes, mission)\n+    User.new(attributes.merge(\n+             reset_password_method: \"print\",\n+             admin: false,\n+             assignments: [Assignment.new(mission: mission, role: User::ROLES.first)],\n+             skip_validations_with_queries: true,\n+             import_num: @import_num += 1))\n+  end\n \n-        # skip blank rows\n-        next if row.all?(&:blank?)\n-\n-        # turn the row into an attribute hash\n-        attributes = Hash[*row.map do |cell|\n-          field = fields[cell.coordinate.column - 1]\n-          [field, cell.value.presence]\n-        end.flatten]\n-\n-        # attempt to create the user with the parsed params\n-        user = User.create(attributes.merge(\n-          reset_password_method: \"print\",\n-          assignments: [Assignment.new(mission_id: mission.id, role: User::ROLES.first)]))\n-\n-        # if the user has errors, add them to the batch's errors\n-        if !user.valid?\n-          user.errors.keys.each do |attribute|\n-            user.errors.full_messages_for(attribute).each do |error|\n-              row_error = I18n.t('import.row_error', row: row_number, error: error)\n-              errors.add(\"users[#{row_index}].#{attribute}\", row_error)\n-            end\n+  def add_validation_error_messages(user, row_number)\n+    # if the user has errors, add them to the batch's errors\n+    unless user.errors.empty?\n+      user.errors.keys.each do |attribute|\n+        if attribute != :persistence_token\n+          user.errors.full_messages_for(attribute).each do |error|\n+            add_error(error, attribute, row_number)\n           end\n-          @validation_error = true\n         end\n+      end\n+      @validation_error = true unless error_is_on_persistence_token? user\n+    end\n+  end\n \n-        users << user\n+  def error_is_on_persistence_token?(user)\n+    (user.errors.keys.length == 1) && (user.errors.keys.include? :persistence_token)\n+  end\n \n-      end # iteration over rows\n+  def add_error(error, attribute, row_number)\n+    row_error = I18n.t('import.row_error', row: row_number, error: error)\n+    errors.add(\"users[#{row_number}].#{attribute}\", row_error)\n+  end\n \n-      # now if there was a validation error with any user, rollback the transaction\n-      raise ActiveRecord::Rollback if @validation_error\n+  def add_too_many_errors(row_number)\n+    # we pass the previous row number to the error message formatting\n+    # since the error count cutoff was surpassed by the previous rows,\n+    # not this row\n+    errors.add(:users, :too_many_errors, row: row_number - 1) if errors_reached_limit\n+  end\n \n-    end # transaction\n+  def errors_reached_limit\n+    errors.count >= IMPORT_ERROR_CUTOFF\n+  end\n \n-    return succeeded?\n+  def actual_row_number(row_start, index)\n+    index + row_start + 1\n+  end\n+\n+  def check_uniqueness_on_objects(objects, field)\n+    key = correct_field_key(field)\n+    # Add errors on fields that aren't unique\n+    @fields_hash_table[key].each do |k,v|\n+      v.each { |i| objects.at(i).errors.add(field, :taken) } if v.length != 1\n+    end\n+  end\n+\n+  def check_uniqueness_on_db(objects, table, field, row_start, columns=[])\n+    results = DirectDBConn.check_uniqueness_on_db(objects, table, field, row_start, columns)\n+\n+    results.each do |result|\n+      @fields_hash_table[field][result.first].each do |index|\n+        p \"UNIQ FAIL DB: #{field} : #{result.first}\"\n+        object = objects.at(index)\n+        object.errors.add(field, :taken)\n+      end\n+    end\n+  end\n+\n+  def correct_field_key(field)\n+    field.include?('phone') ? 'phone' : field\n+  end\n+\n+  def insert_assignments_on_db(objects)\n+    assignments = objects.map{ |u| u.assignments.first }\n+\n+    string_params = DirectDBConn.generate_string_params_template(assignments.length)\n+    column_names = assignments.first.attributes.keys.join(', ')\n+    users_id = objects.map{|o| \"(SELECT id FROM users WHERE import_num=#{o.import_num})\" }\n+\n+    object_values = assignments.map.with_index do |assignment, index|\n+      values_in_string = DirectDBConn.convert_values_to_insert_syntax(assignment)\n+      values_as_array = values_in_string.split(',')\n+\n+      #Add Select query on user_id position\n+      user_id_index = column_names.split(', ').index('user_id')\n+      values_as_array[user_id_index] = users_id[index]\n+\n+      \"(#{values_as_array.join(',')})\"\n+    end\n+\n+    sql_query_array = [\"INSERT INTO assignments (#{column_names}) VALUES #{object_values.join(', ')}\"]\n+\n+    DirectDBConn.execute_sql_query_array(sql_query_array)","path":"app/models/user_batch.rb","position":298,"original_position":298,"commit_id":"bc9e392f88720b21862cd1586ae3e49fc4cdc1e9","original_commit_id":"bc9e392f88720b21862cd1586ae3e49fc4cdc1e9","user":{"login":"hooverlunch","id":702320,"avatar_url":"https://avatars.githubusercontent.com/u/702320?v=3","gravatar_id":"","url":"https://api.github.com/users/hooverlunch","html_url":"https://github.com/hooverlunch","followers_url":"https://api.github.com/users/hooverlunch/followers","following_url":"https://api.github.com/users/hooverlunch/following{/other_user}","gists_url":"https://api.github.com/users/hooverlunch/gists{/gist_id}","starred_url":"https://api.github.com/users/hooverlunch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hooverlunch/subscriptions","organizations_url":"https://api.github.com/users/hooverlunch/orgs","repos_url":"https://api.github.com/users/hooverlunch/repos","events_url":"https://api.github.com/users/hooverlunch/events{/privacy}","received_events_url":"https://api.github.com/users/hooverlunch/received_events","type":"User","site_admin":false},"body":"I don't think so. This is one query and one line of code that replaces the whole method above. I think it is also likely faster than running a huge chunk of inserts because it all happens internal to MySQL. I can help you debug if need be.","created_at":"2015-09-08T13:52:37Z","updated_at":"2015-09-08T13:52:37Z","html_url":"https://github.com/thecartercenter/elmo/pull/140#discussion_r38927357","pull_request_url":"https://api.github.com/repos/thecartercenter/elmo/pulls/140","_links":{"self":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/comments/38927357"},"html":{"href":"https://github.com/thecartercenter/elmo/pull/140#discussion_r38927357"},"pull_request":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/140"}}},"pull_request":{"url":"https://api.github.com/repos/thecartercenter/elmo/pulls/140","id":44156288,"html_url":"https://github.com/thecartercenter/elmo/pull/140","diff_url":"https://github.com/thecartercenter/elmo/pull/140.diff","patch_url":"https://github.com/thecartercenter/elmo/pull/140.patch","issue_url":"https://api.github.com/repos/thecartercenter/elmo/issues/140","number":140,"state":"open","locked":false,"title":"3005 optimize user creation","user":{"login":"lmatiolis","id":5368881,"avatar_url":"https://avatars.githubusercontent.com/u/5368881?v=3","gravatar_id":"","url":"https://api.github.com/users/lmatiolis","html_url":"https://github.com/lmatiolis","followers_url":"https://api.github.com/users/lmatiolis/followers","following_url":"https://api.github.com/users/lmatiolis/following{/other_user}","gists_url":"https://api.github.com/users/lmatiolis/gists{/gist_id}","starred_url":"https://api.github.com/users/lmatiolis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lmatiolis/subscriptions","organizations_url":"https://api.github.com/users/lmatiolis/orgs","repos_url":"https://api.github.com/users/lmatiolis/repos","events_url":"https://api.github.com/users/lmatiolis/events{/privacy}","received_events_url":"https://api.github.com/users/lmatiolis/received_events","type":"User","site_admin":false},"body":"Doing checks in batches on users and on the spreadsheet. We are doing this to avoid a lot of calls to the database for each user. Also doing one query to insert in batches.\r\nThis required some bypassing on users validation (to not trigger some queries)","created_at":"2015-09-03T20:54:54Z","updated_at":"2015-09-08T13:52:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"a0c6b784188a2b9a0834104645c6399b0047ceab","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/thecartercenter/elmo/pulls/140/commits","review_comments_url":"https://api.github.com/repos/thecartercenter/elmo/pulls/140/comments","review_comment_url":"https://api.github.com/repos/thecartercenter/elmo/pulls/comments{/number}","comments_url":"https://api.github.com/repos/thecartercenter/elmo/issues/140/comments","statuses_url":"https://api.github.com/repos/thecartercenter/elmo/statuses/bc9e392f88720b21862cd1586ae3e49fc4cdc1e9","head":{"label":"thecartercenter:3005_optimize_user_creation","ref":"3005_optimize_user_creation","sha":"bc9e392f88720b21862cd1586ae3e49fc4cdc1e9","user":{"login":"thecartercenter","id":5075178,"avatar_url":"https://avatars.githubusercontent.com/u/5075178?v=3","gravatar_id":"","url":"https://api.github.com/users/thecartercenter","html_url":"https://github.com/thecartercenter","followers_url":"https://api.github.com/users/thecartercenter/followers","following_url":"https://api.github.com/users/thecartercenter/following{/other_user}","gists_url":"https://api.github.com/users/thecartercenter/gists{/gist_id}","starred_url":"https://api.github.com/users/thecartercenter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thecartercenter/subscriptions","organizations_url":"https://api.github.com/users/thecartercenter/orgs","repos_url":"https://api.github.com/users/thecartercenter/repos","events_url":"https://api.github.com/users/thecartercenter/events{/privacy}","received_events_url":"https://api.github.com/users/thecartercenter/received_events","type":"Organization","site_admin":false},"repo":{"id":11615188,"name":"elmo","full_name":"thecartercenter/elmo","owner":{"login":"thecartercenter","id":5075178,"avatar_url":"https://avatars.githubusercontent.com/u/5075178?v=3","gravatar_id":"","url":"https://api.github.com/users/thecartercenter","html_url":"https://github.com/thecartercenter","followers_url":"https://api.github.com/users/thecartercenter/followers","following_url":"https://api.github.com/users/thecartercenter/following{/other_user}","gists_url":"https://api.github.com/users/thecartercenter/gists{/gist_id}","starred_url":"https://api.github.com/users/thecartercenter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thecartercenter/subscriptions","organizations_url":"https://api.github.com/users/thecartercenter/orgs","repos_url":"https://api.github.com/users/thecartercenter/repos","events_url":"https://api.github.com/users/thecartercenter/events{/privacy}","received_events_url":"https://api.github.com/users/thecartercenter/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/thecartercenter/elmo","description":"","fork":false,"url":"https://api.github.com/repos/thecartercenter/elmo","forks_url":"https://api.github.com/repos/thecartercenter/elmo/forks","keys_url":"https://api.github.com/repos/thecartercenter/elmo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/thecartercenter/elmo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/thecartercenter/elmo/teams","hooks_url":"https://api.github.com/repos/thecartercenter/elmo/hooks","issue_events_url":"https://api.github.com/repos/thecartercenter/elmo/issues/events{/number}","events_url":"https://api.github.com/repos/thecartercenter/elmo/events","assignees_url":"https://api.github.com/repos/thecartercenter/elmo/assignees{/user}","branches_url":"https://api.github.com/repos/thecartercenter/elmo/branches{/branch}","tags_url":"https://api.github.com/repos/thecartercenter/elmo/tags","blobs_url":"https://api.github.com/repos/thecartercenter/elmo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/thecartercenter/elmo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/thecartercenter/elmo/git/refs{/sha}","trees_url":"https://api.github.com/repos/thecartercenter/elmo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/thecartercenter/elmo/statuses/{sha}","languages_url":"https://api.github.com/repos/thecartercenter/elmo/languages","stargazers_url":"https://api.github.com/repos/thecartercenter/elmo/stargazers","contributors_url":"https://api.github.com/repos/thecartercenter/elmo/contributors","subscribers_url":"https://api.github.com/repos/thecartercenter/elmo/subscribers","subscription_url":"https://api.github.com/repos/thecartercenter/elmo/subscription","commits_url":"https://api.github.com/repos/thecartercenter/elmo/commits{/sha}","git_commits_url":"https://api.github.com/repos/thecartercenter/elmo/git/commits{/sha}","comments_url":"https://api.github.com/repos/thecartercenter/elmo/comments{/number}","issue_comment_url":"https://api.github.com/repos/thecartercenter/elmo/issues/comments{/number}","contents_url":"https://api.github.com/repos/thecartercenter/elmo/contents/{+path}","compare_url":"https://api.github.com/repos/thecartercenter/elmo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/thecartercenter/elmo/merges","archive_url":"https://api.github.com/repos/thecartercenter/elmo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/thecartercenter/elmo/downloads","issues_url":"https://api.github.com/repos/thecartercenter/elmo/issues{/number}","pulls_url":"https://api.github.com/repos/thecartercenter/elmo/pulls{/number}","milestones_url":"https://api.github.com/repos/thecartercenter/elmo/milestones{/number}","notifications_url":"https://api.github.com/repos/thecartercenter/elmo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/thecartercenter/elmo/labels{/name}","releases_url":"https://api.github.com/repos/thecartercenter/elmo/releases{/id}","created_at":"2013-07-23T17:56:04Z","updated_at":"2015-08-01T19:23:18Z","pushed_at":"2015-09-04T21:03:20Z","git_url":"git://github.com/thecartercenter/elmo.git","ssh_url":"git@github.com:thecartercenter/elmo.git","clone_url":"https://github.com/thecartercenter/elmo.git","svn_url":"https://github.com/thecartercenter/elmo","homepage":null,"size":41405,"stargazers_count":14,"watchers_count":14,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":16,"mirror_url":null,"open_issues_count":3,"forks":16,"open_issues":3,"watchers":14,"default_branch":"master"}},"base":{"label":"thecartercenter:develop","ref":"develop","sha":"b1a5c64001552623a5b25e0b39a23cbef0857c0f","user":{"login":"thecartercenter","id":5075178,"avatar_url":"https://avatars.githubusercontent.com/u/5075178?v=3","gravatar_id":"","url":"https://api.github.com/users/thecartercenter","html_url":"https://github.com/thecartercenter","followers_url":"https://api.github.com/users/thecartercenter/followers","following_url":"https://api.github.com/users/thecartercenter/following{/other_user}","gists_url":"https://api.github.com/users/thecartercenter/gists{/gist_id}","starred_url":"https://api.github.com/users/thecartercenter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thecartercenter/subscriptions","organizations_url":"https://api.github.com/users/thecartercenter/orgs","repos_url":"https://api.github.com/users/thecartercenter/repos","events_url":"https://api.github.com/users/thecartercenter/events{/privacy}","received_events_url":"https://api.github.com/users/thecartercenter/received_events","type":"Organization","site_admin":false},"repo":{"id":11615188,"name":"elmo","full_name":"thecartercenter/elmo","owner":{"login":"thecartercenter","id":5075178,"avatar_url":"https://avatars.githubusercontent.com/u/5075178?v=3","gravatar_id":"","url":"https://api.github.com/users/thecartercenter","html_url":"https://github.com/thecartercenter","followers_url":"https://api.github.com/users/thecartercenter/followers","following_url":"https://api.github.com/users/thecartercenter/following{/other_user}","gists_url":"https://api.github.com/users/thecartercenter/gists{/gist_id}","starred_url":"https://api.github.com/users/thecartercenter/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/thecartercenter/subscriptions","organizations_url":"https://api.github.com/users/thecartercenter/orgs","repos_url":"https://api.github.com/users/thecartercenter/repos","events_url":"https://api.github.com/users/thecartercenter/events{/privacy}","received_events_url":"https://api.github.com/users/thecartercenter/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/thecartercenter/elmo","description":"","fork":false,"url":"https://api.github.com/repos/thecartercenter/elmo","forks_url":"https://api.github.com/repos/thecartercenter/elmo/forks","keys_url":"https://api.github.com/repos/thecartercenter/elmo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/thecartercenter/elmo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/thecartercenter/elmo/teams","hooks_url":"https://api.github.com/repos/thecartercenter/elmo/hooks","issue_events_url":"https://api.github.com/repos/thecartercenter/elmo/issues/events{/number}","events_url":"https://api.github.com/repos/thecartercenter/elmo/events","assignees_url":"https://api.github.com/repos/thecartercenter/elmo/assignees{/user}","branches_url":"https://api.github.com/repos/thecartercenter/elmo/branches{/branch}","tags_url":"https://api.github.com/repos/thecartercenter/elmo/tags","blobs_url":"https://api.github.com/repos/thecartercenter/elmo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/thecartercenter/elmo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/thecartercenter/elmo/git/refs{/sha}","trees_url":"https://api.github.com/repos/thecartercenter/elmo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/thecartercenter/elmo/statuses/{sha}","languages_url":"https://api.github.com/repos/thecartercenter/elmo/languages","stargazers_url":"https://api.github.com/repos/thecartercenter/elmo/stargazers","contributors_url":"https://api.github.com/repos/thecartercenter/elmo/contributors","subscribers_url":"https://api.github.com/repos/thecartercenter/elmo/subscribers","subscription_url":"https://api.github.com/repos/thecartercenter/elmo/subscription","commits_url":"https://api.github.com/repos/thecartercenter/elmo/commits{/sha}","git_commits_url":"https://api.github.com/repos/thecartercenter/elmo/git/commits{/sha}","comments_url":"https://api.github.com/repos/thecartercenter/elmo/comments{/number}","issue_comment_url":"https://api.github.com/repos/thecartercenter/elmo/issues/comments{/number}","contents_url":"https://api.github.com/repos/thecartercenter/elmo/contents/{+path}","compare_url":"https://api.github.com/repos/thecartercenter/elmo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/thecartercenter/elmo/merges","archive_url":"https://api.github.com/repos/thecartercenter/elmo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/thecartercenter/elmo/downloads","issues_url":"https://api.github.com/repos/thecartercenter/elmo/issues{/number}","pulls_url":"https://api.github.com/repos/thecartercenter/elmo/pulls{/number}","milestones_url":"https://api.github.com/repos/thecartercenter/elmo/milestones{/number}","notifications_url":"https://api.github.com/repos/thecartercenter/elmo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/thecartercenter/elmo/labels{/name}","releases_url":"https://api.github.com/repos/thecartercenter/elmo/releases{/id}","created_at":"2013-07-23T17:56:04Z","updated_at":"2015-08-01T19:23:18Z","pushed_at":"2015-09-04T21:03:20Z","git_url":"git://github.com/thecartercenter/elmo.git","ssh_url":"git@github.com:thecartercenter/elmo.git","clone_url":"https://github.com/thecartercenter/elmo.git","svn_url":"https://github.com/thecartercenter/elmo","homepage":null,"size":41405,"stargazers_count":14,"watchers_count":14,"language":"Ruby","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":16,"mirror_url":null,"open_issues_count":3,"forks":16,"open_issues":3,"watchers":14,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/140"},"html":{"href":"https://github.com/thecartercenter/elmo/pull/140"},"issue":{"href":"https://api.github.com/repos/thecartercenter/elmo/issues/140"},"comments":{"href":"https://api.github.com/repos/thecartercenter/elmo/issues/140/comments"},"review_comments":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/140/comments"},"review_comment":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/thecartercenter/elmo/pulls/140/commits"},"statuses":{"href":"https://api.github.com/repos/thecartercenter/elmo/statuses/bc9e392f88720b21862cd1586ae3e49fc4cdc1e9"}}}}