timesheet-toggl 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/timesheet/toggl/clients.rb +29 -0
- data/lib/timesheet/toggl/parser.rb +129 -0
- data/lib/timesheet/toggl/projects.rb +28 -0
- data/lib/timesheet/toggl/version.rb +2 -2
- data/lib/timesheet/toggl.rb +16 -56
- data/timesheet-toggl-0.0.2.gem +0 -0
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5abbba2899768c7931df772a3807f6e2707861c6
|
4
|
+
data.tar.gz: 1bdf674dc5fa703fd0f0b4fdd441b828dc48ebd6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a870ed228f2c8dda4bc835063d7f4d0ae843f7c2732a213f833df4743da7901700bfd714e28213e345a35ee86fbfbd3fb9489855051bfb4810b30577d890682
|
7
|
+
data.tar.gz: c216d7566349691f264df6427299de3e1137766025552311dc0e2aeec6eec323d715d5bef657c1f6a4da5964163f40e8ee245d23b1c8fd507c522a90e6cdb930
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Timesheet
|
2
|
+
module Clients
|
3
|
+
CLIENTS_URI = 'https://www.toggl.com/api/v8/clients'
|
4
|
+
|
5
|
+
def create_client(name, workspace_id)
|
6
|
+
params = {
|
7
|
+
client: {
|
8
|
+
name: name,
|
9
|
+
wid: workspace_id,
|
10
|
+
}
|
11
|
+
}
|
12
|
+
headers = {}
|
13
|
+
headers['Content-Type']='application/json'
|
14
|
+
headers['X-Requested-With']='XMLHttpRequest'
|
15
|
+
headers['Accept']='application/json'
|
16
|
+
response = Curl::Easy.http_post(CLIENTS_URI, params.to_json) do |request|
|
17
|
+
request.http_auth_types = :basic
|
18
|
+
request.username = config[:api_token]
|
19
|
+
request.password = 'api_token'
|
20
|
+
request.headers = headers
|
21
|
+
end
|
22
|
+
if response.response_code == 200
|
23
|
+
JSON.parse(response.body, symbolize_names: true)[:data][:id]
|
24
|
+
else
|
25
|
+
Rails.logger.error "Client creation failed: #{response.body}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Timesheet
|
2
|
+
# get issue id
|
3
|
+
# issue-related params
|
4
|
+
# get project
|
5
|
+
# split by several issues
|
6
|
+
# if one issue, then all good
|
7
|
+
# if several:
|
8
|
+
# delete time entries from timesheet
|
9
|
+
# delete time entries from kibana
|
10
|
+
# add new time entries to timesheet
|
11
|
+
# @time_part
|
12
|
+
class TogglRecord
|
13
|
+
attr_accessor :record, :config
|
14
|
+
Time.zone = 'UTC' # not to corrupt start_time and end_time
|
15
|
+
|
16
|
+
def initialize(hash, config)
|
17
|
+
@descriptions = parse_description hash[:description]
|
18
|
+
@record = hash
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_description(description)
|
23
|
+
result = description.scan(/(#\s?\d+[^#]+)/).flatten
|
24
|
+
result.size < 2 ? [description] : result
|
25
|
+
end
|
26
|
+
|
27
|
+
def push
|
28
|
+
return unless params = descriptions_params
|
29
|
+
if params.size > 1
|
30
|
+
TimeEntry
|
31
|
+
.where(external_id: record[:id], data_source_id: config[:source_id])
|
32
|
+
.each { |x| x.delete_from_kibana; x.delete }
|
33
|
+
params.each { |x| return unless x[:user_id]; TimeEntry.create x }
|
34
|
+
else
|
35
|
+
params = params.first
|
36
|
+
return unless params[:user_id]
|
37
|
+
te = TimeEntry.find_or_create_by(
|
38
|
+
external_id: record[:id], data_source_id: config[:source_id])
|
39
|
+
te.update params
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def descriptions_params
|
44
|
+
return unless params = derive_params
|
45
|
+
time_proc = proc { |x| x.scan(/@\s?(\d+)/).flatten.first.to_i }
|
46
|
+
times = @descriptions.map(&time_proc).reject(&:zero?)
|
47
|
+
one_part = (times.size / @descriptions.size.to_f) * params[:hours] / times.sum
|
48
|
+
@descriptions.map do |x|
|
49
|
+
time = time_proc.call(x)
|
50
|
+
hours = time.zero? ?
|
51
|
+
(params[:hours] / @descriptions.size) :
|
52
|
+
(one_part * time)
|
53
|
+
iid = issue_id(comment: x)
|
54
|
+
params.merge(comment: x, hours: hours).merge(issue_related_params(iid))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def derive_params
|
59
|
+
params = record.reduce({}) do |r, (k, v)|
|
60
|
+
next(r) unless params_map[k]
|
61
|
+
r.merge(params_map[k] => v)
|
62
|
+
end
|
63
|
+
params[:data_source_id] = config[:source_id]
|
64
|
+
params[:spent_on] = record[:start]
|
65
|
+
params[:user_id] = user_id
|
66
|
+
return unless params[:user_id]
|
67
|
+
params[:hours] /= 3_600_000.0 # turn milliseconds into hours
|
68
|
+
if iid = issue_id(params)
|
69
|
+
params.merge!(issue_related_params(iid))
|
70
|
+
end
|
71
|
+
params[:client_id] ||= client_id
|
72
|
+
params
|
73
|
+
end
|
74
|
+
|
75
|
+
def params_map
|
76
|
+
{
|
77
|
+
id: :external_id,
|
78
|
+
project: :project,
|
79
|
+
description: :comment,
|
80
|
+
dur: :hours,
|
81
|
+
start: :start_time,
|
82
|
+
end: :finish_time
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def issue_id(params)
|
87
|
+
return unless config[:redmine_time_entry_class]
|
88
|
+
params[:comment].match(/#\s?(\d+)/).try(:[], 1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def issue_related_params(issue_id)
|
92
|
+
time_entry_class = Kernel.const_get(config[:redmine_time_entry_class])
|
93
|
+
begin
|
94
|
+
project_id = time_entry_class.issue_class.find(issue_id).project_id
|
95
|
+
rescue
|
96
|
+
return {}
|
97
|
+
end
|
98
|
+
project_company = time_entry_class.project_company(project_id)
|
99
|
+
issue_company = time_entry_class.issue_company(issue_id)
|
100
|
+
company = issue_company.empty? ? project_company : issue_company
|
101
|
+
alert = time_entry_class.alert? project_company, issue_company
|
102
|
+
{
|
103
|
+
project: time_entry_class.project(project_id),
|
104
|
+
task: time_entry_class.task(issue_id),
|
105
|
+
client_id: time_entry_class.client_id(company),
|
106
|
+
project_company: project_company,
|
107
|
+
issue_company: issue_company,
|
108
|
+
alert: alert
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def client_id
|
113
|
+
if client = Client.find_by(name: record[:client])
|
114
|
+
client_id = client.id
|
115
|
+
else
|
116
|
+
Rails.logger.error "No client match to toggl client #{record[:client]}"
|
117
|
+
end
|
118
|
+
client_id
|
119
|
+
end
|
120
|
+
|
121
|
+
def user_id
|
122
|
+
data_source_user = DataSourceUser.find_by(
|
123
|
+
data_source_id: config[:source_id], external_user_id: record[:uid])
|
124
|
+
error = "No user match to toggl user #{record[:user]} (id #{record[:uid]})"
|
125
|
+
(Rails.logger.error(error); return) unless data_source_user
|
126
|
+
data_source_user.user_id
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Timesheet
|
2
|
+
module Projects
|
3
|
+
PROJECTS_URI = 'https://www.toggl.com/api/v8/projects'
|
4
|
+
|
5
|
+
def create_project(name, workspace_id, client_id)
|
6
|
+
params = {
|
7
|
+
project: {
|
8
|
+
name: name,
|
9
|
+
wid: workspace_id,
|
10
|
+
cid: client_id
|
11
|
+
}
|
12
|
+
}
|
13
|
+
headers = {}
|
14
|
+
headers['Content-Type']='application/json'
|
15
|
+
headers['X-Requested-With']='XMLHttpRequest'
|
16
|
+
headers['Accept']='application/json'
|
17
|
+
response = Curl::Easy.http_post(PROJECTS_URI, params.to_json) do |request|
|
18
|
+
request.http_auth_types = :basic
|
19
|
+
request.username = config[:api_token]
|
20
|
+
request.password = 'api_token'
|
21
|
+
request.headers = headers
|
22
|
+
end
|
23
|
+
unless response.response_code == 200
|
24
|
+
Rails.logger.error "Project creation failed: #{response.body}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/timesheet/toggl.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
require_relative 'toggl/version'
|
2
|
+
require_relative 'toggl/clients'
|
3
|
+
require_relative 'toggl/projects'
|
4
|
+
require_relative 'toggl/parser'
|
2
5
|
require 'curb'
|
3
6
|
require 'active_support/core_ext/time'
|
4
7
|
require 'active_support/core_ext/date'
|
@@ -13,6 +16,9 @@ module Timesheet
|
|
13
16
|
# Timesheet::Toggl.new(config).sync(from: (Date.today - 1.month), to: Date.today)
|
14
17
|
#
|
15
18
|
class Toggl
|
19
|
+
include Clients
|
20
|
+
include Projects
|
21
|
+
|
16
22
|
attr_accessor :config
|
17
23
|
|
18
24
|
BASE_URI = 'https://toggl.com/reports/api/v2/details'
|
@@ -44,18 +50,20 @@ module Timesheet
|
|
44
50
|
# until=2013-05-20&
|
45
51
|
# user_agent=api_test"
|
46
52
|
#
|
53
|
+
# @return array of time entries' ids
|
54
|
+
#
|
47
55
|
def synchronize(from, to, user_ids = [])
|
48
56
|
params = {
|
49
57
|
workspace_id: config[:workspace_id],
|
50
58
|
since: from.to_s,
|
51
59
|
until: to.to_s,
|
52
|
-
user_agent: 'export_to_timesheet'
|
53
|
-
user_ids: user_ids.join(',')
|
60
|
+
user_agent: 'export_to_timesheet'
|
54
61
|
}
|
62
|
+
params[:user_ids] = user_ids.join(',') unless user_ids.empty?
|
55
63
|
first_page = fetch(params, 1)
|
56
|
-
push(first_page)
|
64
|
+
te_ids = push(first_page)
|
57
65
|
pages = first_page[:total_count] / first_page[:per_page]
|
58
|
-
pages.times { |page| sync(params, page + 2) }
|
66
|
+
pages.times.map { |page| sync(params, page + 2) }.flatten + te_ids
|
59
67
|
end
|
60
68
|
|
61
69
|
# Since toggl has per-page api, we will follow them.
|
@@ -78,7 +86,7 @@ module Timesheet
|
|
78
86
|
request.password = 'api_token'
|
79
87
|
end
|
80
88
|
parsed = JSON.parse(response.body, symbolize_names: true)
|
81
|
-
fail "Request failed: #{parsed}" unless response.response_code == 200
|
89
|
+
fail "Request failed: #{parsed} with params #{params}" unless response.response_code == 200
|
82
90
|
parsed
|
83
91
|
end
|
84
92
|
|
@@ -87,7 +95,7 @@ module Timesheet
|
|
87
95
|
#
|
88
96
|
def push(parsed_response)
|
89
97
|
TimeEntry.transaction do
|
90
|
-
parsed_response[:data].
|
98
|
+
parsed_response[:data].map { |x| push_record x }
|
91
99
|
end
|
92
100
|
end
|
93
101
|
|
@@ -95,56 +103,8 @@ module Timesheet
|
|
95
103
|
# Don't push time entry if no user set.
|
96
104
|
#
|
97
105
|
def push_record(record)
|
98
|
-
params =
|
99
|
-
|
100
|
-
te = TimeEntry.find_or_create_by(
|
101
|
-
external_id: record[:id], data_source_id: config[:source_id])
|
102
|
-
te.update params
|
103
|
-
end
|
104
|
-
|
105
|
-
def derive_params(record)
|
106
|
-
params = record.reduce({}) do |r, (k, v)|
|
107
|
-
next(r) unless params_map[k]
|
108
|
-
r.merge(params_map[k] => v)
|
109
|
-
end
|
110
|
-
params[:data_source_id] = config[:source_id]
|
111
|
-
params[:spent_on] = record[:start].to_date
|
112
|
-
params[:hours] /= 3_600_000.0 # turn milliseconds into hours
|
113
|
-
if config[:redmine_time_entry_class]
|
114
|
-
if issue_id = params[:comment].match(/\#(\d+)/)[1]
|
115
|
-
params[:client_id] = Kernel.const_get(config[:redmine_time_entry_class])
|
116
|
-
.client_id(issue_id)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
unless params[:client_id]
|
120
|
-
if client = Client.find_by(name: record[:client])
|
121
|
-
params[:client_id] = client.id
|
122
|
-
else
|
123
|
-
Rails.logger.error "No client match to toggl client #{record[:client]}"
|
124
|
-
end
|
125
|
-
end
|
126
|
-
data_source_user = DataSourceUser.find_by(
|
127
|
-
data_source_id: config[:source_id], external_user_id: record[:uid])
|
128
|
-
if data_source_user
|
129
|
-
params[:user_id] = data_source_user.user_id
|
130
|
-
else
|
131
|
-
Rails.logger.error "No user match to toggl user
|
132
|
-
#{record[:user]} (id #{record[:uid]})"
|
133
|
-
end
|
134
|
-
params
|
135
|
-
end
|
136
|
-
|
137
|
-
def params_map
|
138
|
-
{
|
139
|
-
id: :external_id,
|
140
|
-
project: :project,
|
141
|
-
description: :comment,
|
142
|
-
dur: :hours,
|
143
|
-
start: :start_time,
|
144
|
-
end: :finish_time
|
145
|
-
}
|
106
|
+
params = TogglRecord.new(record, config).push
|
107
|
+
record[:id]
|
146
108
|
end
|
147
|
-
|
148
|
-
extend self
|
149
109
|
end
|
150
110
|
end
|
Binary file
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timesheet-toggl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergey Smagin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -79,7 +79,11 @@ files:
|
|
79
79
|
- README.md
|
80
80
|
- Rakefile
|
81
81
|
- lib/timesheet/toggl.rb
|
82
|
+
- lib/timesheet/toggl/clients.rb
|
83
|
+
- lib/timesheet/toggl/parser.rb
|
84
|
+
- lib/timesheet/toggl/projects.rb
|
82
85
|
- lib/timesheet/toggl/version.rb
|
86
|
+
- timesheet-toggl-0.0.2.gem
|
83
87
|
- timesheet-toggl.gemspec
|
84
88
|
homepage: https://github.com/s-mage/timesheet-toggl
|
85
89
|
licenses:
|
@@ -101,9 +105,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
105
|
version: '0'
|
102
106
|
requirements: []
|
103
107
|
rubyforge_project:
|
104
|
-
rubygems_version: 2.
|
108
|
+
rubygems_version: 2.4.5
|
105
109
|
signing_key:
|
106
110
|
specification_version: 4
|
107
111
|
summary: Toggl integration for timesheet
|
108
112
|
test_files: []
|
109
|
-
has_rdoc:
|