module Reaper

Constants

CONFIG_FILE_PATH
HARVEST_CLIENT_ID
LOCAL_SERVER_PORT
LOGIN_FILE_PATH
VERSION

Public Instance Methods

check_auth() click to toggle source
# File lib/reaper.rb, line 71
def check_auth
  abort('Cannot find cached login info. Please run `reaper login` first.') unless restore_login_info
end
list_projects() click to toggle source
# File lib/reaper.rb, line 129
def list_projects
  check_auth

  puts "Fetching your project list..."
  response = request 'users/me/project_assignments'
  puts "Cannot fetch your project list, please try agian later." unless response

  puts ''

  projects = response['project_assignments']
  # puts JSON.pretty_generate projects
  
  projects = projects.map do |p|
    pcode = p['project']['code']
    pname = p['project']['name']
    desc = "[#{pcode}] #{pname}"
    tasks = p['task_assignments']

    {
      :project_id => p['project']['id'],
      :project_name => pname,
      :project_code => pcode,
      :desc => desc,
      :client => p['client']['name'],
      :tasks => tasks.map do |t|
        {
          'tid' => t['task']['id'],
          'name' => t['task']['name']
        }
      end
    }
  end

  max_chars = (projects.max_by { |p| p[:desc].length })[:desc].length

  clients = projects.group_by { |p| p[:client] }.to_h.sort.to_h
  
  clients.each do |k, v|
    puts k
    puts '-' * max_chars
    v.sort_by! { |p| p[:project_code] }
    v.each do |p|
      puts p[:desc]
    end
    puts ''
  end

  puts "Total: #{projects.size}"

  clients
end
load_config() click to toggle source
# File lib/reaper.rb, line 96
def load_config
  return false if !File.exist? CONFIG_FILE_PATH
  $config = YAML.load File.read(CONFIG_FILE_PATH)
  true
end
openWebpage(link) click to toggle source
# File lib/reaper.rb, line 27
def openWebpage(link)
  system("open", link)
end
request(endpoint) click to toggle source
# File lib/reaper.rb, line 31
def request(endpoint)
  uri = URI("https://api.harvestapp.com/v2/#{endpoint}")
  req = Net::HTTP::Get.new(uri)
  req['Accept'] = "application/json"
  req['Authorization'] = "Bearer #{$token}"
  req['Harvest-Account-ID'] = $acc_id

  begin
    res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http|
      http.request(req)
    }
  rescue
    abort 'Cannot send request. Please check your network connection and try again.'
  end

  if res.kind_of? Net::HTTPSuccess
    JSON.parse res.body
  else
    nil
  end
end
request_delete(endpoint) click to toggle source
# File lib/reaper.rb, line 53
def request_delete(endpoint)
  uri = URI("https://api.harvestapp.com/v2/#{endpoint}")
  req = Net::HTTP::Delete.new(uri)
  req['Accept'] = "application/json"
  req['Content-Type'] = "application/json"
  req['Authorization'] = "Bearer #{$token}"
  req['Harvest-Account-ID'] = $acc_id
  res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http|
    http.request(req)
  }
  
  if res.kind_of? Net::HTTPSuccess
    JSON.parse res.body
  else
    nil
  end
end
restore_login_info() click to toggle source
# File lib/reaper.rb, line 75
def restore_login_info
  return false if !File.exist? LOGIN_FILE_PATH
  login_data = YAML.load File.read(LOGIN_FILE_PATH)
  if login_data[:harvest_login]
    login = login_data[:harvest_login]
    token = login[:token]
    account_id = login[:account_id]
    user_id = login[:user_id]

    if token && !token.empty? && 
      account_id && account_id.is_a?(Integer) && 
      user_id && user_id.is_a?(Integer)
      $token = token
      $acc_id = account_id
      $user_id = user_id
      return true
    end
  end
  false
end
root() click to toggle source
# File lib/reaper.rb, line 249
def root
  File.expand_path '../', File.dirname(__FILE__)
end
show_config(config) click to toggle source
# File lib/reaper.rb, line 102
def show_config(config)
  puts "Reaper Configuration (version #{$config[:version]})"
  puts ''

  title = 'Global Settings'
  rows = []
  rows << ['Daily working hours negative offset', "#{$config[:daily_negative_offset]} hour(s)"]
  rows << ['Daily working hours positive offset', "#{$config[:daily_positive_offset]} hour(s)"]
  table = Terminal::Table.new :title => title, :rows => rows
  puts table

  puts ''

  title = 'Projects & Tasks Settings'
  headers = ['Project', 'Task', 'Percentage']

  rows = []
  $config[:tasks].each do |task|
    desc = "[#{task[:pcode]}] #{task[:pname]} (#{task[:client]})"
    rows << [desc.scan(/.{1,30}/).join("\n"), task[:tname], "#{(task[:percentage] * 100).round}%"]
  end

  table = Terminal::Table.new :title => title, :headings => headers, :rows => rows
  table.style = { :all_separators => true }
  puts table
end
start_config_server(global_settings, projects, added_tasks) click to toggle source
# File lib/reaper.rb, line 181
def start_config_server(global_settings, projects, added_tasks)
  puts 'Launching the configuration page in your browser...'
  
  server = TCPServer.new LOCAL_SERVER_PORT
  
  while session = server.accept
    request = session.gets

    next unless request
    
    if match = request.match(/\/reaper-config\s+/i)
      template_path = File.join root, 'assets/reaper_config_template.html'

      template = File.read(template_path)
        .sub('"{{RPR_GLOBAL_SETTINGS}}"', "JSON.parse('#{global_settings.to_json}')")
        .sub('"{{RPR_PROJECTS}}"', "JSON.parse('#{projects.to_json}')")
        .sub('"{{RPR_ADDED_TASKS}}"', "JSON.parse('#{added_tasks.to_json}')")
        
      session.print "HTTP/1.1 200\r\n"
      session.print "Content-Type: text/html\r\n"
      session.print "\r\n"
      session.print template
    elsif match = request.match(/\/submitTimeEntries\?([\S]+)\s+HTTP/i)
      params = match.captures.first

      raw_config = params.split('&').map { |arg| arg.split '=' }.to_h

      max_index = raw_config.keys.select { |e| e =~ /p\d+/ }
        .map { |e| e[1..-1].to_i }
        .max

      tasks = []
      (max_index + 1).times do |n|
        tasks << {
          :pid => raw_config["p#{n}"].to_i,
          :tid => raw_config["t#{n}"].to_i,
          :percentage => raw_config["pct#{n}"].to_i / 100.0
        }
      end

      tasks.each do |t|
        proj = projects.select { |p| p['pid'] == t[:pid] }.first
        if proj
          task = proj['tasks'].select { |it| it['tid'] == t[:tid] }.first
          t[:pcode] = proj['code']
          t[:pname] = proj['name']
          t[:client] = proj['client']
          t[:tname] = task['name']
        end

        abort 'Something went wrong' unless proj && task
      end
      
      $config = {
        :version => '0.1.0',
        :daily_negative_offset => raw_config['no'].to_i,
        :daily_positive_offset => raw_config['po'].to_i,
        :tasks => tasks
      }
      
      session.close
      break
    end

    session.close
  end
end