Created
December 9, 2016 03:04
-
-
Save vasi/a4e63ec09e1316674c086cb3bbb16aff to your computer and use it in GitHub Desktop.
Import time logs from Toggl to Redmine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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