togglv9 0.1.0
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 +7 -0
- data/.gitignore +59 -0
- data/.rspec +1 -0
- data/.rubocop.yml +1156 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +155 -0
- data/Rakefile +6 -0
- data/lib/logging.rb +38 -0
- data/lib/reportsv2.rb +177 -0
- data/lib/togglv9/clients.rb +38 -0
- data/lib/togglv9/connection.rb +105 -0
- data/lib/togglv9/dashboard.rb +32 -0
- data/lib/togglv9/project_users.rb +32 -0
- data/lib/togglv9/projects.rb +114 -0
- data/lib/togglv9/tags.rb +25 -0
- data/lib/togglv9/tasks.rb +40 -0
- data/lib/togglv9/time_entries.rb +114 -0
- data/lib/togglv9/togglv9.rb +43 -0
- data/lib/togglv9/users.rb +74 -0
- data/lib/togglv9/version.rb +4 -0
- data/lib/togglv9/workspaces.rb +43 -0
- data/lib/togglv9.rb +13 -0
- data/scripts/dump.sh +5 -0
- data/spec/lib/reportsv2_spec.rb +254 -0
- data/spec/lib/togglv9/clients_spec.rb +140 -0
- data/spec/lib/togglv9/dashboard_spec.rb +31 -0
- data/spec/lib/togglv9/projects_spec.rb +111 -0
- data/spec/lib/togglv9/tags_spec.rb +54 -0
- data/spec/lib/togglv9/tasks_spec.rb +100 -0
- data/spec/lib/togglv9/time_entries_spec.rb +456 -0
- data/spec/lib/togglv9/users_spec.rb +87 -0
- data/spec/lib/togglv9/workspaces_spec.rb +46 -0
- data/spec/lib/togglv9_spec.rb +88 -0
- data/spec/spec_helper.rb +92 -0
- data/spec/togglv9_spec_helper.rb +75 -0
- data/togglv9.gemspec +34 -0
- metadata +233 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Tomoya Kabe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Toggl API v9
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/togglv9-limitusus)
|
|
4
|
+
|
|
5
|
+
[Toggl](http://www.toggl.com) is a time tracking tool.
|
|
6
|
+
|
|
7
|
+
[togglv9](/) is a Ruby Wrapper for [Toggl API v9](https://engineering.toggl.com/docs/). It is designed to mirror the Toggl API as closely as possible.
|
|
8
|
+
|
|
9
|
+
togglv9 supports both [Toggl API](https://github.com/toggl/toggl_api_docs/blob/master/toggl_api.md) and [Reports API](https://github.com/toggl/toggl_api_docs/blob/master/reports.md)
|
|
10
|
+
|
|
11
|
+
NOTE: currently Reports API is not supported yet.
|
|
12
|
+
|
|
13
|
+
## Change Log
|
|
14
|
+
|
|
15
|
+
See [CHANGELOG](CHANGELOG.md) for a summary of notable changes in each version.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add this line to your application's Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'togglv9'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
And then execute:
|
|
26
|
+
|
|
27
|
+
$ bundle
|
|
28
|
+
|
|
29
|
+
Or install it yourself as:
|
|
30
|
+
|
|
31
|
+
$ gem install togglv9
|
|
32
|
+
|
|
33
|
+
## Initialization
|
|
34
|
+
|
|
35
|
+
### TogglV9::API
|
|
36
|
+
|
|
37
|
+
TogglV9::API communicates with [Toggl API v9](https://engineering.toggl.com/docs/) and can be initialized in one of three ways.
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
TogglV9::API.new # reads API token from file ~/.toggl
|
|
41
|
+
TogglV9::API.new(api_token) # explicit API token
|
|
42
|
+
TogglV9::API.new(email, password) # email & password
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### TogglV9::ReportsV2
|
|
46
|
+
|
|
47
|
+
NOTE: not supported yet. Reports V2 API is already deprecated.
|
|
48
|
+
|
|
49
|
+
TogglV9::ReportsV2 communicates with [Toggl Reports API v2](https://github.com/toggl/toggl_api_docs/blob/master/reports.md) and can be initialized in one of three ways. Toggl.com requires authentication with an API token for Reports API v2.
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
TogglV9::ReportsV2.new # reads API token from file ~/.toggl
|
|
53
|
+
TogglV9::ReportsV2.new(toggl_api_file: toggl_file) # reads API token from toggl_file
|
|
54
|
+
TogglV9::ReportsV2.new(api_token: api_token) # explicit API token
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Note:** `workspace_id` must be set in order to generate reports.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
toggl = TogglV9::API.new
|
|
61
|
+
reports = TogglV9::ReportsV2.new
|
|
62
|
+
reports.workspace_id = toggl.workspaces.first['id']
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
This short example shows one way to create a time entry for the first workspace of the user identified by `<API_TOKEN>`. It then generates various reports containing that time entry.
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require 'togglv9'
|
|
71
|
+
require 'json'
|
|
72
|
+
|
|
73
|
+
toggl_api = TogglV9::API.new(<API_TOKEN>)
|
|
74
|
+
user = toggl_api.me(all=true)
|
|
75
|
+
workspaces = toggl_api.my_workspaces(user)
|
|
76
|
+
workspace_id = workspaces.first['id']
|
|
77
|
+
time_entry = toggl_api.create_time_entry(workspace_id, {
|
|
78
|
+
'description' => "My awesome workspace time entry",
|
|
79
|
+
'wid' => workspace_id,
|
|
80
|
+
'duration' => 1200,
|
|
81
|
+
'start' => toggl_api.iso8601((Time.now - 3600).to_datetime),
|
|
82
|
+
'created_with' => "My awesome Ruby application"
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
reports = TogglV9::ReportsV2.new(api_token: <API_TOKEN>)
|
|
87
|
+
begin
|
|
88
|
+
reports.summary
|
|
89
|
+
rescue Exception => e
|
|
90
|
+
puts e.message # workspace_id is required
|
|
91
|
+
end
|
|
92
|
+
reports.workspace_id = workspace_id
|
|
93
|
+
summary = reports.summary
|
|
94
|
+
puts "Generating summary JSON..."
|
|
95
|
+
puts JSON.pretty_generate(summary)
|
|
96
|
+
puts "Generating summary PDF..."
|
|
97
|
+
reports.write_summary('toggl_summary.pdf')
|
|
98
|
+
puts "Generating weekly CSV..."
|
|
99
|
+
reports.write_weekly('toggl_weekly.csv')
|
|
100
|
+
puts "Generating details XLS..."
|
|
101
|
+
reports.write_details('toggl_details.xls')
|
|
102
|
+
# Note: toggl.com does not generate Weekly XLS report (as of 2016-07-24)
|
|
103
|
+
ensure
|
|
104
|
+
toggl_api.delete_time_entry(time_entry['id'])
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See specs for more examples.
|
|
109
|
+
|
|
110
|
+
**Note:** Requests are rate-limited. The togglv9 gem will handle a 429 response by pausing for 1 second and trying again, for up to 3 attempts. See [Toggl API docs](https://engineering.toggl.com/docs/#generic-responses):
|
|
111
|
+
|
|
112
|
+
> in case of 429 (Too Many Requests) - back off for a few minutes (you can expect a rate of 1req/sec to be available)
|
|
113
|
+
|
|
114
|
+
## Debugging
|
|
115
|
+
|
|
116
|
+
The `TogglV9::API#debug` method determines if debug output is printed to STDOUT. This code snippet demonstrates the debug output.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
require 'togglv9'
|
|
120
|
+
|
|
121
|
+
toggl = TogglV9::API.new
|
|
122
|
+
|
|
123
|
+
toggl.debug(true) # or simply toggl.debug
|
|
124
|
+
user1 = toggl.me
|
|
125
|
+
puts "user: #{user1['fullname']}, debug: true"
|
|
126
|
+
|
|
127
|
+
puts '-'*80
|
|
128
|
+
|
|
129
|
+
toggl.debug(false)
|
|
130
|
+
user2 = toggl.me
|
|
131
|
+
puts "user: #{user2['fullname']}, debug: false"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Documentation
|
|
135
|
+
|
|
136
|
+
Run `rdoc` to generate documentation. Open `doc/index.html` in your browser.
|
|
137
|
+
|
|
138
|
+
## Acknowledgements
|
|
139
|
+
|
|
140
|
+
- Thanks to [kanet77](https://github.com/kanet77) and its contributers, from which this repository code is based.
|
|
141
|
+
- Thanks to the Toggl team for exposing the API.
|
|
142
|
+
|
|
143
|
+
## Contributing
|
|
144
|
+
|
|
145
|
+
1. Fork it ( https://github.com/limitusus/togglv9/fork )
|
|
146
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
147
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
148
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
149
|
+
5. Create a new Pull Request
|
|
150
|
+
|
|
151
|
+
Pull Requests that include tests are **much** more likely to be accepted and merged quickly.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
Copyright (c) 2024 Tomoya Kabe.
|
data/Rakefile
ADDED
data/lib/logging.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# :nocov:
|
|
2
|
+
require 'logger'
|
|
3
|
+
# require 'awesome_print' # for debug output
|
|
4
|
+
|
|
5
|
+
# From http://stackoverflow.com/questions/917566/ruby-share-logger-instance-among-module-classes
|
|
6
|
+
module Logging
|
|
7
|
+
class << self
|
|
8
|
+
def logger
|
|
9
|
+
@logger ||= Logger.new($stdout)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def logger=(logger)
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Addition
|
|
18
|
+
def self.included(base)
|
|
19
|
+
class << base
|
|
20
|
+
def logger
|
|
21
|
+
Logging.logger
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logger
|
|
27
|
+
Logging.logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def debug(debug=true)
|
|
31
|
+
if debug
|
|
32
|
+
logger.level = Logger::DEBUG
|
|
33
|
+
else
|
|
34
|
+
logger.level = Logger::WARN
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
# :nocov:
|
data/lib/reportsv2.rb
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
module TogglV9
|
|
2
|
+
TOGGL_REPORTS_URL = 'https://api.track.toggl.com/reports/api/'
|
|
3
|
+
|
|
4
|
+
class ReportsV2
|
|
5
|
+
include TogglV9::Connection
|
|
6
|
+
|
|
7
|
+
REPORTS_V2_URL = TOGGL_REPORTS_URL + 'v2/'
|
|
8
|
+
|
|
9
|
+
attr_reader :conn
|
|
10
|
+
|
|
11
|
+
attr_accessor :workspace_id
|
|
12
|
+
|
|
13
|
+
def initialize(opts={})
|
|
14
|
+
debug(false)
|
|
15
|
+
|
|
16
|
+
@user_agent = TogglV9::NAME
|
|
17
|
+
|
|
18
|
+
username = opts[:api_token]
|
|
19
|
+
if username.nil?
|
|
20
|
+
toggl_api_file = opts[:toggl_api_file] || File.join(Dir.home, TOGGL_FILE)
|
|
21
|
+
if File.exist?(toggl_api_file) then
|
|
22
|
+
username = IO.read(toggl_api_file)
|
|
23
|
+
else
|
|
24
|
+
raise "Expecting one of:\n" +
|
|
25
|
+
" 1) api_token in file #{toggl_api_file}, or\n" +
|
|
26
|
+
" 2) parameter: (toggl_api_file), or\n" +
|
|
27
|
+
" 3) parameter: (api_token), or\n" +
|
|
28
|
+
"\n\tSee https://github.com/limitusus/togglv9#togglv9reportsv2" +
|
|
29
|
+
"\n\tand https://github.com/toggl/toggl_api_docs/blob/master/reports.md#authentication"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@conn = TogglV9::Connection.open(username, API_TOKEN, REPORTS_V2_URL, opts)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# ---------
|
|
38
|
+
# :section: Report
|
|
39
|
+
#
|
|
40
|
+
# The following parameters and filters can be used in all of the reports
|
|
41
|
+
#
|
|
42
|
+
# user_agent : the name of this application so Toggl can get in touch
|
|
43
|
+
# (string, *required*)
|
|
44
|
+
# workspace_id : The workspace whose data you want to access.
|
|
45
|
+
# (integer, *required*)
|
|
46
|
+
# since : ISO 8601 date (YYYY-MM-DD), by default until - 6 days.
|
|
47
|
+
# (string)
|
|
48
|
+
# until : ISO 8601 date (YYYY-MM-DD), by default today
|
|
49
|
+
# (string)
|
|
50
|
+
# billable : possible values: yes/no/both, default both
|
|
51
|
+
# client_ids : client ids separated by a comma, 0 if you want to filter out time entries without a client
|
|
52
|
+
# project_ids : project ids separated by a comma, 0 if you want to filter out time entries without a project
|
|
53
|
+
# user_ids : user ids separated by a comma
|
|
54
|
+
# members_of_group_ids : group ids separated by a comma. This limits provided user_ids to the provided group members
|
|
55
|
+
# or_members_of_group_ids : group ids separated by a comma. This extends provided user_ids with the provided group members
|
|
56
|
+
# tag_ids : tag ids separated by a comma, 0 if you want to filter out time entries without a tag
|
|
57
|
+
# task_ids : task ids separated by a comma, 0 if you want to filter out time entries without a task
|
|
58
|
+
# time_entry_ids : time entry ids separated by a comma
|
|
59
|
+
# description : time entry description
|
|
60
|
+
# (string)
|
|
61
|
+
# without_description : filters out the time entries which do not have a description ('(no description)')
|
|
62
|
+
# (true/false)
|
|
63
|
+
# order_field : date/description/duration/user in detailed reports
|
|
64
|
+
# title/duration/amount in summary reports
|
|
65
|
+
# title/day1/day2/day3/day4/day5/day6/day7/week_total in weekly report
|
|
66
|
+
# order_desc : on for descending and off for ascending order
|
|
67
|
+
# (on/off)
|
|
68
|
+
# distinct_rates : on/off, default off
|
|
69
|
+
# rounding : on/off, default off, rounds time according to workspace settings
|
|
70
|
+
# display_hours : decimal/minutes, display hours with minutes or as a decimal number, default minutes
|
|
71
|
+
#
|
|
72
|
+
# NB! Maximum date span (until - since) is one year.
|
|
73
|
+
|
|
74
|
+
# extension can be one of ['.pdf', '.csv', '.xls']. Possibly others?
|
|
75
|
+
def report(type, extension, params)
|
|
76
|
+
raise "workspace_id is required" if @workspace_id.nil?
|
|
77
|
+
get "#{type}#{extension}", {
|
|
78
|
+
:'user_agent' => @user_agent,
|
|
79
|
+
:'workspace_id' => @workspace_id,
|
|
80
|
+
}.merge(params)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def weekly(extension='', params={})
|
|
84
|
+
report('weekly', extension, params)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def details(extension='', params={})
|
|
88
|
+
report('details', extension, params)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def summary(extension='', params={})
|
|
92
|
+
report('summary', extension, params)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# ---------
|
|
97
|
+
# :section: Write report to file
|
|
98
|
+
#
|
|
99
|
+
def write_report(filename)
|
|
100
|
+
extension = File.extname(filename)
|
|
101
|
+
report = yield(extension)
|
|
102
|
+
File.open(filename, "wb") do |file|
|
|
103
|
+
file.write(report)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def write_weekly(filename, params={})
|
|
108
|
+
write_report(filename) do |extension|
|
|
109
|
+
weekly(extension, params)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write_details(filename, params={})
|
|
114
|
+
write_report(filename) do |extension|
|
|
115
|
+
details(extension, params)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write_summary(filename, params={})
|
|
120
|
+
write_report(filename) do |extension|
|
|
121
|
+
summary(extension, params)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# ---------
|
|
128
|
+
# :section: Miscellaneous information
|
|
129
|
+
#
|
|
130
|
+
def revision
|
|
131
|
+
get "revision"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def index
|
|
135
|
+
get "index"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def env
|
|
139
|
+
get "env"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
# ---------
|
|
144
|
+
# :section: Project Dashboard
|
|
145
|
+
#
|
|
146
|
+
# Project dashboard returns at-a-glance information for a single project.
|
|
147
|
+
# This feature is only available with Toggl pro.
|
|
148
|
+
#
|
|
149
|
+
# user_agent : email, or other way to contact client application developer
|
|
150
|
+
# (string, *required*)
|
|
151
|
+
# workspace_id : The workspace whose data you want to access
|
|
152
|
+
# (integer, *required*)
|
|
153
|
+
# project_id : The project whose data you want to access
|
|
154
|
+
# (integer, *required*)
|
|
155
|
+
# page : number of 'tasks_page' you want to fetch
|
|
156
|
+
# (integer, optional)
|
|
157
|
+
# order_field string : name/assignee/duration/billable_amount/estimated_seconds
|
|
158
|
+
# order_desc string : on/off, on for descending and off for ascending order
|
|
159
|
+
def project(project_id, params={})
|
|
160
|
+
raise "workspace_id is required" if @workspace_id.nil?
|
|
161
|
+
get "project", {
|
|
162
|
+
:'user_agent' => @user_agent,
|
|
163
|
+
:'workspace_id' => @workspace_id,
|
|
164
|
+
:'project_id' => project_id,
|
|
165
|
+
}.merge(params)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
##
|
|
169
|
+
# ---------
|
|
170
|
+
# :section: Error (for testing)
|
|
171
|
+
#
|
|
172
|
+
# excludes endpoints 'error500' and 'division_by_zero_error'
|
|
173
|
+
def error400
|
|
174
|
+
get "error400"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module TogglV9
|
|
2
|
+
class API
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# ---------
|
|
6
|
+
# :section: Clients
|
|
7
|
+
#
|
|
8
|
+
# name : The name of the client (string, required, unique in workspace)
|
|
9
|
+
# wid : workspace ID, where the client will be used (integer, required)
|
|
10
|
+
# notes : Notes for the client (string, not required)
|
|
11
|
+
# hrate : The hourly rate for this client (float, not required, available only for pro workspaces)
|
|
12
|
+
# cur : The name of the client's currency (string, not required, available only for pro workspaces)
|
|
13
|
+
# at : timestamp that is sent in the response, indicates the time client was last updated
|
|
14
|
+
|
|
15
|
+
def create_client(workspace_id, params)
|
|
16
|
+
requireParams(params, ['name', 'wid'])
|
|
17
|
+
post "workspaces/#{workspace_id}/clients", params
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_client(workspace_id, client_id)
|
|
21
|
+
get "workspaces/#{workspace_id}/clients/#{client_id}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update_client(workspace_id, client_id, params)
|
|
25
|
+
put "workspaces/#{workspace_id}/clients/#{client_id}", params
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete_client(workspace_id, client_id)
|
|
29
|
+
delete "workspaces/#{workspace_id}/clients/#{client_id}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_client_projects(workspace_id, client_id, params={})
|
|
33
|
+
qs = "?clients=#{client_id}"
|
|
34
|
+
active = params.has_key?('active') ? "&active=#{params['active']}" : ""
|
|
35
|
+
get "workspaces/#{workspace_id}/projects#{qs}#{active}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'oj'
|
|
3
|
+
|
|
4
|
+
require_relative '../logging'
|
|
5
|
+
|
|
6
|
+
module TogglV9
|
|
7
|
+
module Connection
|
|
8
|
+
include Logging
|
|
9
|
+
|
|
10
|
+
DELAY_SEC = 1
|
|
11
|
+
MAX_RETRIES = 3
|
|
12
|
+
|
|
13
|
+
API_TOKEN = 'api_token'
|
|
14
|
+
TOGGL_FILE = '.toggl'
|
|
15
|
+
|
|
16
|
+
def self.open(username=nil, password=API_TOKEN, url=nil, opts={})
|
|
17
|
+
raise 'Missing URL' if url.nil?
|
|
18
|
+
|
|
19
|
+
Faraday.new(:url => url, :ssl => {:verify => true}) do |faraday|
|
|
20
|
+
faraday.request :url_encoded
|
|
21
|
+
faraday.response :logger, Logger.new('faraday.log') if opts[:log]
|
|
22
|
+
faraday.adapter Faraday.default_adapter
|
|
23
|
+
faraday.headers = { "Content-Type" => "application/json" }
|
|
24
|
+
faraday.request :authorization, :basic, username, password
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def requireParams(params, fields=[])
|
|
29
|
+
raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash
|
|
30
|
+
return if fields.empty?
|
|
31
|
+
errors = []
|
|
32
|
+
for f in fields
|
|
33
|
+
errors.push("params[#{f}] is required") unless params.has_key?(f)
|
|
34
|
+
end
|
|
35
|
+
raise ArgumentError, errors.join(', ') if !errors.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _call_api(procs)
|
|
39
|
+
# logger.debug(procs[:debug_output].call)
|
|
40
|
+
full_resp = nil
|
|
41
|
+
i = 0
|
|
42
|
+
loop do
|
|
43
|
+
i += 1
|
|
44
|
+
full_resp = procs[:api_call].call
|
|
45
|
+
# logger.ap(full_resp.env, :debug)
|
|
46
|
+
break if full_resp.status != 429 || i >= MAX_RETRIES
|
|
47
|
+
sleep(DELAY_SEC)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
raise full_resp.headers['warning'] if full_resp.headers['warning']
|
|
51
|
+
raise "HTTP Status: #{full_resp.status}" unless full_resp.success?
|
|
52
|
+
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
|
53
|
+
|
|
54
|
+
full_resp
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get(resource, params={})
|
|
58
|
+
query_params = params.map { |k,v| "#{k}=#{v}" }.join('&')
|
|
59
|
+
resource += "?#{query_params}" unless query_params.empty?
|
|
60
|
+
resource.gsub!('+', '%2B')
|
|
61
|
+
full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
|
|
62
|
+
api_call: lambda { self.conn.get(resource) } )
|
|
63
|
+
return {} if full_resp == {}
|
|
64
|
+
begin
|
|
65
|
+
resp = Oj.load(full_resp.body)
|
|
66
|
+
return resp['data'] if resp.respond_to?(:has_key?) && resp.has_key?('data')
|
|
67
|
+
return resp
|
|
68
|
+
rescue Oj::ParseError
|
|
69
|
+
return full_resp.body
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def post(resource, data='')
|
|
74
|
+
resource.gsub!('+', '%2B')
|
|
75
|
+
full_resp = _call_api(debug_output: lambda { "POST #{resource} / #{data}" },
|
|
76
|
+
api_call: lambda { self.conn.post(resource, Oj.dump(data)) } )
|
|
77
|
+
return {} if full_resp == {}
|
|
78
|
+
Oj.load(full_resp.body)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def put(resource, data='')
|
|
82
|
+
resource.gsub!('+', '%2B')
|
|
83
|
+
full_resp = _call_api(debug_output: lambda { "PUT #{resource} / #{data}" },
|
|
84
|
+
api_call: lambda { self.conn.put(resource, Oj.dump(data)) } )
|
|
85
|
+
return {} if full_resp == {}
|
|
86
|
+
Oj.load(full_resp.body)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def patch(resource, data='')
|
|
90
|
+
resource.gsub!('+', '%2B')
|
|
91
|
+
full_resp = _call_api(debug_output: lambda { "PATCH #{resource} / #{data}" },
|
|
92
|
+
api_call: lambda { self.conn.patch(resource, Oj.dump(data)) } )
|
|
93
|
+
return {} if full_resp == {}
|
|
94
|
+
Oj.load(full_resp.body)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def delete(resource)
|
|
98
|
+
resource.gsub!('+', '%2B')
|
|
99
|
+
full_resp = _call_api(debug_output: lambda { "DELETE #{resource}" },
|
|
100
|
+
api_call: lambda { self.conn.delete(resource) } )
|
|
101
|
+
return {} if full_resp == {}
|
|
102
|
+
full_resp.body
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module TogglV9
|
|
2
|
+
class API
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# ---------
|
|
6
|
+
# :section: Dashboard
|
|
7
|
+
#
|
|
8
|
+
# See https://github.com/toggl/toggl_api_docs/blob/master/chapters/dashboard.md
|
|
9
|
+
|
|
10
|
+
def dashboard(workspace_id)
|
|
11
|
+
dashboard = {}
|
|
12
|
+
dashboard['all_activity'] = all_activity(workspace_id)
|
|
13
|
+
dashboard['most_active_user'] = most_active_user(workspace_id)
|
|
14
|
+
dashboard['activity'] = top_activity(workspace_id)
|
|
15
|
+
dashboard
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def all_activity(workspace_id)
|
|
21
|
+
get "workspaces/#{workspace_id}/dashboard/all_activity"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def most_active_user(workspace_id)
|
|
25
|
+
get "workspaces/#{workspace_id}/dashboard/most_active"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def top_activity(workspace_id)
|
|
29
|
+
get "workspaces/#{workspace_id}/dashboard/top_activity"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module TogglV9
|
|
2
|
+
class API
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# ---------
|
|
6
|
+
# :section: Project Users
|
|
7
|
+
#
|
|
8
|
+
# pid : project ID (integer, required)
|
|
9
|
+
# uid : user ID, who is added to the project (integer, required)
|
|
10
|
+
# wid : workspace ID, where the project belongs to (integer, not-required, project's workspace id is used)
|
|
11
|
+
# manager : admin rights for this project (boolean, default false)
|
|
12
|
+
# rate : hourly rate for the project user (float, not-required, only for pro workspaces) in the currency of the project's client or in workspace default currency.
|
|
13
|
+
# at : timestamp that is sent in the response, indicates when the project user was last updated
|
|
14
|
+
# -- Additional fields --
|
|
15
|
+
# fullname : full name of the user, who is added to the project
|
|
16
|
+
|
|
17
|
+
def create_project_user(params)
|
|
18
|
+
requireParams(params, ['pid', 'uid'])
|
|
19
|
+
params[:fields] = "fullname" # for simplicity, always request fullname field
|
|
20
|
+
post "project_users", { 'project_user' => params }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update_project_user(project_user_id, params)
|
|
24
|
+
params[:fields] = "fullname" # for simplicity, always request fullname field
|
|
25
|
+
put "project_users/#{project_user_id}", { 'project_user' => params }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete_project_user(project_user_id)
|
|
29
|
+
delete "project_users/#{project_user_id}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|