Skip to content

Instantly share code, notes, and snippets.

@vasi
Created December 9, 2016 03:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vasi/a4e63ec09e1316674c086cb3bbb16aff to your computer and use it in GitHub Desktop.
Save vasi/a4e63ec09e1316674c086cb3bbb16aff to your computer and use it in GitHub Desktop.
Import time logs from Toggl to Redmine
#!/usr/bin/ruby
require 'date'
require 'json'
require 'net/http'
require 'openssl'
require 'optparse'
require 'ostruct'
require 'pp'
def req(url, method = :Get)
uri = URI(url) unless URI === uri
@https ||= {}
http = @https[uri.host] ||= begin
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http
end
req = Net::HTTP.const_get(method).new(uri)
yield req if block_given?
resp = http.request(req)
raise resp.message unless Net::HTTPSuccess === resp
resp.body
end
def toggl(token, path, method: :Get, query: nil)
uri = URI('https://www.toggl.com/' + path)
uri.query = URI.encode_www_form(query) if query
body = req(uri, method) do |req|
req.basic_auth token, 'api_token'
end
JSON.parse(body)
end
def toggl_workspaces(token)
toggl(token, 'api/v8/workspaces')
end
class Entry < Struct.new(:project, :description, :ms)
def hours; (ms.to_f / 1000 / 60 / 60).round(2); end
end
def toggl_entries(token, workspace, date)
entries = []
1.upto(Float::INFINITY) do |page|
resp = toggl token, 'reports/api/v2/details', query: {
'user_agent' => 'toggl2rm',
'workspace_id' => workspace,
'since' => date,
'until' => date,
'page' => page,
}
break if resp['data'].empty?
entries += resp['data'].map do |entry|
Entry.new(entry['project'], entry['description'], entry['dur'])
end
end
entries
end
def rm(host, token, path, data = nil)
body = req(host + path, data ? :Post : :Get) do |req|
req.basic_auth token, 'none'
if data
req['Content-Type'] = 'application/json'
req.body = JSON.dump(data)
end
end
JSON.parse(body)
end
def rm_add_time(host, token, issue, date, hours, comments)
rm(host, token, '/time_entries.json', {
'time_entry' => {
'issue_id' => issue.to_i,
# 'spent_on' => date,
'hours' => hours,
'comments' => comments
}
})
end
def choose_workspace(token)
# For now just pick the first one
toggl_workspaces(token).first['id']
end
def issue_number(entry)
# Look for '#12345'
/#(\d{5,})/.match(entry.description) ? $1.to_i : nil
end
def toggl2rm(toggl_token, rm_url, rm_token, date)
workspace = choose_workspace(toggl_token)
entries = toggl_entries(toggl_token, workspace, date)
entries.each do |entry|
next unless entry.hours > 0
issue = issue_number(entry) or next
puts "Adding %.2f hours for %s" % [entry.hours, issue]
rm_add_time(rm_url, rm_token, issue, date, entry.hours, entry.description)
end
end
options = OpenStruct.new(
:date => Date.today.strftime('%F')
)
parser = OptionParser.new do |opts|
opts.banner = <<EOM
Import Toggl time logs into Redmine
Usage: toggle2rm -r URL -a TOKEN -t TOKEN [2012-01-31]
EOM
opts.separator ''
opts.on('-r', '--redmine-url URL',
'Set the Redmine base URL to use') { |v| options.rm_url = v }
opts.on('-a', '--redmine-token TOKEN',
'Set the Redmine auth token') { |v| options.rm_token = v }
opts.on('-t', '--toggl-token TOKEN',
'Set the Toggl auth token') { |v| options.toggl_token = v }
end
parser.parse!
unless [:rm_url, :rm_token, :toggl_token].all? { |o| options[o] }
puts parser
exit
end
if d = ARGV.shift
Date.strptime(d, '%F') # Check for OK format, will throw on error
options.date = d
end
toggl2rm(options.toggl_token, options.rm_url, options.rm_token, options.date)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment