{"action":"created","comment":{"url":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments/43232707","id":43232707,"diff_hunk":"@@ -0,0 +1,556 @@\n+# encoding: utf-8\n+\n+require_relative '../../../../../lib/carto/http/client'\n+\n+module CartoDB\n+  module Datasources\n+    module Url\n+      # BoxAPI module is a replacement for Boxr, which requires Ruby >= 2.\n+      # Most of this code has been extracted from Boxr.\n+      # This should be migrated when we upgrade to Ruby 2.\n+      module BoxAPI\n+\n+        class ExpiredTokenError < StandardError; end\n+\n+        def self.oauth_url(state, options = {})\n+          host = options.fetch(:host, \"app.box.com\")\n+          response_type = options.fetch(:response_type, \"code\")\n+          scope = options[:scope]\n+          folder_id = options[:folder_id]\n+          client_id = options[:client_id]\n+\n+          template = Addressable::Template.new(\"https://{host}/api/oauth2/authorize{?query*}\")\n+\n+          query = { \"response_type\" => \"#{response_type}\", \"state\" => \"#{state}\", \"client_id\" => \"#{client_id}\" }\n+          query[\"scope\"] = \"#{scope}\" unless scope.nil?\n+          query[\"folder_id\"] = \"#{folder_id}\" unless folder_id.nil?\n+\n+          template.expand({\"host\" => \"#{host}\", \"query\" => query})\n+        end\n+\n+        def self.get_tokens(code, options = {})\n+          grant_type = options[:grant_type]\n+          assertion = options[:assertion]\n+          scope = options[:scope]\n+          username = options[:username]\n+          client_id = options[:client_id]\n+          client_secret = options[:client_secret]\n+\n+          uri = \"https://api.box.com/oauth2/token\"\n+          body = \"grant_type=#{grant_type}&client_id=#{client_id}&client_secret=#{client_secret}\"\n+          body = body + \"&code=#{code}\" unless code.nil?\n+          body = body + \"&scope=#{scope}\" unless scope.nil?\n+          body = body + \"&username=#{username}\" unless username.nil?\n+          body = body + \"&assertion=#{assertion}\" unless assertion.nil?\n+\n+          auth_post(uri, body)\n+        end\n+\n+        def self.refresh_tokens(refresh_token, options = {})\n+          client_id = options[:client_id]\n+          client_secret = options[:client_secret]\n+\n+          uri = \"https://api.box.com/oauth2/token\"\n+          body = \"grant_type=refresh_token&refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}\"\n+\n+          auth_post(uri, body)\n+        end\n+\n+        def self.auth_post(uri, body, json_body = true)\n+          uri = Addressable::URI.encode(uri)\n+\n+          res = post(uri, body: body)\n+\n+          if res.response_code == 200\n+            json_body ? JSON.parse(res.response_body) : res.response_body\n+          else\n+            handle_error_response(res)\n+          end\n+        end\n+\n+        def self.handle_error_response(res)\n+          body_json = JSON.parse(res.response_body)\n+\n+          if body_json['error'] == 'invalid_grant'\n+            raise ExpiredTokenError.new(body_json.fetch('error_description', 'Expired token'))\n+          else\n+            raise \"Box Error status: #{res.response_code}, body: #{res.response_body}, headers: #{res.response_headers}\"\n+          end\n+        rescue => e\n+          CartoDB.notify_exception(e, self: inspect, response: res.inspect)\n+          raise e\n+        end\n+\n+        def self.post(uri, options = {})\n+          http_client = Carto::Http::Client.get('box',\n+            connecttimeout: 60,\n+            timeout: 600\n+            )\n+          http_client.post(uri.to_s, options)\n+        end\n+\n+        def self.get(uri, options = {})\n+          query = options[:query]\n+          header = options[:header]\n+          follow_redirect = options[:follow_redirect]\n+\n+          http_client = Carto::Http::Client.get('box',\n+            connecttimeout: 60,\n+            timeout: 600)\n+          response = http_client.get(uri.to_s, headers: header, followlocation: follow_redirect, params: query)\n+          response\n+        end\n+\n+      end\n+\n+      module BoxAPI\n+\n+        class Client\n+\n+          API_URI = \"https://api.box.com/2.0\"\n+          SEARCH_URI = \"#{API_URI}/search\"\n+          FILES_URI = \"#{API_URI}/files\"\n+\n+          def initialize(access_token, options = {})\n+            client_id = options[:client_id]\n+            client_secret = options[:client_secret]\n+\n+            @access_token = access_token\n+            raise \"Access token cannot be nil\" if @access_token.nil?\n+            @client_id = client_id\n+            @client_secret = client_secret\n+          end\n+\n+          def search(query, options = {})\n+            scope = options[:scope]\n+            file_extensions = options[:file_extensions]\n+            created_at_range = options[:created_at_range]\n+            updated_at_range = options[:updated_at_range]\n+            size_range = options[:size_range]\n+            owner_user_ids = options[:owner_user_ids]\n+            ancestor_folder_ids = options[:ancestor_folder_ids]\n+            content_types = options[:content_types]\n+            type = options[:type]\n+            limit = options.fetch(:limit, 30)\n+            offset = options.fetch(:offset, 0)\n+\n+            query = { query: query }\n+            query[:scope] = scope unless scope.nil?\n+            query[:file_extensions] = file_extensions unless file_extensions.nil?\n+            query[:created_at_range] = created_at_range unless created_at_range.nil?\n+            query[:updated_at_range] = updated_at_range unless updated_at_range.nil?\n+            query[:size_range] = size_range unless size_range.nil?\n+            query[:owner_user_ids] = owner_user_ids unless owner_user_ids.nil?\n+            query[:ancestor_folder_ids] = ancestor_folder_ids unless ancestor_folder_ids.nil?\n+            query[:content_types] = content_types unless content_types.nil?\n+            query[:type] = type unless type.nil?\n+            query[:limit] = limit unless limit.nil?\n+            query[:offset] = offset unless offset.nil?\n+\n+            results, response = get(SEARCH_URI, query: query)\n+            results['entries']\n+          end\n+\n+          def download_url(file, options = {})\n+            version = options[:version]\n+\n+            download_file(file, version: version, follow_redirect: false)\n+          end\n+\n+          def download_file(file, options = {})\n+            version = options[:version]\n+            follow_redirect = options.fetch(:follow_redirect, true)\n+\n+            file_id = ensure_id(file)\n+            begin\n+              uri = \"#{FILES_URI}/#{file_id}/content\"\n+              query = {}\n+              query[:version] = version unless version.nil?\n+\n+              # Boxr didn't have 200\n+              body_json, response = get(uri, query: query, success_codes: [302,202,200], follow_redirect: false, process_response: false)\n+\n+              if(response.response_code == 302)\n+                location = response.header['Location'][0]\n+\n+                if follow_redirect\n+                  file, response = get(location, process_response: false)\n+                else\n+                  return location #simply return the url\n+                end\n+              elsif response.response_code == 202\n+                retry_after_seconds = response.header['Retry-After'][0]\n+                sleep retry_after_seconds.to_i\n+              elsif response.response_code == 200\n+                file = response.response_body\n+              end\n+            end until file\n+\n+            file\n+          end\n+\n+          FOLDER_AND_FILE_FIELDS = [:type, :id, :sequence_id, :etag, :name, :created_at, :modified_at, :description,\n+                              :size, :path_collection, :created_by, :modified_by, :trashed_at, :purged_at,\n+                              :content_created_at, :content_modified_at, :owned_by, :shared_link, :folder_upload_email,\n+                              :parent, :item_status, :item_collection, :sync_state, :has_collaborations, :permissions, :tags,\n+                              :sha1, :shared_link, :version_number, :comment_count, :lock, :extension, :is_package,\n+                              :expiring_embed_link,  :can_non_owners_invite]\n+          FOLDER_AND_FILE_FIELDS_QUERY = FOLDER_AND_FILE_FIELDS.join(',')\n+\n+          def file_from_id(file_id, fields = [])\n+            file_id = ensure_id(file_id)\n+            uri = \"#{FILES_URI}/#{file_id}\"\n+            query = build_fields_query(fields, FOLDER_AND_FILE_FIELDS_QUERY)\n+            file, response = get(uri, query: query)\n+            file\n+          end\n+\n+          # Required for all providers\n+          DATASOURCE_NAME = 'box'\n+\n+          def revoke_tokens(token)\n+            uri = \"https://api.box.com/oauth2/revoke\"\n+            body = \"client_id=#{@client_id}&client_secret=#{@client_secret}&token=#{token}\"\n+\n+            BoxAPI::auth_post(uri, body, false)\n+          rescue => ex\n+            raise AuthError.new(\"revoke_token: #{ex.message}\", DATASOURCE_NAME)\n+          end\n+\n+          private\n+\n+          def build_fields_query(fields, all_fields_query)\n+            if fields == :all\n+              {:fields => all_fields_query}\n+            elsif fields.is_a?(Array) && fields.length > 0\n+              {:fields => fields.join(',')}\n+            else\n+              {}\n+            end\n+          end\n+\n+          def ensure_id(item)\n+            return item if item.class == String || item.class == Fixnum || item.nil?\n+            return item.id if item.respond_to?(:id)\n+            return item['id'] if item.class == Hash\n+            raise \"Expecting an id of class String or Fixnum, or object that responds to :id\"\n+          end\n+\n+          def get(uri, options = {})\n+            query = options[:query]\n+            success_codes = options.fetch(:success_codes, [200])\n+            process_response = options.fetch(:process_response, true)\n+            if_match = options[:if_match]\n+            box_api_header = options[:box_api_header]\n+            follow_redirect = options.fetch(follow_redirect, true)\n+\n+            headers = standard_headers\n+            headers['If-Match'] = if_match unless if_match.nil?\n+            headers['BoxApi'] = box_api_header unless box_api_header.nil?\n+\n+            res = BoxAPI::get(uri, query: query, header: headers, follow_redirect: follow_redirect)\n+\n+            check_response_status(res, success_codes)\n+\n+            if process_response\n+              return JSON.parse(res.response_body)\n+            else\n+              return res.response_body, res\n+            end\n+          end\n+\n+          def check_response_status(res, success_codes)\n+            raise \"BoxError: status: #{res.response_code}, body: #{res.response_body}, header: #{res.response_headers}\" unless success_codes.include?(res.response_code)\n+          end\n+\n+          def standard_headers\n+            { \"Authorization\" => \"Bearer #{@access_token}\" }\n+          end\n+\n+        end\n+\n+      end\n+\n+      class Box < BaseOAuth\n+\n+        # Required for all providers\n+        DATASOURCE_NAME = 'box'\n+\n+        # Constructor (hidden)\n+        # @param config\n+        # [\n+        #  'application_name'\n+        #  'client_id'\n+        #  'client_secret'\n+        # ]\n+        # @param user ::User\n+        # @throws UninitializedError\n+        # @throws MissingConfigurationError\n+        def initialize(config, user)\n+          super(config, user, %w{ application_name client_id client_secret box_host }, DATASOURCE_NAME)\n+\n+          raise UninitializedError.new('missing user instance', DATASOURCE_NAME)            if user.nil?\n+\n+          @access_token = nil\n+          @refresh_token = nil\n+        end\n+\n+        # Factory method\n+        # @param config {}\n+        # @param user ::User\n+        # @return CartoDB::Datasources::Url::GDrive\n+        def self.get_new(config, user)\n+          new(config, user)\n+        end\n+\n+        # If will provide a url to download the resource, or requires calling get_resource()\n+        # @return bool\n+        def providers_download_url?\n+          false\n+        end\n+\n+        # Return the url to be displayed or sent the user to to authenticate and get authorization code.\n+        # Older implementations had a use_callback_flow parameter that became deprecated. Not implemented.\n+        # @return string | nil\n+        def get_auth_url\n+          service_name = service_name_for_user(DATASOURCE_NAME, @user)\n+          state = CALLBACK_STATE_DATA_PLACEHOLDER.sub('user', @user.username).sub('service', service_name)\n+          BoxAPI::oauth_url(state,\n+                            host: config['box_host'],\n+                            response_type: \"code\",\n+                            scope: nil,\n+                            folder_id: nil,\n+                            client_id: config['client_id']).to_s\n+        end\n+\n+        # Validates authorization code, sets access and refresh tokens for current instance and stores (refresh) token\n+        # @param auth_code : string\n+        # @return string : Refresh token\n+        # Older implementations had a use_callback_flow parameter that became deprecated. Not implemented.\n+        # @throws AuthError\n+        def validate_auth_code(auth_code)\n+          set_tokens(get_tokens(auth_code))\n+          @refresh_token\n+        end\n+\n+        # Validates the authorization callback\n+        # @param params : mixed\n+        def validate_callback(params)\n+          if params[:error].present?\n+            raise AuthError.new(\"validate_callback: #{params[:error]}\", DATASOURCE_NAME)\n+          end\n+\n+          if params[:code]\n+            validate_auth_code(params[:code])\n+          else\n+            raise AuthError.new('validate_callback: Missing authorization code', DATASOURCE_NAME)\n+          end\n+        end\n+\n+        # Store (refresh) token. If it's not valid both access_token and refresh_token will be nil.\n+        # Triggers generation of a valid access token for the lifetime of this instance\n+        # @param token string\n+        # @throws AuthError\n+        def token=(token)\n+          set_tokens(get_fresh_tokens(token))\n+        rescue CartoDB::Datasources::Url::BoxAPI::ExpiredTokenError => e\n+          CartoDB.notify_exception(e, self: inspect, token: token)\n+          set_tokens({ 'access_token' => nil, 'refresh_token' => nil})\n+        end\n+\n+        # Retrieve (refresh) token\n+        # @return string | nil\n+        def token\n+          @refresh_token\n+        end\n+\n+        # Perform the listing and return results\n+        # @param filter Array : (Optional) filter to specify which resources to retrieve. Leave empty for all supported.\n+        # @return [ { :id, :title, :url, :service } ]\n+        # @throws TokenExpiredOrInvalidError\n+        # @throws DataDownloadError\n+        def get_resources_list(filter = [])\n+          self.filter = filter\n+\n+          # Box doesn't have a way to \"retrieve everything\" but it supports whitespaces for multiple search terms\n+          result = client.search(SUPPORTED_EXTENSIONS.join(' '),\n+                 scope: nil,\n+                 file_extensions: nil,\n+                 created_at_range: nil,\n+                 updated_at_range: nil,\n+                 size_range: nil,\n+                 owner_user_ids: nil,\n+                 ancestor_folder_ids: nil,\n+                 content_types: nil,\n+                 type: nil,\n+                 limit: 200,\n+                 offset: 0)\n+\n+          result = result.map { |i| format_item_data(i) }.sort { |x, y| y[:updated_at] <=> x[:updated_at] }\n+\n+          unless @formats.nil? || @formats.empty?\n+            result = result.select { |item|\n+              item[:filename] =~ /.*(#{@formats.join(')|(')})$/i\n+            }\n+          end\n+\n+          result\n+        end\n+\n+        # Retrieves a resource and returns its contents\n+        # @param id string\n+        # @return mixed\n+        # @throws TokenExpiredOrInvalidError\n+        # @throws DataDownloadError\n+        def get_resource(id)\n+          file = client.file_from_id(id)\n+          client.download_file(file)\n+        end\n+\n+        # @param id string\n+        # @return Hash\n+        # @throws TokenExpiredOrInvalidError\n+        # @throws DataDownloadError\n+        # @throws NotFoundDownloadError\n+        def get_resource_metadata(id)\n+          result = client.file_from_id(id)\n+          raise NotFoundDownloadError.new(\"Retrieving file #{id} metadata: #{result.inspect}, should stop syncing\", DATASOURCE_NAME) if result.nil?\n+          raise DataDownloadError.new(\"Retrieving file #{id} metadata: #{result.inspect}\", DATASOURCE_NAME) if result['item_status'] != 'active'\n+          item_data = format_item_data(result)\n+          return item_data\n+        end\n+\n+        # Retrieves current filters\n+        # @return {}\n+        def filter\n+          @formats\n+        end\n+\n+        # Sets current filters\n+        # @param filter_data {}\n+        def filter=(filter_data = [])\n+          @formats = filter_data\n+        end\n+\n+        # Just return datasource name\n+        # @return string\n+        def to_s\n+          DATASOURCE_NAME\n+        end\n+\n+        # If this datasource accepts a data import instance\n+        # @return Boolean\n+        def persists_state_via_data_import?\n+          false\n+        end\n+\n+        # Stores the data import item instance to use/manipulate it\n+        # @param value DataImport\n+        def data_import_item=(value)\n+          nil\n+        end\n+\n+        # Checks if token is still valid or has been revoked\n+        # @return bool\n+        # @throws AuthError\n+        def token_valid?\n+          raise 'invalid_token' unless token\n+\n+          # Any call would do, we just want to see if communicates or refuses the token\n+          result = client.search('test search')\n+          !result.nil?\n+        rescue => e\n+          if e.message =~ /invalid_token/\n+            CartoDB.notify_debug('Box invalid_token', self: inspect)\n+            false\n+          else\n+            CartoDB.notify_exception(e, self: inspect)\n+            raise e\n+          end\n+        end\n+\n+        # Revokes current set token\n+        def revoke_token\n+          client.revoke_tokens(token)\n+        end\n+\n+        # Sets an error reporting component\n+        # @param component mixed\n+        def report_component=(component)\n+          nil\n+        end\n+\n+        private\n+\n+        def set_tokens(tokens)\n+          @access_token = tokens['access_token']\n+          @refresh_token = tokens['refresh_token']\n+        end\n+\n+        def client\n+          @client ||= get_client(@user)\n+        end\n+\n+        def get_client(user)\n+          BoxAPI::Client.new(@access_token,\n+                           client_id: config['client_id'],\n+                           client_secret: config['client_secret'])\n+        end\n+\n+        def get_tokens(code)\n+          BoxAPI::get_tokens(code,\n+                             grant_type: \"authorization_code\",\n+                             assertion: nil,\n+                             scope: nil,\n+                             username: nil,\n+                             client_id: config['client_id'],\n+                             client_secret: config['client_secret'])\n+        end\n+\n+        def get_fresh_tokens(refresh_token)\n+          tokens = BoxAPI::refresh_tokens(refresh_token,\n+                               client_id: config['client_id'],\n+                               client_secret: config['client_secret'])\n+          # Box refresh tokens can only be used once\n+          update_user_oauth(tokens['refresh_token'])\n+          tokens\n+        end\n+\n+        def update_user_oauth(refresh_token)\n+          carto_user = Carto::User.find(@user.id)\n+          oauth = carto_user.oauth_for_service('box')\n+          oauth.token = refresh_token\n+          oauth.save\n+        end\n+\n+        def get_code(user)\n+          url = get_auth_url\n+\n+          http_client = Carto::Http::Client.get('box',\n+            connecttimeout: 60,","path":"services/datasources/lib/datasources/url/box.rb","position":530,"original_position":530,"commit_id":"aca3d79c555aaaa364e193ee4990422417fd2610","original_commit_id":"aca3d79c555aaaa364e193ee4990422417fd2610","user":{"login":"houndci","id":6697940,"avatar_url":"https://avatars.githubusercontent.com/u/6697940?v=3","gravatar_id":"","url":"https://api.github.com/users/houndci","html_url":"https://github.com/houndci","followers_url":"https://api.github.com/users/houndci/followers","following_url":"https://api.github.com/users/houndci/following{/other_user}","gists_url":"https://api.github.com/users/houndci/gists{/gist_id}","starred_url":"https://api.github.com/users/houndci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/houndci/subscriptions","organizations_url":"https://api.github.com/users/houndci/orgs","repos_url":"https://api.github.com/users/houndci/repos","events_url":"https://api.github.com/users/houndci/events{/privacy}","received_events_url":"https://api.github.com/users/houndci/received_events","type":"User","site_admin":false},"body":"Align the parameters of a method call if they span more than one line.","created_at":"2015-10-28T09:30:28Z","updated_at":"2015-10-28T09:30:28Z","html_url":"https://github.com/CartoDB/cartodb/pull/6042#discussion_r43232707","pull_request_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042","_links":{"self":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments/43232707"},"html":{"href":"https://github.com/CartoDB/cartodb/pull/6042#discussion_r43232707"},"pull_request":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042"}}},"pull_request":{"url":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042","id":48979930,"html_url":"https://github.com/CartoDB/cartodb/pull/6042","diff_url":"https://github.com/CartoDB/cartodb/pull/6042.diff","patch_url":"https://github.com/CartoDB/cartodb/pull/6042.patch","issue_url":"https://api.github.com/repos/CartoDB/cartodb/issues/6042","number":6042,"state":"open","locked":false,"title":"5975 box integration","user":{"login":"juanignaciosl","id":932968,"avatar_url":"https://avatars.githubusercontent.com/u/932968?v=3","gravatar_id":"","url":"https://api.github.com/users/juanignaciosl","html_url":"https://github.com/juanignaciosl","followers_url":"https://api.github.com/users/juanignaciosl/followers","following_url":"https://api.github.com/users/juanignaciosl/following{/other_user}","gists_url":"https://api.github.com/users/juanignaciosl/gists{/gist_id}","starred_url":"https://api.github.com/users/juanignaciosl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/juanignaciosl/subscriptions","organizations_url":"https://api.github.com/users/juanignaciosl/orgs","repos_url":"https://api.github.com/users/juanignaciosl/repos","events_url":"https://api.github.com/users/juanignaciosl/events{/privacy}","received_events_url":"https://api.github.com/users/juanignaciosl/received_events","type":"User","site_admin":false},"body":"This closes #5975. Integration code is similar to GDrive and Dropbox ones. Box specifics:\r\n- Refresh token is one use only.\r\n- Search is broader.","created_at":"2015-10-28T09:13:04Z","updated_at":"2015-10-28T09:30:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c7ca63a01d92aa23da0d60dc6654ffff9b7aa33d","assignee":{"login":"juanignaciosl","id":932968,"avatar_url":"https://avatars.githubusercontent.com/u/932968?v=3","gravatar_id":"","url":"https://api.github.com/users/juanignaciosl","html_url":"https://github.com/juanignaciosl","followers_url":"https://api.github.com/users/juanignaciosl/followers","following_url":"https://api.github.com/users/juanignaciosl/following{/other_user}","gists_url":"https://api.github.com/users/juanignaciosl/gists{/gist_id}","starred_url":"https://api.github.com/users/juanignaciosl/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/juanignaciosl/subscriptions","organizations_url":"https://api.github.com/users/juanignaciosl/orgs","repos_url":"https://api.github.com/users/juanignaciosl/repos","events_url":"https://api.github.com/users/juanignaciosl/events{/privacy}","received_events_url":"https://api.github.com/users/juanignaciosl/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042/commits","review_comments_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042/comments","review_comment_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments{/number}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/issues/6042/comments","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/aca3d79c555aaaa364e193ee4990422417fd2610","head":{"label":"CartoDB:5975-Box_integration","ref":"5975-Box_integration","sha":"aca3d79c555aaaa364e193ee4990422417fd2610","user":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"repo":{"id":4909355,"name":"cartodb","full_name":"CartoDB/cartodb","owner":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CartoDB/cartodb","description":"Location Intelligence & Data Visualization tool","fork":false,"url":"https://api.github.com/repos/CartoDB/cartodb","forks_url":"https://api.github.com/repos/CartoDB/cartodb/forks","keys_url":"https://api.github.com/repos/CartoDB/cartodb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CartoDB/cartodb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CartoDB/cartodb/teams","hooks_url":"https://api.github.com/repos/CartoDB/cartodb/hooks","issue_events_url":"https://api.github.com/repos/CartoDB/cartodb/issues/events{/number}","events_url":"https://api.github.com/repos/CartoDB/cartodb/events","assignees_url":"https://api.github.com/repos/CartoDB/cartodb/assignees{/user}","branches_url":"https://api.github.com/repos/CartoDB/cartodb/branches{/branch}","tags_url":"https://api.github.com/repos/CartoDB/cartodb/tags","blobs_url":"https://api.github.com/repos/CartoDB/cartodb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CartoDB/cartodb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CartoDB/cartodb/git/refs{/sha}","trees_url":"https://api.github.com/repos/CartoDB/cartodb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/{sha}","languages_url":"https://api.github.com/repos/CartoDB/cartodb/languages","stargazers_url":"https://api.github.com/repos/CartoDB/cartodb/stargazers","contributors_url":"https://api.github.com/repos/CartoDB/cartodb/contributors","subscribers_url":"https://api.github.com/repos/CartoDB/cartodb/subscribers","subscription_url":"https://api.github.com/repos/CartoDB/cartodb/subscription","commits_url":"https://api.github.com/repos/CartoDB/cartodb/commits{/sha}","git_commits_url":"https://api.github.com/repos/CartoDB/cartodb/git/commits{/sha}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/comments{/number}","issue_comment_url":"https://api.github.com/repos/CartoDB/cartodb/issues/comments{/number}","contents_url":"https://api.github.com/repos/CartoDB/cartodb/contents/{+path}","compare_url":"https://api.github.com/repos/CartoDB/cartodb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CartoDB/cartodb/merges","archive_url":"https://api.github.com/repos/CartoDB/cartodb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CartoDB/cartodb/downloads","issues_url":"https://api.github.com/repos/CartoDB/cartodb/issues{/number}","pulls_url":"https://api.github.com/repos/CartoDB/cartodb/pulls{/number}","milestones_url":"https://api.github.com/repos/CartoDB/cartodb/milestones{/number}","notifications_url":"https://api.github.com/repos/CartoDB/cartodb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CartoDB/cartodb/labels{/name}","releases_url":"https://api.github.com/repos/CartoDB/cartodb/releases{/id}","created_at":"2012-07-05T15:23:45Z","updated_at":"2015-10-28T02:47:13Z","pushed_at":"2015-10-28T09:30:14Z","git_url":"git://github.com/CartoDB/cartodb.git","ssh_url":"git@github.com:CartoDB/cartodb.git","clone_url":"https://github.com/CartoDB/cartodb.git","svn_url":"https://github.com/CartoDB/cartodb","homepage":"http://cartodb.com","size":516762,"stargazers_count":972,"watchers_count":972,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":232,"mirror_url":null,"open_issues_count":816,"forks":232,"open_issues":816,"watchers":972,"default_branch":"master"}},"base":{"label":"CartoDB:master","ref":"master","sha":"964e15f5b97cac88d8b50d65015d10ccf6b58955","user":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"repo":{"id":4909355,"name":"cartodb","full_name":"CartoDB/cartodb","owner":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CartoDB/cartodb","description":"Location Intelligence & Data Visualization tool","fork":false,"url":"https://api.github.com/repos/CartoDB/cartodb","forks_url":"https://api.github.com/repos/CartoDB/cartodb/forks","keys_url":"https://api.github.com/repos/CartoDB/cartodb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CartoDB/cartodb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CartoDB/cartodb/teams","hooks_url":"https://api.github.com/repos/CartoDB/cartodb/hooks","issue_events_url":"https://api.github.com/repos/CartoDB/cartodb/issues/events{/number}","events_url":"https://api.github.com/repos/CartoDB/cartodb/events","assignees_url":"https://api.github.com/repos/CartoDB/cartodb/assignees{/user}","branches_url":"https://api.github.com/repos/CartoDB/cartodb/branches{/branch}","tags_url":"https://api.github.com/repos/CartoDB/cartodb/tags","blobs_url":"https://api.github.com/repos/CartoDB/cartodb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CartoDB/cartodb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CartoDB/cartodb/git/refs{/sha}","trees_url":"https://api.github.com/repos/CartoDB/cartodb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/{sha}","languages_url":"https://api.github.com/repos/CartoDB/cartodb/languages","stargazers_url":"https://api.github.com/repos/CartoDB/cartodb/stargazers","contributors_url":"https://api.github.com/repos/CartoDB/cartodb/contributors","subscribers_url":"https://api.github.com/repos/CartoDB/cartodb/subscribers","subscription_url":"https://api.github.com/repos/CartoDB/cartodb/subscription","commits_url":"https://api.github.com/repos/CartoDB/cartodb/commits{/sha}","git_commits_url":"https://api.github.com/repos/CartoDB/cartodb/git/commits{/sha}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/comments{/number}","issue_comment_url":"https://api.github.com/repos/CartoDB/cartodb/issues/comments{/number}","contents_url":"https://api.github.com/repos/CartoDB/cartodb/contents/{+path}","compare_url":"https://api.github.com/repos/CartoDB/cartodb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CartoDB/cartodb/merges","archive_url":"https://api.github.com/repos/CartoDB/cartodb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CartoDB/cartodb/downloads","issues_url":"https://api.github.com/repos/CartoDB/cartodb/issues{/number}","pulls_url":"https://api.github.com/repos/CartoDB/cartodb/pulls{/number}","milestones_url":"https://api.github.com/repos/CartoDB/cartodb/milestones{/number}","notifications_url":"https://api.github.com/repos/CartoDB/cartodb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CartoDB/cartodb/labels{/name}","releases_url":"https://api.github.com/repos/CartoDB/cartodb/releases{/id}","created_at":"2012-07-05T15:23:45Z","updated_at":"2015-10-28T02:47:13Z","pushed_at":"2015-10-28T09:30:14Z","git_url":"git://github.com/CartoDB/cartodb.git","ssh_url":"git@github.com:CartoDB/cartodb.git","clone_url":"https://github.com/CartoDB/cartodb.git","svn_url":"https://github.com/CartoDB/cartodb","homepage":"http://cartodb.com","size":516762,"stargazers_count":972,"watchers_count":972,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":232,"mirror_url":null,"open_issues_count":816,"forks":232,"open_issues":816,"watchers":972,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042"},"html":{"href":"https://github.com/CartoDB/cartodb/pull/6042"},"issue":{"href":"https://api.github.com/repos/CartoDB/cartodb/issues/6042"},"comments":{"href":"https://api.github.com/repos/CartoDB/cartodb/issues/6042/comments"},"review_comments":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042/comments"},"review_comment":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/6042/commits"},"statuses":{"href":"https://api.github.com/repos/CartoDB/cartodb/statuses/aca3d79c555aaaa364e193ee4990422417fd2610"}}}}