toggl_cache 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/HISTORY.md +27 -0
- data/README.md +9 -3
- data/VERSION +1 -1
- data/lib/toggl_api/reports_client.rb +9 -9
- data/lib/toggl_cache/data/report_repository.rb +42 -14
- data/lib/toggl_cache.rb +140 -22
- data/lib/toggl_cli.rb +3 -5
- data/scripts/sync.rb +8 -2
- data/scripts/sync_check_and_fix.rb +19 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/toggl_api/reports_client_spec.rb +21 -17
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5c97e1c35e17dd0e4e74f0194eefb1b9d100199
|
4
|
+
data.tar.gz: c4b6bfe066da7c539256d39dc98c00539795e7ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4f4ce467a89a23278e731de36b6a2e0df90d6164eb742a87e496f9c42667c232d7764df84b0a0c2b90a0ad0780d640fd65a7715c098ca70f29a04864fa34f41
|
7
|
+
data.tar.gz: 167afe2ed80546b63b0007e69dab6bd1d5ce2fd7a49deff03cf975a4578bbe16194c0cfcaf9e1d5b88812cc1ad9714230f5368a9b7ad6930c01fdb387c1f02f1
|
data/Gemfile
CHANGED
data/HISTORY.md
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# History
|
2
2
|
|
3
|
+
## 2017-03-25 - 0.2.0
|
4
|
+
|
5
|
+
Synchronizing deleted records and old changes.
|
6
|
+
|
7
|
+
Approach #1: calculate aggregates over periods and perform a full sync over the period if there is a diff.
|
8
|
+
|
9
|
+
- Pros: more efficient, smarter, also provides a synchronization correctness control.
|
10
|
+
- Cons: harder to implement.
|
11
|
+
|
12
|
+
Approach #2: brute-force, i.e. simply clear the content for a given period and perform a full sync.
|
13
|
+
|
14
|
+
- Pros: simpler to implement.
|
15
|
+
- Cons: synchronization is currently to long.
|
16
|
+
|
17
|
+
Trying approach #2. Optimizing synchronization:
|
18
|
+
|
19
|
+
- [X] processing fetched records once a page has been fetched, not at the end.
|
20
|
+
- [ ] perform requests in parallel.
|
21
|
+
|
22
|
+
The approach #2 is not applicable. Reviewing Toggl API's documentation, the rate limit is about 1 request per second, so parallelizing does not seem to be an option.
|
23
|
+
|
24
|
+
NB: performing a full synchronization leading to about 40K reports takes about 0.65 hour.
|
25
|
+
|
26
|
+
The approach #1 has been implemented. The algorithm first checks each full year. If a difference is detected, it checks each month of the year. For each month with a difference, a sync is done by clearing the cache for the month and fetching the reports.
|
27
|
+
|
28
|
+
The implementation has been done in `TogglCache.sync_check_and_fix`.
|
29
|
+
|
3
30
|
## 2017-03-18 - 0.1.1
|
4
31
|
|
5
32
|
- Changed default period of import to one week from now, instead of one day.
|
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# TogglCache
|
2
2
|
|
3
|
-
Fetches reports data from Toggl API and caches them in a
|
3
|
+
Fetches reports data from Toggl API and caches them in a PostgreSQL database.
|
4
4
|
|
5
5
|
This allows you to build applications performing complex operations on a large number of Toggl content without the Toggl API latency. You may also use it to backup your precious data!
|
6
6
|
|
7
7
|
[![Build Status](https://travis-ci.org/rchampourlier/toggl_cache.svg)](https://travis-ci.org/rchampourlier/toggl_cache)
|
8
|
-
[![Code Climate](https://codeclimate.com/
|
9
|
-
[![Coverage
|
8
|
+
[![Code Climate](https://codeclimate.com/repos/58d7ff3b88ccb7027b000baa/badges/182b308109bf20bd9dbf/gpa.svg)](https://codeclimate.com/repos/58d7ff3b88ccb7027b000baa/feed)
|
9
|
+
[![Test Coverage](https://codeclimate.com/repos/58d7ff3b88ccb7027b000baa/badges/182b308109bf20bd9dbf/coverage.svg)](https://codeclimate.com/repos/58d7ff3b88ccb7027b000baa/coverage)
|
10
10
|
|
11
11
|
## Installation
|
12
12
|
|
@@ -39,6 +39,12 @@ client = TogglCache::Client.new(
|
|
39
39
|
TogglCache.sync_reports(client, 'TOGGL-WORKSPACE-ID')
|
40
40
|
```
|
41
41
|
|
42
|
+
## The CLI
|
43
|
+
|
44
|
+
```
|
45
|
+
ruby lib/toggl_cli.rb [batch|help]
|
46
|
+
```
|
47
|
+
|
42
48
|
## Contributing
|
43
49
|
|
44
50
|
1. Fork it ( https://github.com/rchampourlier/toggl_cache/fork )
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
@@ -10,28 +10,26 @@ module TogglAPI
|
|
10
10
|
API_URL = "https://toggl.com/reports/api/v2"
|
11
11
|
|
12
12
|
# @param params [Hash]: Toggl API params
|
13
|
-
# -
|
14
|
-
# -
|
13
|
+
# - since
|
14
|
+
# - until
|
15
15
|
# - workspace_id
|
16
16
|
# - ... more params available, see Toggl API documentation for details
|
17
17
|
def fetch_reports(params)
|
18
|
+
raise "Must give a block" unless block_given?
|
18
19
|
page = 1
|
19
|
-
|
20
|
+
current_total = 0
|
20
21
|
loop do
|
21
22
|
results_raw = fetch_reports_details_raw(
|
22
23
|
params.merge(page: page)
|
23
24
|
)
|
24
|
-
|
25
|
+
yield(results_raw["data"])
|
25
26
|
|
26
|
-
|
27
|
-
break if
|
27
|
+
current_total += results_raw["data"].count
|
28
|
+
break if current_total == results_raw["total_count"]
|
28
29
|
page += 1
|
29
30
|
end
|
30
|
-
all_results
|
31
31
|
end
|
32
32
|
|
33
|
-
private
|
34
|
-
|
35
33
|
def fetch_reports_details_raw(params)
|
36
34
|
fetch_reports_raw(api_url(:details), params)
|
37
35
|
end
|
@@ -40,6 +38,8 @@ module TogglAPI
|
|
40
38
|
fetch_reports_raw(api_url(:summary), params)
|
41
39
|
end
|
42
40
|
|
41
|
+
private
|
42
|
+
|
43
43
|
# @param url [String]
|
44
44
|
# @param params [Hash]: Toggl API params
|
45
45
|
def fetch_reports_raw(url, params)
|
@@ -5,8 +5,10 @@ require "active_support/inflector"
|
|
5
5
|
module TogglCache
|
6
6
|
module Data
|
7
7
|
|
8
|
-
#
|
9
|
-
#
|
8
|
+
# Repository for Toggl reports.
|
9
|
+
#
|
10
|
+
# TODO: should be used through instances
|
11
|
+
# TODO: #table should be private
|
10
12
|
class ReportRepository
|
11
13
|
|
12
14
|
MAPPED_REPORT_ATTRIBUTES = %w(
|
@@ -22,7 +24,7 @@ module TogglCache
|
|
22
24
|
# It inserts a new issue row with the specified data.
|
23
25
|
# If the issue already exists (unicity key is `id`)
|
24
26
|
# the row is updated instead.
|
25
|
-
def
|
27
|
+
def create_or_update(report)
|
26
28
|
id = report["id"].to_s
|
27
29
|
if exist_with_id?(id)
|
28
30
|
update_where({ id: id }, row(report: report))
|
@@ -31,39 +33,65 @@ module TogglCache
|
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
36
|
+
def find_by_id(id)
|
35
37
|
table.where(id: id).first
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
40
|
+
def exist_with_id?(id)
|
39
41
|
table.where(id: id).count != 0
|
40
42
|
end
|
41
43
|
|
42
|
-
def
|
44
|
+
def delete_where(where_data)
|
43
45
|
table.where(where_data).delete
|
44
46
|
end
|
45
47
|
|
46
|
-
def
|
48
|
+
def update_where(where_data, values)
|
47
49
|
table.where(where_data).update(values)
|
48
50
|
end
|
49
51
|
|
50
|
-
def
|
52
|
+
def first(by: :start)
|
53
|
+
table.order(by).first
|
54
|
+
end
|
55
|
+
|
56
|
+
def first_where(where_data)
|
51
57
|
table.where(where_data).first
|
52
58
|
end
|
53
59
|
|
54
|
-
def
|
60
|
+
def index
|
55
61
|
table.entries
|
56
62
|
end
|
57
63
|
|
58
|
-
def
|
64
|
+
def count
|
59
65
|
table.count
|
60
66
|
end
|
61
67
|
|
62
|
-
|
68
|
+
# Returns reports whose `start` time is within the specified range.
|
69
|
+
#
|
70
|
+
# @param since: [Time]
|
71
|
+
# @param until: [Time]
|
72
|
+
def starting(time_since:, time_until:)
|
73
|
+
table.where("start >= ? AND start <= ?", time_since, time_until).entries
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete_starting(time_since:, time_until:)
|
77
|
+
table.where("start >= ? AND start <= ?", time_since, time_until).delete
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param pid [Integer]
|
81
|
+
# @param tid [Integer] optional
|
82
|
+
def where(project_id:, task_id: nil)
|
83
|
+
where_criteria = { pid: project_id }
|
84
|
+
where_criteria[:tid] = tid if task_id
|
85
|
+
table.where(where_criteria).entries
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def table
|
63
91
|
DB[:toggl_cache_reports]
|
64
92
|
end
|
65
93
|
|
66
|
-
def
|
94
|
+
def row(report:, insert_created_at: false, insert_updated_at: true)
|
67
95
|
new_report = map_report_attributes(report: report)
|
68
96
|
new_report = add_timestamps(
|
69
97
|
report: new_report,
|
@@ -73,7 +101,7 @@ module TogglCache
|
|
73
101
|
new_report
|
74
102
|
end
|
75
103
|
|
76
|
-
def
|
104
|
+
def map_report_attributes(report:)
|
77
105
|
new_report = report.select { |k, _| MAPPED_REPORT_ATTRIBUTES.include?(k) }
|
78
106
|
new_report = new_report.merge(
|
79
107
|
duration: report["dur"] / 1_000,
|
@@ -85,7 +113,7 @@ module TogglCache
|
|
85
113
|
new_report
|
86
114
|
end
|
87
115
|
|
88
|
-
def
|
116
|
+
def add_timestamps(report:, insert_created_at:, insert_updated_at:)
|
89
117
|
new_report = {}.merge(report)
|
90
118
|
new_report["created_at"] = Time.now if insert_created_at
|
91
119
|
new_report["updated_at"] = Time.now if insert_updated_at
|
data/lib/toggl_cache.rb
CHANGED
@@ -20,17 +20,130 @@ module TogglCache
|
|
20
20
|
# The fetched reports either update the already
|
21
21
|
# existing ones, or create new ones.
|
22
22
|
#
|
23
|
-
# @param client [TogglAPI::Client] a configured client
|
24
|
-
# @param workspace_id [String] Toggl workspace ID (mandatory)
|
25
23
|
# @param date_since [Date] Date since when to fetch
|
26
24
|
# the reports.
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
# @param date_until [Date] Date until when to fetch. Defaults to `Time.now`.
|
26
|
+
# @param client [TogglAPI::Client] a configured client
|
27
|
+
def self.sync_reports(
|
28
|
+
date_since: default_date_since,
|
29
|
+
date_until: Time.now,
|
30
|
+
logger: default_logger,
|
31
|
+
client: default_client
|
32
|
+
)
|
33
|
+
logger.info "Syncing reports from #{date_since} to #{date_until}."
|
34
|
+
clear_cache(
|
35
|
+
time_since: Time.parse("#{date_since} 00:00:00Z"),
|
36
|
+
time_until: Time.parse("#{date_until} 23:59:59Z")
|
37
|
+
)
|
38
|
+
fetch_reports(
|
30
39
|
client: client,
|
31
|
-
date_since: date_since
|
40
|
+
date_since: date_since,
|
41
|
+
date_until: date_until
|
42
|
+
) do |reports|
|
43
|
+
process_reports(reports)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Performs a full synchronization check, from the time of the first
|
48
|
+
# report in the cache to now. Proceeds by comparing reports total
|
49
|
+
# duration from Toggl (using the Reports API) and the total contained
|
50
|
+
# in the cache. If a difference is detected, proceeds monthly and
|
51
|
+
# clear and reconstructs the cache for the invovled month.
|
52
|
+
#
|
53
|
+
# TODO: enable detecting a change in project/task level aggregates.
|
54
|
+
def self.sync_check_and_fix(logger: default_logger)
|
55
|
+
reports = TogglCache::Data::ReportRepository.new
|
56
|
+
first_report = reports.first
|
57
|
+
|
58
|
+
year_start = first_report[:start].year
|
59
|
+
year_end = Time.now.year
|
60
|
+
month_start = first_report[:start].month
|
61
|
+
month_end = Time.now.month
|
62
|
+
|
63
|
+
(year_start..year_end).each do |year|
|
64
|
+
year_toggl = TogglCache.toggl_total(year: year)
|
65
|
+
year_cache = TogglCache.cache_total(year: year)
|
66
|
+
if year_toggl == year_cache
|
67
|
+
logger.info "Checked total for #{year}: ✅ (#{year_toggl})"
|
68
|
+
next
|
69
|
+
end
|
70
|
+
logger.info "Checked total for #{year}: ❌ (Toggl: #{year_toggl}, cache: #{year_cache})"
|
71
|
+
(1..12).each do |month|
|
72
|
+
next if year == year_start && month < month_start
|
73
|
+
next if year == year_end && month > month_end
|
74
|
+
month_toggl = TogglCache.toggl_total(year: year, month: month)
|
75
|
+
month_cache = TogglCache.cache_total(year: year, month: month)
|
76
|
+
if month_toggl == month_cache
|
77
|
+
logger.info "Checked total for #{year}/#{month}: ✅ (#{month_toggl})"
|
78
|
+
else
|
79
|
+
logger.info "Checked total for #{year}/#{month}: ❌ (Toggl: #{month_toggl}, cache: #{month_cache})"
|
80
|
+
TogglCache.clear_cache(year: year, month: month)
|
81
|
+
TogglCache.sync_reports_for_month(year: year, month: month)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# An easy-to-use method to sync reports for a given month. Simply
|
88
|
+
# performs a call to `sync_reports`.
|
89
|
+
#
|
90
|
+
# @param year [Integer]
|
91
|
+
# @param month [Integer
|
92
|
+
def self.sync_reports_for_month(year:, month:)
|
93
|
+
date_since = Date.civil(year, month, 1)
|
94
|
+
date_until = Date.civil(year, month, -1)
|
95
|
+
sync_reports(
|
96
|
+
date_since: date_since,
|
97
|
+
date_until: date_until
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Remove TogglCache's reports between the specified dates.
|
102
|
+
def self.clear_cache(time_since:, time_until:)
|
103
|
+
logger.info "Clearing cache from #{date_since} to #{date_until}."
|
104
|
+
reports = Data::ReportRepository.new
|
105
|
+
reports.delete_starting(
|
106
|
+
time_since: time_since,
|
107
|
+
time_until: time_until
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Remove TogglCache's reports for the specified month.
|
112
|
+
def self.clear_cache_for_month(year:, month:, logger: default_logger)
|
113
|
+
date_since = Date.civil(year, month, 1)
|
114
|
+
date_until = Date.civil(year, month, -1)
|
115
|
+
clear_cache(
|
116
|
+
time_since: Time.parse("#{date_since} 00:00:00Z"),
|
117
|
+
time_until: Time.parse("#{date_until} 23:59:59Z")
|
32
118
|
)
|
33
|
-
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the total duration from Toggl (using Reports API)
|
122
|
+
# for the specified year or month.
|
123
|
+
def self.toggl_total(year:, month: nil)
|
124
|
+
reports_client = TogglAPI::ReportsClient.new
|
125
|
+
date_since = month ? Date.civil(year, month, 1) : Date.civil(year, 1, 1)
|
126
|
+
date_until = month ? Date.civil(year, month, -1) : Date.civil(year, 12, -1)
|
127
|
+
total_grand = reports_client.fetch_reports_summary_raw(
|
128
|
+
since: date_since.to_s,
|
129
|
+
until: date_until.to_s,
|
130
|
+
workspace_id: ENV["TOGGL_WORKSPACE_ID"]
|
131
|
+
)["total_grand"]
|
132
|
+
total_grand ? total_grand / 3600 / 1000 : 0
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns the total duration in TogglCache reports
|
136
|
+
# for the specified year or month.
|
137
|
+
def self.cache_total(year:, month: nil)
|
138
|
+
reports = TogglCache::Data::ReportRepository.new
|
139
|
+
date_since = month ? Date.civil(year, month, 1).to_s : Date.civil(year, 1, 1)
|
140
|
+
date_until = month ? Date.civil(year, month, -1).to_s : Date.civil(year, 12, -1)
|
141
|
+
time_since = Time.parse("#{date_since} 00:00:00Z")
|
142
|
+
time_until = Time.parse("#{date_until} 23:59:59Z")
|
143
|
+
reports.starting(
|
144
|
+
time_since: time_since,
|
145
|
+
time_until: time_until
|
146
|
+
).inject(0) { |sum, r| sum + r[:duration] } / 3600
|
34
147
|
end
|
35
148
|
|
36
149
|
# Fetch from Toggl
|
@@ -47,32 +160,37 @@ module TogglCache
|
|
47
160
|
client: default_client,
|
48
161
|
workspace_id: default_workspace_id,
|
49
162
|
date_since:,
|
50
|
-
date_until: Time.now
|
163
|
+
date_until: Time.now,
|
164
|
+
&block
|
51
165
|
)
|
166
|
+
raise "You must give a block to process fetched records" unless block_given?
|
52
167
|
if date_since && date_until.year > date_since.year
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
168
|
+
[
|
169
|
+
[date_since, Date.new(date_since.year, 12, 31)],
|
170
|
+
[Date.new(date_since.year + 1, 1, 1), date_until]
|
171
|
+
].each do |dates|
|
172
|
+
fetch_reports(
|
173
|
+
client: client,
|
174
|
+
workspace_id: workspace_id,
|
175
|
+
date_since: dates.first,
|
176
|
+
date_until: dates.last,
|
177
|
+
&block
|
178
|
+
)
|
179
|
+
end
|
64
180
|
else
|
65
181
|
options = {
|
66
182
|
workspace_id: workspace_id, until: date_until.strftime("%Y-%m-%d")
|
67
183
|
}
|
68
184
|
options[:since] = date_since.strftime("%Y-%m-%d") unless date_since.nil?
|
69
|
-
client.fetch_reports(options)
|
185
|
+
client.fetch_reports(options, &block)
|
70
186
|
end
|
71
187
|
end
|
72
188
|
|
73
|
-
def self.process_reports(reports)
|
189
|
+
def self.process_reports(reports, logger: default_logger)
|
190
|
+
logger.debug "Processing #{reports.count} Toggl reports"
|
191
|
+
repository = Data::ReportRepository.new
|
74
192
|
reports.each do |report|
|
75
|
-
|
193
|
+
repository.create_or_update(report)
|
76
194
|
end
|
77
195
|
end
|
78
196
|
|
data/lib/toggl_cli.rb
CHANGED
@@ -3,7 +3,6 @@ require "thor"
|
|
3
3
|
require "tty-pager"
|
4
4
|
require "tty-progressbar"
|
5
5
|
require "tty-spinner"
|
6
|
-
# require "tty-table"
|
7
6
|
require File.expand_path("../../config/boot", __FILE__)
|
8
7
|
require "toggl_api/client"
|
9
8
|
require "toggl_cache/data/report_repository"
|
@@ -48,7 +47,7 @@ class TogglCLI < Thor
|
|
48
47
|
say("MOVING!!!")
|
49
48
|
end
|
50
49
|
end
|
51
|
-
|
50
|
+
TogglCache::Data::ReportRepository.new.create_or_update(report)
|
52
51
|
end
|
53
52
|
|
54
53
|
private
|
@@ -112,10 +111,9 @@ class TogglCLI < Thor
|
|
112
111
|
end
|
113
112
|
|
114
113
|
def find_reports(project_id, task_id)
|
114
|
+
reports = TogglCache::Data::ReportRepository.new
|
115
115
|
with_spinner("Fetching matching reports...") do
|
116
|
-
|
117
|
-
where_criteria[:tid] = task_id if task_id
|
118
|
-
TogglCache::Data::ReportRepository.table.where(where_criteria).entries
|
116
|
+
reports.where(project_id: project_id&.to_i, task_id: task_id&.to_i)
|
119
117
|
end
|
120
118
|
end
|
121
119
|
|
data/scripts/sync.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Usage:
|
4
4
|
#
|
5
|
-
# - Syncs reports since
|
5
|
+
# - Syncs reports since 31 days::
|
6
6
|
# ruby scripts/sync.rb
|
7
7
|
#
|
8
8
|
# - Sync reports since the specified date (use Ruby-parseable
|
@@ -16,5 +16,11 @@ require 'bundler/setup'
|
|
16
16
|
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
|
17
17
|
require 'config/boot'
|
18
18
|
|
19
|
-
date_since =
|
19
|
+
date_since = (
|
20
|
+
if ARGV[0].nil? || ARGV[0].empty?
|
21
|
+
Time.now - 31 * 24 * 3600
|
22
|
+
else
|
23
|
+
Time.parse(ARGV[0])
|
24
|
+
end
|
25
|
+
)
|
20
26
|
TogglCache.sync_reports(date_since: date_since)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Check that reports stored in TogglCache match report summaries
|
5
|
+
# provided by toggl Reports API.
|
6
|
+
#
|
7
|
+
# Usage:
|
8
|
+
#
|
9
|
+
# ruby scripts/sync_check_and_fix.rb
|
10
|
+
|
11
|
+
# Load dependencies
|
12
|
+
require "rubygems"
|
13
|
+
require "bundler/setup"
|
14
|
+
require "logger"
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift File.expand_path("../..", __FILE__)
|
17
|
+
require "config/boot"
|
18
|
+
|
19
|
+
TogglCache.sync_check_and_fix
|
data/spec/spec_helper.rb
CHANGED
@@ -4,8 +4,13 @@ require "toggl_api/reports_client"
|
|
4
4
|
|
5
5
|
describe TogglAPI::ReportsClient do
|
6
6
|
let(:options) { {} }
|
7
|
-
let(:expected_api_call_url) { "https://
|
8
|
-
|
7
|
+
let(:expected_api_call_url) { "https://toggl.com/reports/api/v2/details?page=1&user_agent=TogglAPI" }
|
8
|
+
let(:expected_headers) do
|
9
|
+
{
|
10
|
+
"Authorization" => "Basic #{["toggl_api_token:api_token"].pack('m').delete("\r\n")}",
|
11
|
+
"Content-Type" => "application/json"
|
12
|
+
}
|
13
|
+
end
|
9
14
|
let(:api_response_status) { 200 }
|
10
15
|
let(:api_response_body) do
|
11
16
|
{
|
@@ -20,24 +25,25 @@ describe TogglAPI::ReportsClient do
|
|
20
25
|
|
21
26
|
before do
|
22
27
|
stub_request(:get, expected_api_call_url)
|
23
|
-
.with(headers:
|
28
|
+
.with(headers: expected_headers)
|
24
29
|
.to_return(status: api_response_status, body: api_response_body, headers: {})
|
25
30
|
end
|
26
31
|
|
27
|
-
describe "#fetch_reports(params)" do
|
32
|
+
describe "#fetch_reports(params, &block)" do
|
28
33
|
|
29
34
|
context "only 1 page" do
|
30
35
|
let(:total_count) { 50 }
|
31
36
|
|
32
37
|
it "fetches the data only once" do
|
33
|
-
described_class.new.fetch_reports(options)
|
38
|
+
described_class.new.fetch_reports(options) {}
|
34
39
|
# It will fail if it fetches several times because
|
35
40
|
# the second request, with page=2, is not mocked.
|
36
41
|
end
|
37
42
|
|
38
|
-
it "
|
39
|
-
results = described_class.new.fetch_reports(options)
|
40
|
-
|
43
|
+
it "passes the received data to the specified block" do
|
44
|
+
results = described_class.new.fetch_reports(options) do |reports|
|
45
|
+
expect(reports.count).to eq(50)
|
46
|
+
end
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
@@ -53,18 +59,16 @@ describe TogglAPI::ReportsClient do
|
|
53
59
|
data: data_page2
|
54
60
|
}.to_json
|
55
61
|
stub_request(:get, expected_api_call_url.gsub('page=1', 'page=2'))
|
56
|
-
.with(headers:
|
62
|
+
.with(headers: expected_headers)
|
57
63
|
.to_return(status: api_response_status, body: api_response_body_page2, headers: {})
|
58
64
|
end
|
59
65
|
|
60
66
|
it "fetches the additional pages" do
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
results = described_class.new.fetch_reports(options)
|
67
|
-
expect(results.inject(0) { |s, i| s + i["id"] }).to eq(50 * 1 + 10 * 2)
|
67
|
+
total_count = 0
|
68
|
+
results = described_class.new.fetch_reports(options) do |reports|
|
69
|
+
total_count += reports.count
|
70
|
+
end
|
71
|
+
expect(total_count).to eq(60)
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
@@ -72,7 +76,7 @@ describe TogglAPI::ReportsClient do
|
|
72
76
|
let(:api_response_status) { 500 }
|
73
77
|
|
74
78
|
it "raises a TogglAPI::Error" do
|
75
|
-
expect { described_class.new.fetch_reports(options) }
|
79
|
+
expect { described_class.new.fetch_reports(options) {} }
|
76
80
|
.to raise_error(TogglAPI::Error)
|
77
81
|
end
|
78
82
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: toggl_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Romain Champourlier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|
@@ -204,6 +204,7 @@ files:
|
|
204
204
|
- lib/toggl_cache/version.rb
|
205
205
|
- lib/toggl_cli.rb
|
206
206
|
- scripts/sync.rb
|
207
|
+
- scripts/sync_check_and_fix.rb
|
207
208
|
- spec/spec_helper.rb
|
208
209
|
- spec/unit/toggl_api/reports_client_spec.rb
|
209
210
|
- toggl_cache.gemspec
|