togglv8-lastobelus 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/togglv8.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'togglv8/version'
2
+
3
+ require_relative 'togglv8/connection'
4
+
5
+ require_relative 'togglv8/togglv8'
6
+ require_relative 'reportsv2'
7
+
8
+ # :mode => :compat will convert symbols to strings
9
+ Oj.default_options = { :mode => :compat }
10
+
11
+ module TogglV8
12
+ NAME = "TogglV8 v#{TogglV8::VERSION}"
13
+ end
@@ -0,0 +1,37 @@
1
+ module TogglV8
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(params)
16
+ requireParams(params, ['name', 'wid'])
17
+ post "clients", { 'client' => params }
18
+ end
19
+
20
+ def get_client(client_id)
21
+ get "clients/#{client_id}"
22
+ end
23
+
24
+ def update_client(client_id, params)
25
+ put "clients/#{client_id}", { 'client' => params }
26
+ end
27
+
28
+ def delete_client(client_id)
29
+ delete "clients/#{client_id}"
30
+ end
31
+
32
+ def get_client_projects(client_id, params={})
33
+ active = params.has_key?('active') ? "?active=#{params['active']}" : ""
34
+ get "clients/#{client_id}/projects#{active}"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,139 @@
1
+ require 'faraday'
2
+ require 'oj'
3
+
4
+ require_relative '../logging'
5
+
6
+ module TogglV8
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.basic_auth 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
+ break if full_resp.status != 429 || i >= MAX_RETRIES
46
+ sleep(DELAY_SEC)
47
+ end
48
+
49
+ raise full_resp.headers['warning'] if full_resp.headers['warning']
50
+ raise "HTTP Status: #{full_resp.status}" unless full_resp.success?
51
+ return {} if full_resp.body.nil? || full_resp.body == 'null'
52
+
53
+ full_resp
54
+ end
55
+
56
+ def get_all(resource, params={})
57
+ resp = get_paginated(resource, params)
58
+ (2..resp[:pages]).reduce(resp[:data]) do |data, page_number|
59
+ data.concat(get(resource, params.merge(page: page_number)))
60
+ end
61
+ end
62
+
63
+ def get_paginated(resource, params={}, page=1)
64
+ query_params = params.map { |k,v| "#{k}=#{v}" }.join('&')
65
+ resource += "?#{query_params}" unless query_params.empty?
66
+ resource.gsub!('+', '%2B')
67
+ full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
68
+ api_call: lambda { self.conn.get(resource) } )
69
+ resp = Oj.load(full_resp.body)
70
+ total_cnt = resp['total_count']
71
+ per_page = resp['per_page']
72
+ pages = total_cnt / per_page
73
+ unless (total_cnt % per_page) == 0
74
+ pages = pages + 1
75
+ end
76
+ {data: resp['data'], pages: pages}
77
+ end
78
+
79
+ def get(resource, params={})
80
+ extension = File.extname(resource)
81
+
82
+ query_params = params.map { |k,v| "#{k}=#{v}" }.join('&')
83
+ resource += "?#{query_params}" unless query_params.empty?
84
+ resource.gsub!('+', '%2B')
85
+
86
+ full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
87
+ api_call: lambda { self.conn.get(resource) } )
88
+ return {} if full_resp == {}
89
+
90
+ # if we know explicitly the response is not json, return it
91
+ return full_resp.body if %w[.pdf .csv .xls].include? extension
92
+
93
+ # expect that implicit route format responses are json
94
+ begin
95
+ resp = Oj.load(full_resp.body)
96
+ return resp['data'] if resp.respond_to?(:has_key?) && resp.has_key?('data')
97
+ return resp
98
+ rescue Oj::ParseError, EncodingError
99
+ # Oj.load now raises EncodingError for responses that are simple strings instead of json (like /revision)
100
+ return full_resp.body
101
+ end
102
+ end
103
+
104
+ def post(resource, data='')
105
+ resource.gsub!('+', '%2B')
106
+ full_resp = _call_api(debug_output: lambda { "POST #{resource} / #{data}" },
107
+ api_call: lambda { self.conn.post(resource, Oj.dump(data)) } )
108
+ return {} if full_resp == {}
109
+ resp = Oj.load(full_resp.body)
110
+ resp['data']
111
+ end
112
+
113
+ def put(resource, data='')
114
+ resource.gsub!('+', '%2B')
115
+ full_resp = _call_api(debug_output: lambda { "PUT #{resource} / #{data}" },
116
+ api_call: lambda { self.conn.put(resource, Oj.dump(data)) } )
117
+ return {} if full_resp == {}
118
+ resp = Oj.load(full_resp.body)
119
+ resp['data']
120
+ end
121
+
122
+ def patch_v9(resource, ops)
123
+ full_resp = _call_api(debug_output: lambda { "PATCH #{resource} / #{ops}" },
124
+ api_call: lambda { self.v9_conn.patch(resource, Oj.dump(ops)) } )
125
+ return {} if full_resp == {}
126
+ Oj.load(full_resp.body)
127
+ end
128
+
129
+ attr_reader :v9_conn
130
+
131
+ def delete(resource)
132
+ resource.gsub!('+', '%2B')
133
+ full_resp = _call_api(debug_output: lambda { "DELETE #{resource}" },
134
+ api_call: lambda { self.conn.delete(resource) } )
135
+ return {} if full_resp == {}
136
+ full_resp.body
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,14 @@
1
+ module TogglV8
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
+ get "dashboard/#{workspace_id}"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ module TogglV8
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
@@ -0,0 +1,112 @@
1
+ module TogglV8
2
+ class API
3
+
4
+ ##
5
+ # ---------
6
+ # :section: Projects
7
+ #
8
+ # See Toggl {Projects}[https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md]
9
+ #
10
+ # name : The name of the project
11
+ # (string, *required*, unique for client and workspace)
12
+ # wid : workspace ID, where the project will be saved
13
+ # (integer, *required*)
14
+ # cid : client ID
15
+ # (integer, not required)
16
+ # active : whether the project is archived or not
17
+ # (boolean, by default true)
18
+ # is_private : whether project is accessible for only project users or for all workspace users
19
+ # (boolean, default true)
20
+ # template : whether the project can be used as a template
21
+ # (boolean, not required)
22
+ # template_id : id of the template project used on current project's creation
23
+ # billable : whether the project is billable or not
24
+ # (boolean, default true, available only for pro workspaces)
25
+ # auto_estimates : whether the estimated hours is calculated based on task estimations or is fixed manually
26
+ # (boolean, default false, not required, premium functionality)
27
+ # estimated_hours : if auto_estimates is true then the sum of task estimations is returned, otherwise user inserted hours
28
+ # (integer, not required, premium functionality)
29
+ # at : timestamp that is sent in the response for PUT, indicates the time task was last updated
30
+ # color : id of the color selected for the project
31
+ # rate : hourly rate of the project
32
+ # (float, not required, premium functionality)
33
+ # created_at : timestamp indicating when the project was created (UTC time), read-only
34
+ # ---------
35
+
36
+ ##
37
+ # :category: Projects
38
+ #
39
+ # Public: Create a new project
40
+ #
41
+ # params - The Hash used to create the project (default: {})
42
+ # :name - The name of the project (string, required, unique for client and workspace)
43
+ # :wid - workspace ID, where the project will be saved (integer, required)
44
+ # :cid - client ID (integer, not required)
45
+ # :active - whether the project is archived or not (boolean, by default true)
46
+ # :is_private - whether project is accessible for only project users or for all workspace users (boolean, default true)
47
+ # :template - whether the project can be used as a template (boolean, not required)
48
+ # :template_id - id of the template project used on current project's creation
49
+ # :billable - whether the project is billable or not (boolean, default true, available only for pro workspaces)
50
+ # :auto_estimates - whether the estimated hours is calculated based on task estimations or is fixed manually (boolean, default false, not required, premium functionality)
51
+ # :estimated_hours - if auto_estimates is true then the sum of task estimations is returned, otherwise user inserted hours (integer, not required, premium functionality)
52
+ # :at - timestamp that is sent in the response for PUT, indicates the time task was last updated
53
+ # :color - id of the color selected for the project
54
+ # :rate - hourly rate of the project (float, not required, premium functionality)
55
+ # :created_at - timestamp indicating when the project was created (UTC time), read-only
56
+ #
57
+ # Examples
58
+ #
59
+ # toggl.create_project({ :name => 'My project', :wid => 1060392 })
60
+ # => {"id"=>10918774,
61
+ # "wid"=>1060392,
62
+ # "name"=>"project5",
63
+ # "billable"=>false,
64
+ # "is_private"=>true,
65
+ # "active"=>true,
66
+ # "template"=>false,
67
+ # "at"=>"2015-08-18T10:03:51+00:00",
68
+ # "color"=>"5",
69
+ # "auto_estimates"=>false}
70
+ #
71
+ # Returns a +Hash+ representing the newly created Project.
72
+ #
73
+ # See Toggl {Create Project}[https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#create-project]
74
+ def create_project(params)
75
+ requireParams(params, ['name', 'wid'])
76
+ post "projects", { 'project' => params }
77
+ end
78
+
79
+ # [Get project data](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#get-project-data)
80
+ def get_project(project_id)
81
+ get "projects/#{project_id}"
82
+ end
83
+
84
+ # [Update project data](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#update-project-data)
85
+ def update_project(project_id, params)
86
+ put "projects/#{project_id}", { 'project' => params }
87
+ end
88
+
89
+ # [Delete a project](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#delete-a-project)
90
+ def delete_project(project_id)
91
+ delete "projects/#{project_id}"
92
+ end
93
+
94
+ # [Get project users](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#get-project-users)
95
+ def get_project_users(project_id)
96
+ get "projects/#{project_id}/project_users"
97
+ end
98
+
99
+ # [Get project tasks](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#get-project-tasks)
100
+ def get_project_tasks(project_id)
101
+ get "projects/#{project_id}/tasks"
102
+ end
103
+
104
+ # [Get workspace projects](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#get-workspace-projects)
105
+
106
+ # [Delete multiple projects](https://github.com/toggl/toggl_api_docs/blob/master/chapters/projects.md#delete-multiple-projects)
107
+ def delete_projects(project_ids)
108
+ return if project_ids.nil?
109
+ delete "projects/#{project_ids.join(',')}"
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,25 @@
1
+ module TogglV8
2
+ class API
3
+
4
+ ##
5
+ # ---------
6
+ # :section: Tags
7
+ #
8
+ # name : The name of the tag (string, required, unique in workspace)
9
+ # wid : workspace ID, where the tag will be used (integer, required)
10
+
11
+ def create_tag(params)
12
+ requireParams(params, ['name', 'wid'])
13
+ post "tags", { 'tag' => params }
14
+ end
15
+
16
+ # ex: update_tag(12345, { :name => "same tame game" })
17
+ def update_tag(tag_id, params)
18
+ put "tags/#{tag_id}", { 'tag' => params }
19
+ end
20
+
21
+ def delete_tag(tag_id)
22
+ delete "tags/#{tag_id}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module TogglV8
2
+ class API
3
+
4
+ ##
5
+ # ---------
6
+ # :section: Tasks
7
+ #
8
+ # NOTE: Tasks are available only for pro workspaces.
9
+ #
10
+ # name : The name of the task (string, required, unique in project)
11
+ # pid : project ID for the task (integer, required)
12
+ # wid : workspace ID, where the task will be saved
13
+ # (integer, project's workspace id is used when not supplied)
14
+ # uid : user ID, to whom the task is assigned to (integer, not required)
15
+ # estimated_seconds : estimated duration of task in seconds (integer, not required)
16
+ # active : whether the task is done or not (boolean, by default true)
17
+ # at : timestamp that is sent in the response for PUT, indicates the time task was last updated
18
+ # -- Additional fields --
19
+ # done_seconds : duration (in seconds) of all the time entries registered for this task
20
+ # uname : full name of the person to whom the task is assigned to
21
+
22
+ def create_task(params)
23
+ requireParams(params, ['name', 'pid'])
24
+ post "tasks", { 'task' => params }
25
+ end
26
+
27
+ def get_task(task_id)
28
+ get "tasks/#{task_id}"
29
+ end
30
+
31
+ # ex: update_task(1894675, { :active => true, :estimated_seconds => 4500, :fields => "done_seconds,uname"})
32
+ def update_task(task_id, params)
33
+ put "tasks/#{task_id}", { 'task' => params }
34
+ end
35
+
36
+ def delete_task(task_id)
37
+ delete "tasks/#{task_id}"
38
+ end
39
+
40
+ # ------------ #
41
+ # Mass Actions #
42
+ # ------------ #
43
+
44
+ def update_tasks(task_ids, params)
45
+ return if task_ids.nil?
46
+ put "tasks/#{task_ids.join(',')}", { 'task' => params }
47
+ end
48
+
49
+ def delete_tasks(task_ids)
50
+ return if task_ids.nil?
51
+ delete "tasks/#{task_ids.join(',')}"
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,114 @@
1
+ module TogglV8
2
+ class API
3
+
4
+ ##
5
+ # ---------
6
+ # :section: Time Entries
7
+ #
8
+ # https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md
9
+ #
10
+ # description : (string, strongly suggested to be used)
11
+ # wid : workspace ID (integer, required if pid or tid not supplied)
12
+ # pid : project ID (integer, not required)
13
+ # tid : task ID (integer, not required)
14
+ # billable : (boolean, not required, default false, available for pro workspaces)
15
+ # start : time entry start time (string, required, ISO 8601 date and time)
16
+ # stop : time entry stop time (string, not required, ISO 8601 date and time)
17
+ # duration : time entry duration in seconds. If the time entry is currently running,
18
+ # the duration attribute contains a negative value,
19
+ # denoting the start of the time entry in seconds since epoch (Jan 1 1970).
20
+ # The correct duration can be calculated as current_time + duration,
21
+ # where current_time is the current time in seconds since epoch. (integer, required)
22
+ # created_with : the name of your client app (string, required)
23
+ # tags : a list of tag names (array of strings, not required)
24
+ # duronly : should Toggl show the start and stop time of this time entry? (boolean, not required)
25
+ # at : timestamp that is sent in the response, indicates the time item was last updated
26
+
27
+ def create_time_entry(params)
28
+ params['created_with'] = TogglV8::NAME unless params.has_key?('created_with')
29
+ requireParams(params, ['start', 'duration', 'created_with'])
30
+ if !params.has_key?('wid') and !params.has_key?('pid') and !params.has_key?('tid') then
31
+ raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required"
32
+ end
33
+ post "time_entries", { 'time_entry' => params }
34
+ end
35
+
36
+ def start_time_entry(params)
37
+ params['created_with'] = TogglV8::NAME unless params.has_key?('created_with')
38
+ if !params.has_key?('wid') and !params.has_key?('pid') and !params.has_key?('tid') then
39
+ raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required"
40
+ end
41
+ post "time_entries/start", { 'time_entry' => params }
42
+ end
43
+
44
+ def stop_time_entry(time_entry_id)
45
+ put "time_entries/#{time_entry_id}/stop", {}
46
+ end
47
+
48
+ def get_time_entry(time_entry_id)
49
+ get "time_entries/#{time_entry_id}"
50
+ end
51
+
52
+ def get_current_time_entry
53
+ get "time_entries/current"
54
+ end
55
+
56
+ def update_time_entry(time_entry_id, params)
57
+ put "time_entries/#{time_entry_id}", { 'time_entry' => params }
58
+ end
59
+
60
+ def delete_time_entry(time_entry_id)
61
+ delete "time_entries/#{time_entry_id}"
62
+ end
63
+
64
+ def iso8601(timestamp)
65
+ return nil if timestamp.nil?
66
+ if timestamp.is_a?(DateTime) or timestamp.is_a?(Date)
67
+ formatted_ts = timestamp.iso8601
68
+ elsif timestamp.is_a?(String)
69
+ formatted_ts = DateTime.parse(timestamp).iso8601
70
+ else
71
+ raise ArgumentError, "Can't convert #{timestamp.class} to ISO-8601 Date/Time"
72
+ end
73
+ return formatted_ts.sub('+00:00', 'Z')
74
+ end
75
+
76
+ def get_time_entries(dates = {})
77
+ start_date = dates[:start_date]
78
+ end_date = dates[:end_date]
79
+ params = []
80
+ params.push("start_date=#{iso8601(start_date)}") unless start_date.nil?
81
+ params.push("end_date=#{iso8601(end_date)}") unless end_date.nil?
82
+ get "time_entries%s" % [params.empty? ? "" : "?#{params.join('&')}"]
83
+ end
84
+
85
+ # Example params: {'tags' =>['billed','productive'], 'tag_action' => 'add'}
86
+ # tag_action can be 'add' or 'remove'
87
+ def update_time_entries_tags(time_entry_ids, params)
88
+ return if time_entry_ids.nil?
89
+ requireParams(params, ['tags', 'tag_action'])
90
+ put "time_entries/#{time_entry_ids.join(',')}", { 'time_entry' => params }
91
+ end
92
+
93
+ # TEMPORARY FIXED version of API issue
94
+ # see https://github.com/toggl/toggl_api_docs/issues/20 for more info
95
+ def update_time_entries_tags_fixed(time_entry_ids, params)
96
+ time_entries = update_time_entries_tags(time_entry_ids, params)
97
+ return time_entries if params['tag_action'] == 'add'
98
+
99
+ time_entries_for_removing_all_tags_ids = []
100
+ [].push(time_entries).flatten.map! do |time_entry|
101
+ unless time_entry['tags'].nil?
102
+ time_entry['tags'] = time_entry['tags'] - params['tags']
103
+ time_entries_for_removing_all_tags_ids << time_entry['id'] if time_entry['tags'].empty?
104
+ end
105
+ time_entry
106
+ end
107
+
108
+ remove_params = {'tags' => []}
109
+ put "time_entries/#{time_entries_for_removing_all_tags_ids.join(',')}", { 'time_entry' => remove_params } unless time_entries_for_removing_all_tags_ids.empty?
110
+
111
+ time_entries
112
+ end
113
+ end
114
+ end