rimesync 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/rimesync.rb +1010 -0
- data/lib/rimesync/mock_rimesync.rb +425 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c1489e1e1923737e6266ce70528709b63b602fcc
|
4
|
+
data.tar.gz: 47eef1df05562b8642410f7fa95232ab4e30ad1b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 193aa6cee72c28c8b1665e7d9de8f04b6a929a5052ffb0d16771b7643ed9d138f2eb1eb3596a902e625c2ef0905e2b7ed2c89cfb76590210207f53c4eff0d2e5
|
7
|
+
data.tar.gz: b045234b3d165cabf9ac69b68519b37f4de0db08d0d62377d2b57b934adb85f5e2e1d3e2698c1928166ec5d70e1cafb4ae0ab2edb2e15a542f83c71d642b1079
|
data/lib/rimesync.rb
ADDED
@@ -0,0 +1,1010 @@
|
|
1
|
+
# rimesync - Ruby Port of rimesync
|
2
|
+
|
3
|
+
# Allows for interactions with the TimeSync API
|
4
|
+
|
5
|
+
# - authenticate(username, password, auth_type) - Authorizes user with TimeSync
|
6
|
+
# - token_expiration_time - Returns datetime expiration of user authentication
|
7
|
+
# - create_time(time) - Sends time to baseurl (TimeSync)
|
8
|
+
# - update_time(time, uuid) - Updates time by uuid
|
9
|
+
# - create_project(project) - Creates project
|
10
|
+
# - update_project(project, slug) - Updates project by slug
|
11
|
+
# - create_activity(activity) - Creates activity
|
12
|
+
# - update_activity(activity, slug) - Updates activity by slug
|
13
|
+
# - create_user(user) - Creates a user
|
14
|
+
# - update_user(user, username) - Updates user by username
|
15
|
+
# - get_times(query_parameters) - Get times from TimeSync
|
16
|
+
# - get_projects(query_parameters) - Get project information from TimeSync
|
17
|
+
# - get_activities(query_parameters) - Get activity information from TimeSync
|
18
|
+
# - get_users(username) - Get user information from TimeSync
|
19
|
+
|
20
|
+
# Supported TimeSync versions:
|
21
|
+
# v0
|
22
|
+
|
23
|
+
require 'json'
|
24
|
+
require_relative 'rimesync/mock_rimesync'
|
25
|
+
require 'bcrypt'
|
26
|
+
require 'base64'
|
27
|
+
require 'rest-client'
|
28
|
+
|
29
|
+
# workaround for making `''.is_a? Boolean` work
|
30
|
+
module Boolean
|
31
|
+
end
|
32
|
+
class TrueClass
|
33
|
+
include Boolean
|
34
|
+
end
|
35
|
+
class FalseClass
|
36
|
+
include Boolean
|
37
|
+
end
|
38
|
+
|
39
|
+
class TimeSync
|
40
|
+
def initialize(baseurl, token = nil, test = false)
|
41
|
+
@baseurl = if baseurl.end_with? '/'
|
42
|
+
baseurl.chop
|
43
|
+
else
|
44
|
+
baseurl
|
45
|
+
end
|
46
|
+
@user = nil
|
47
|
+
@password = nil
|
48
|
+
@auth_type = nil
|
49
|
+
@token = token
|
50
|
+
@error = 'rimesync error'
|
51
|
+
@test = test
|
52
|
+
|
53
|
+
@valid_get_queries = Array['user', 'project', 'activity',
|
54
|
+
'start', 'end', 'include_revisions',
|
55
|
+
'include_deleted', 'uuid']
|
56
|
+
@required_params = Hash[
|
57
|
+
'time' => %w(duration project user date_worked),
|
58
|
+
'project' => %w(name slugs),
|
59
|
+
'activity' => %w(name slug),
|
60
|
+
'user' => %w(username password)
|
61
|
+
]
|
62
|
+
@optional_params = Hash[
|
63
|
+
'time' => %w(notes issue_uri activities),
|
64
|
+
'project' => %w(uri users default_activity),
|
65
|
+
'activity' => [],
|
66
|
+
'user' => %w(display_name email site_admin site_spectator
|
67
|
+
site_manager meta active)
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
# authenticate(username, password, auth_type)
|
72
|
+
# Authenticate a username and password with TimeSync via a POST request
|
73
|
+
# to the login endpoint. This method will return a list containing a
|
74
|
+
# single ruby hash. If successful, the hash will contain
|
75
|
+
# the token in the form [{'token': 'SOMETOKEN'}]. If an error is returned
|
76
|
+
# the hash will contain the error information.
|
77
|
+
# ``username`` is a string containing the username of the TimeSync user
|
78
|
+
# ``password`` is a string containing the user's password
|
79
|
+
# ``auth_type`` is a string containing the authentication method used by
|
80
|
+
# TimeSync
|
81
|
+
# rubocop:disable MethodLength
|
82
|
+
def authenticate(username: nil, password: nil, auth_type: nil)
|
83
|
+
# Check for correct arguments in method call
|
84
|
+
arg_error_list = Array[]
|
85
|
+
|
86
|
+
arg_error_list.push('username') if username.nil?
|
87
|
+
|
88
|
+
arg_error_list.push('password') if password.nil?
|
89
|
+
|
90
|
+
arg_error_list.push('auth_type') if auth_type.nil?
|
91
|
+
|
92
|
+
unless arg_error_list.empty?
|
93
|
+
return Hash[@error =>
|
94
|
+
format('Missing %s; please add to method call',
|
95
|
+
arg_error_list.join(', '))]
|
96
|
+
end
|
97
|
+
|
98
|
+
arg_error_list.clear
|
99
|
+
|
100
|
+
@user = username
|
101
|
+
@password = password
|
102
|
+
@auth_type = auth_type
|
103
|
+
|
104
|
+
# Create the auth block to send to the login endpoint
|
105
|
+
auth_hash = Hash['auth' => auth].to_json
|
106
|
+
|
107
|
+
# Construct the url with the login endpoint
|
108
|
+
url = format('%s/login', @baseurl)
|
109
|
+
|
110
|
+
# Test mode, set token and return it from the mocked method
|
111
|
+
if @test
|
112
|
+
@token = 'TESTTOKEN'
|
113
|
+
m = MockTimeSync.new
|
114
|
+
return m.authenticate
|
115
|
+
# return mock_rimesync.authenticate
|
116
|
+
end
|
117
|
+
|
118
|
+
# Send the request, then convert the resonse to a ruby hash
|
119
|
+
begin
|
120
|
+
# Success!
|
121
|
+
response = RestClient.post(url, auth_hash, content_type: :json,
|
122
|
+
accept: :json)
|
123
|
+
token_response = response_to_ruby(response.body, response.code)
|
124
|
+
rescue => e
|
125
|
+
err_msg = format('connection to TimeSync failed at baseurl %s - ', @baseurl)
|
126
|
+
err_msg += format('response status was %s', e.http_code)
|
127
|
+
return Hash[@error => err_msg]
|
128
|
+
end
|
129
|
+
|
130
|
+
# If TimeSync returns an error, return the error without setting the
|
131
|
+
# token.
|
132
|
+
# Else set the token to the returned token and return the dict.
|
133
|
+
if token_response.key?('error') || !token_response.key?('token')
|
134
|
+
return token_response
|
135
|
+
else
|
136
|
+
@token = token_response['token']
|
137
|
+
return token_response
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# create_time(time)
|
142
|
+
# Send a time entry to TimeSync via a POST request in a JSON body. This
|
143
|
+
# method will return that body in the form of a list containing a single
|
144
|
+
# ruby hash. The hash will contain a representation of that
|
145
|
+
# JSON body if it was successful or error information if it was not.
|
146
|
+
# ``time`` is a ruby hash containing the time information to send
|
147
|
+
# to TimeSync.
|
148
|
+
def create_time(time)
|
149
|
+
unless time['duration'].is_a? Integer
|
150
|
+
duration = duration_to_seconds(time['duration'])
|
151
|
+
time['duration'] = duration
|
152
|
+
|
153
|
+
# Duration at this point contains an error_msg if it's not an int
|
154
|
+
return duration unless time['duration'].is_a? Integer
|
155
|
+
end
|
156
|
+
|
157
|
+
if time['duration'] < 0
|
158
|
+
return Hash[@error => 'time object: duration cannot be negative']
|
159
|
+
end
|
160
|
+
|
161
|
+
create_or_update(time, nil, 'time', 'times')
|
162
|
+
end
|
163
|
+
|
164
|
+
# update_time(time, uuid)
|
165
|
+
# Send a time entry update to TimeSync via a POST request in a JSON body.
|
166
|
+
# This method will return that body in the form of a list containing a
|
167
|
+
# single ruby hash. The hash will contain a representation
|
168
|
+
# of that updated time object if it was successful or error information
|
169
|
+
# if it was not.
|
170
|
+
# ``time`` is a ruby hash containing the time information to send
|
171
|
+
# to TimeSync.
|
172
|
+
# ``uuid`` contains the uuid for a time entry to update.
|
173
|
+
def update_time(time, uuid)
|
174
|
+
if time.key?('duration')
|
175
|
+
unless time['duration'].is_a? Integer
|
176
|
+
duration = duration_to_seconds(time['duration'])
|
177
|
+
time['duration'] = duration
|
178
|
+
|
179
|
+
# Duration at this point contains an error_msg if not an int
|
180
|
+
return duration unless time['duration'].is_a? Integer
|
181
|
+
end
|
182
|
+
|
183
|
+
if time['duration'] < 0
|
184
|
+
return Hash[@error => 'time object: duration cannot be negative']
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
create_or_update(time, uuid, 'time', 'times', false)
|
189
|
+
end
|
190
|
+
|
191
|
+
# create_project(project)
|
192
|
+
# Post a project to TimeSync via a POST request in a JSON body. This
|
193
|
+
# method will return that body in the form of a list containing a single
|
194
|
+
# ruby hash. The hash will contain a representation of that
|
195
|
+
# JSON body if it was successful or error information if it was not.
|
196
|
+
# ``project`` is a ruby hash containing the project information
|
197
|
+
# to send to TimeSync.
|
198
|
+
def create_project(project)
|
199
|
+
create_or_update(project, nil, 'project', 'projects')
|
200
|
+
end
|
201
|
+
|
202
|
+
# update_project(project, slug)
|
203
|
+
# Send a project update to TimeSync via a POST request in a JSON body.
|
204
|
+
# This method will return that body in the form of a list containing a
|
205
|
+
# single ruby hash. The hash will contain a representation
|
206
|
+
# of that updated project object if it was successful or error
|
207
|
+
# information if it was not.
|
208
|
+
# ``project`` is a ruby hash containing the project information
|
209
|
+
# to send to TimeSync.
|
210
|
+
# ``slug`` contains the slug for a project entry to update.
|
211
|
+
def update_project(project, slug)
|
212
|
+
create_or_update(project, slug, 'project', 'projects',
|
213
|
+
false)
|
214
|
+
end
|
215
|
+
|
216
|
+
# create_activity(activity, slug=nil)
|
217
|
+
# Post an activity to TimeSync via a POST request in a JSON body. This
|
218
|
+
# method will return that body in the form of a list containing a single
|
219
|
+
# ruby hash. The hash will contain a representation of that
|
220
|
+
# JSON body if it was successful or error information if it was not.
|
221
|
+
# ``activity`` is a ruby hash containing the activity information
|
222
|
+
# to send to TimeSync.
|
223
|
+
def create_activity(activity)
|
224
|
+
create_or_update(activity, nil,
|
225
|
+
'activity', 'activities')
|
226
|
+
end
|
227
|
+
|
228
|
+
# update_activity(activity, slug)
|
229
|
+
# Send an activity update to TimeSync via a POST request in a JSON body.
|
230
|
+
# This method will return that body in the form of a list containing a
|
231
|
+
# single ruby hash. The hash will contain a representation
|
232
|
+
# of that updated activity object if it was successful or error
|
233
|
+
# information if it was not.
|
234
|
+
# ``activity`` is a ruby hash containing the project information
|
235
|
+
# to send to TimeSync.
|
236
|
+
# ``slug`` contains the slug for an activity entry to update.
|
237
|
+
def update_activity(activity, slug)
|
238
|
+
create_or_update(activity, slug,
|
239
|
+
'activity', 'activities',
|
240
|
+
false)
|
241
|
+
end
|
242
|
+
|
243
|
+
# create_user(user)
|
244
|
+
# Post a user to TimeSync via a POST request in a JSON body. This
|
245
|
+
# method will return that body in the form of a list containing a single
|
246
|
+
# ruby hash. The hash will contain a representation of that
|
247
|
+
# JSON body if it was successful or error information if it was not.
|
248
|
+
# ``user`` is a ruby hash containing the user information to send
|
249
|
+
# to TimeSync.
|
250
|
+
def create_user(user)
|
251
|
+
ary = %w(site_admin site_manager site_spectator active)
|
252
|
+
ary.each do |perm|
|
253
|
+
if user.key?(perm) && !(user[perm].is_a? Boolean)
|
254
|
+
return Hash[@error =>
|
255
|
+
format('user object: %s must be True or False', perm)]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
hash_user_password(user)
|
260
|
+
create_or_update(user, nil, 'user', 'users')
|
261
|
+
end
|
262
|
+
|
263
|
+
# update_user(user, username)
|
264
|
+
# Send a user update to TimeSync via a POST request in a JSON body.
|
265
|
+
# This method will return that body in the form of a list containing a
|
266
|
+
# single ruby hash. The hash will contain a representation
|
267
|
+
# of that updated user object if it was successful or error
|
268
|
+
# information if it was not.
|
269
|
+
# ``user`` is a ruby hash containing the user information to send
|
270
|
+
# to TimeSync.
|
271
|
+
# ``username`` contains the username for a user to update.
|
272
|
+
def update_user(user, username)
|
273
|
+
ary = %w(site_admin site_manager site_spectator active)
|
274
|
+
ary.each do |perm|
|
275
|
+
if user.key?(perm) && !(user[perm].is_a? Boolean)
|
276
|
+
return Hash[@error =>
|
277
|
+
format('user object: %s must be True or False', perm)]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
hash_user_password(user)
|
282
|
+
create_or_update(user, username, 'user', 'users', false)
|
283
|
+
end
|
284
|
+
|
285
|
+
# get_times(query_parameters)
|
286
|
+
# Request time entries filtered by parameters passed in
|
287
|
+
# ``query_parameters``. Returns a list of ruby objects representing the
|
288
|
+
# JSON time information returned by TimeSync or an error message if
|
289
|
+
# unsuccessful.
|
290
|
+
# ``query_parameters`` is a ruby hash containing the optional
|
291
|
+
# query parameters described in the TimeSync documentation. If
|
292
|
+
# ``query_parameters`` is empty or nil, ``get_times`` will return all
|
293
|
+
# times in the database. The syntax for each argument is
|
294
|
+
# ``{'query': ['parameter']}``.
|
295
|
+
def get_times(query_parameters = nil)
|
296
|
+
# Check that user has authenticated
|
297
|
+
@local_auth_error = local_auth_error
|
298
|
+
return [Hash[@error => @local_auth_error]] if @local_auth_error
|
299
|
+
|
300
|
+
# Check for key error
|
301
|
+
unless query_parameters.nil?
|
302
|
+
query_parameters.each do |key, _value|
|
303
|
+
unless @valid_get_queries.include?(key)
|
304
|
+
return [Hash[@error =>
|
305
|
+
format('invalid query: %s', key)]]
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Initialize the query string
|
311
|
+
query_string = ''
|
312
|
+
|
313
|
+
# If there are filtering parameters, construct them correctly.
|
314
|
+
# Else reinitialize the query string to a ? so we can add the token.
|
315
|
+
query_string = if query_parameters.nil?
|
316
|
+
'?'
|
317
|
+
else
|
318
|
+
construct_filter_query(query_parameters)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Construct query url, at this point query_string ends with a ?
|
322
|
+
url = format('%s/times%stoken=%s', @baseurl, query_string, @token)
|
323
|
+
|
324
|
+
# Test mode, return one or many objects depending on if uuid is passed
|
325
|
+
if @test
|
326
|
+
m = MockTimeSync.new
|
327
|
+
if !query_parameters.nil? && query_parameters.key?('uuid')
|
328
|
+
return m.get_times(query_parameters['uuid'])
|
329
|
+
else
|
330
|
+
return m.get_times(nil)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Attempt to GET times, then convert the response to a ruby
|
335
|
+
# hash. Always returns a list.
|
336
|
+
begin
|
337
|
+
# Success!
|
338
|
+
response = RestClient.get url
|
339
|
+
res_dict = response_to_ruby(response.body, response.code)
|
340
|
+
return (res_dict.is_a?(Array) ? res_dict : [res_dict])
|
341
|
+
rescue => e
|
342
|
+
# Request Error
|
343
|
+
return [Hash[@error => e]]
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# get_projects(query_parameters)
|
348
|
+
# Request project information filtered by parameters passed to
|
349
|
+
# ``query_parameters``. Returns a list of ruby objects representing the
|
350
|
+
# JSON project information returned by TimeSync or an error message if
|
351
|
+
# unsuccessful.
|
352
|
+
# ``query_parameters`` is a ruby dict containing the optional query
|
353
|
+
# parameters described in the TimeSync documentation. If
|
354
|
+
# ``query_parameters`` is empty or nil, ``get_projects`` will return
|
355
|
+
# all projects in the database. The syntax for each argument is
|
356
|
+
# ``{'query': 'parameter'}`` or ``{'bool_query': <boolean>}``.
|
357
|
+
# Optional parameters:
|
358
|
+
# 'slug': '<slug>'
|
359
|
+
# 'include_deleted': <boolean>
|
360
|
+
# 'revisions': <boolean>
|
361
|
+
# Does not accept a slug combined with include_deleted, but does accept
|
362
|
+
# any other combination.
|
363
|
+
def get_projects(query_parameters = nil)
|
364
|
+
# Check that user has authenticated
|
365
|
+
@local_auth_error = local_auth_error
|
366
|
+
return [Hash[@error => @local_auth_error]] if @local_auth_error
|
367
|
+
|
368
|
+
# Save for passing to test mode since format_endpoints deletes
|
369
|
+
# kwargs['slug'] if it exists
|
370
|
+
slug = if !query_parameters.to_s.empty? && query_parameters.key?('slug')
|
371
|
+
query_parameters['slug']
|
372
|
+
end
|
373
|
+
|
374
|
+
query_string = ''
|
375
|
+
|
376
|
+
# If kwargs exist, create a correct query string
|
377
|
+
# Else, prepare query_string for the token
|
378
|
+
if !query_parameters.to_s.empty?
|
379
|
+
query_string = format_endpoints(query_parameters)
|
380
|
+
# If format_endpoints returns nil, it was passed both slug and
|
381
|
+
# include_deleted, which is not allowed by the TimeSync API
|
382
|
+
if query_string.nil?
|
383
|
+
error_message = 'invalid combination: slug and include_deleted'
|
384
|
+
return [Hash[@error => error_message]]
|
385
|
+
end
|
386
|
+
else
|
387
|
+
query_string = format('?token=%s', @token)
|
388
|
+
end
|
389
|
+
|
390
|
+
# Construct query url - at this point query_string ends with
|
391
|
+
# ?token=token
|
392
|
+
url = format('%s/projects%s', @baseurl, query_string)
|
393
|
+
|
394
|
+
# Test mode, return list of projects if slug is nil, or a single
|
395
|
+
# project
|
396
|
+
if @test
|
397
|
+
m = MockTimeSync.new
|
398
|
+
return m.get_projects(slug)
|
399
|
+
end
|
400
|
+
|
401
|
+
# Attempt to GET projects, then convert the response to a ruby
|
402
|
+
# hash. Always returns a list.
|
403
|
+
begin
|
404
|
+
# Success!
|
405
|
+
response = RestClient.get url
|
406
|
+
res_dict = response_to_ruby(response.body, response.code)
|
407
|
+
return (res_dict.is_a?(Array) ? res_dict : [res_dict])
|
408
|
+
rescue => e
|
409
|
+
# Request Error
|
410
|
+
return [Hash[@error => e]]
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# get_activities(query_parameters)
|
415
|
+
# Request activity information filtered by parameters passed to
|
416
|
+
# ``query_parameters``. Returns a list of ruby objects representing
|
417
|
+
# the JSON activity information returned by TimeSync or an error message
|
418
|
+
# if unsuccessful.
|
419
|
+
# ``query_parameters`` is a hash containing the optional query
|
420
|
+
# parameters described in the TimeSync documentation. If
|
421
|
+
# ``query_parameters`` is empty or nil, ``get_activities`` will
|
422
|
+
# return all activities in the database. The syntax for each argument is
|
423
|
+
# ``{'query': 'parameter'}`` or ``{'bool_query': <boolean>}``.
|
424
|
+
# Optional parameters:
|
425
|
+
# 'slug': '<slug>'
|
426
|
+
# 'include_deleted': <boolean>
|
427
|
+
# 'revisions': <boolean>
|
428
|
+
# Does not accept a slug combined with include_deleted, but does accept
|
429
|
+
# any other combination.
|
430
|
+
def get_activities(query_parameters = nil)
|
431
|
+
# Check that user has authenticated
|
432
|
+
@local_auth_error = local_auth_error
|
433
|
+
return [Hash[@error => @local_auth_error]] if @local_auth_error
|
434
|
+
|
435
|
+
# Save for passing to test mode since format_endpoints deletes
|
436
|
+
# kwargs['slug'] if it exists
|
437
|
+
slug = if !query_parameters.to_s.empty? && query_parameters.key?('slug')
|
438
|
+
query_parameters['slug']
|
439
|
+
end
|
440
|
+
|
441
|
+
query_string = ''
|
442
|
+
|
443
|
+
# If kwargs exist, create a correct query string
|
444
|
+
# Else, prepare query_string for the token
|
445
|
+
if !query_parameters.to_s.empty?
|
446
|
+
query_string = format_endpoints(query_parameters)
|
447
|
+
# If format_endpoints returns nil, it was passed both slug and
|
448
|
+
# include_deleted, which is not allowed by the TimeSync API
|
449
|
+
if query_string.nil?
|
450
|
+
error_message = 'invalid combination: slug and include_deleted'
|
451
|
+
return [Hash[@error => error_message]]
|
452
|
+
end
|
453
|
+
else
|
454
|
+
query_string = format('?token=%s', @token)
|
455
|
+
end
|
456
|
+
|
457
|
+
# Construct query url - at this point query_string ends with
|
458
|
+
# ?token=token
|
459
|
+
url = format('%s/activities%s', @baseurl, query_string)
|
460
|
+
|
461
|
+
# Test mode, return list of projects if slug is nil, or a list of
|
462
|
+
# projects
|
463
|
+
if @test
|
464
|
+
m = MockTimeSync.new
|
465
|
+
return m.get_activities(slug)
|
466
|
+
end
|
467
|
+
|
468
|
+
# Attempt to GET activities, then convert the response to a ruby
|
469
|
+
# hash. Always returns a list.
|
470
|
+
begin
|
471
|
+
# Success!
|
472
|
+
response = RestClient.get url
|
473
|
+
res_dict = response_to_ruby(response.body, response.code)
|
474
|
+
return (res_dict.is_a?(Array) ? res_dict : [res_dict])
|
475
|
+
rescue => e
|
476
|
+
# Request Error
|
477
|
+
return [Hash[@error => e]]
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# get_users(username=nil)
|
482
|
+
|
483
|
+
# Request user entities from the TimeSync instance specified by the
|
484
|
+
# baseurl provided when instantiating the TimeSync object. Returns a list
|
485
|
+
# of ruby dictionaries containing the user information returned by
|
486
|
+
# TimeSync or an error message if unsuccessful.
|
487
|
+
|
488
|
+
# ``username`` is an optional parameter containing a string of the
|
489
|
+
# specific username to be retrieved. If ``username`` is not provided, a
|
490
|
+
# list containing all users will be returned. Defaults to ``nil``.
|
491
|
+
def get_users(username = nil)
|
492
|
+
# Check that user has authenticated
|
493
|
+
@local_auth_error = local_auth_error
|
494
|
+
return [Hash[@error => @local_auth_error]] if @local_auth_error
|
495
|
+
|
496
|
+
# url should end with /users if no username is passed else
|
497
|
+
# /users/username
|
498
|
+
url = if username
|
499
|
+
format('%s/users/%s', @baseurl, username)
|
500
|
+
else
|
501
|
+
format('%s/users', @baseurl)
|
502
|
+
end
|
503
|
+
|
504
|
+
# The url should always end with a token
|
505
|
+
url += format('?token=%s', @token)
|
506
|
+
|
507
|
+
# Test mode, return one user object if username is passed else return
|
508
|
+
# several user objects
|
509
|
+
if @test
|
510
|
+
m = MockTimeSync.new
|
511
|
+
return m.get_users(username)
|
512
|
+
end
|
513
|
+
|
514
|
+
# Attempt to GET users, then convert the response to a ruby
|
515
|
+
# hash. Always returns a list.
|
516
|
+
|
517
|
+
begin
|
518
|
+
# Success!
|
519
|
+
response = RestClient.get url
|
520
|
+
res_dict = response_to_ruby(response.body, response.code)
|
521
|
+
return (res_dict.is_a?(Array) ? res_dict : [res_dict])
|
522
|
+
rescue => e
|
523
|
+
# Request Error
|
524
|
+
return [Hash[@error => e]]
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
# delete_time(uuid=nil)
|
529
|
+
# Allows the currently authenticated user to delete their own time entry
|
530
|
+
# by uuid.
|
531
|
+
# ``uuid`` is a string containing the uuid of the time entry to be
|
532
|
+
# deleted.
|
533
|
+
def delete_time(uuid = nil)
|
534
|
+
# Check that user has authenticated
|
535
|
+
@local_auth_error = local_auth_error
|
536
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
537
|
+
|
538
|
+
return Hash[@error => 'missing uuid; please add to method call'] unless uuid
|
539
|
+
|
540
|
+
delete_object('times', uuid)
|
541
|
+
end
|
542
|
+
|
543
|
+
# delete_project(slug=nil)
|
544
|
+
# Allows the currently authenticated admin user to delete a project
|
545
|
+
# record by slug.
|
546
|
+
# ``slug`` is a string containing the slug of the project to be deleted.
|
547
|
+
def delete_project(slug = nil)
|
548
|
+
# Check that user has authenticated
|
549
|
+
@local_auth_error = local_auth_error
|
550
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
551
|
+
|
552
|
+
return Hash[@error => 'missing slug; please add to method call'] unless slug
|
553
|
+
|
554
|
+
delete_object('projects', slug)
|
555
|
+
end
|
556
|
+
|
557
|
+
# delete_activity(slug=nil)
|
558
|
+
# Allows the currently authenticated admin user to delete an activity
|
559
|
+
# record by slug.
|
560
|
+
# ``slug`` is a string containing the slug of the activity to be deleted.
|
561
|
+
def delete_activity(slug = nil)
|
562
|
+
# Check that user has authenticated
|
563
|
+
@local_auth_error = local_auth_error
|
564
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
565
|
+
|
566
|
+
return Hash[@error => 'missing slug; please add to method call'] unless slug
|
567
|
+
|
568
|
+
delete_object('activities', slug)
|
569
|
+
end
|
570
|
+
|
571
|
+
# delete_user(username=nil)
|
572
|
+
# Allows the currently authenticated admin user to delete a user
|
573
|
+
# record by username.
|
574
|
+
# ``username`` is a string containing the username of the user to be
|
575
|
+
# deleted.
|
576
|
+
def delete_user(username = nil)
|
577
|
+
# Check that user has authenticated
|
578
|
+
@local_auth_error = local_auth_error
|
579
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
580
|
+
|
581
|
+
unless username
|
582
|
+
return Hash[@error =>
|
583
|
+
'missing username; please add to method call']
|
584
|
+
end
|
585
|
+
|
586
|
+
delete_object('users', username)
|
587
|
+
end
|
588
|
+
|
589
|
+
# token_expiration_time
|
590
|
+
# Returns the expiration time of the JWT (JSON Web Token) associated with
|
591
|
+
# this object.
|
592
|
+
def token_expiration_time
|
593
|
+
# Check that user has authenticated
|
594
|
+
@local_auth_error = local_auth_error
|
595
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
596
|
+
|
597
|
+
# Return valid date if in test mode
|
598
|
+
if @test
|
599
|
+
m = MockTimeSync.new
|
600
|
+
return m.token_expiration_time
|
601
|
+
end
|
602
|
+
|
603
|
+
# Decode the token, then get the second dict (payload) from the
|
604
|
+
# resulting string. The payload contains the expiration time.
|
605
|
+
begin
|
606
|
+
decoded_payload = Base64.decode64(@token.split('.')[1])
|
607
|
+
rescue
|
608
|
+
return Hash[@error => 'improperly encoded token']
|
609
|
+
end
|
610
|
+
|
611
|
+
# literal_eval the string representation of a dict to convert it to a
|
612
|
+
# dict, then get the value at 'exp'. The value at 'exp' is epoch time
|
613
|
+
# in ms
|
614
|
+
exp_int = JSON.load(decoded_payload)['exp'] # not sure about this
|
615
|
+
|
616
|
+
# Convert the epoch time from ms to s
|
617
|
+
exp_int /= 1000
|
618
|
+
|
619
|
+
# Convert and format the epoch time to ruby datetime.
|
620
|
+
exp_datetime = Time.at(exp_int)
|
621
|
+
|
622
|
+
exp_datetime
|
623
|
+
end
|
624
|
+
|
625
|
+
# project_users(project)
|
626
|
+
# Returns a dict of users for the specified project containing usernames
|
627
|
+
# mapped to their list of permissions for the project.
|
628
|
+
def project_users(project = nil)
|
629
|
+
# Check that user has authenticated
|
630
|
+
@local_auth_error = local_auth_error
|
631
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
632
|
+
|
633
|
+
# Check that a project slug was passed
|
634
|
+
unless project
|
635
|
+
return Hash[@error =>
|
636
|
+
'Missing project slug, please include in method call']
|
637
|
+
end
|
638
|
+
|
639
|
+
# Construct query url
|
640
|
+
url = format('%s/projects/%s?token=%s', @baseurl, project, @token)
|
641
|
+
# Return valid user object if in test mode
|
642
|
+
if @test
|
643
|
+
m = MockTimeSync.new
|
644
|
+
return m.project_users
|
645
|
+
end
|
646
|
+
# Try to get the project object
|
647
|
+
begin
|
648
|
+
# Success!
|
649
|
+
response = RestClient.get(url)
|
650
|
+
project_object = response_to_ruby(response.body, response.code)
|
651
|
+
rescue => e
|
652
|
+
# Request Error
|
653
|
+
return Hash[@error => e]
|
654
|
+
end
|
655
|
+
|
656
|
+
# Create the user dict to return
|
657
|
+
# There was an error, don't do anything with it, return as a list
|
658
|
+
return project_object if project_object.key?('error')
|
659
|
+
|
660
|
+
# Get the user object from the project
|
661
|
+
users = project_object['users']
|
662
|
+
|
663
|
+
# Convert the nested permissions dict to a list containing only
|
664
|
+
# relevant (true) permissions
|
665
|
+
|
666
|
+
rv = Hash[]
|
667
|
+
|
668
|
+
users.each do |user, permissions|
|
669
|
+
rv_perms = Array[]
|
670
|
+
permissions.each do |perm, value|
|
671
|
+
rv_perms.push(perm) if value
|
672
|
+
end
|
673
|
+
rv[user] = rv_perms
|
674
|
+
end
|
675
|
+
|
676
|
+
rv
|
677
|
+
end
|
678
|
+
|
679
|
+
################################################
|
680
|
+
# Internal methods
|
681
|
+
################################################
|
682
|
+
|
683
|
+
# Returns auth object to log in to TimeSync
|
684
|
+
def auth
|
685
|
+
Hash['type' => @auth_type,
|
686
|
+
'username' => @user,
|
687
|
+
'password' => @password]
|
688
|
+
end
|
689
|
+
|
690
|
+
# Returns auth object with a token to send to TimeSync endpoints
|
691
|
+
def token_auth
|
692
|
+
Hash['type' => 'token',
|
693
|
+
'token' => @token,]
|
694
|
+
end
|
695
|
+
|
696
|
+
# Checks that token is set.
|
697
|
+
# Returns error if not set, otherwise returns nil
|
698
|
+
def local_auth_error
|
699
|
+
(@token ? nil : 'Not authenticated with TimeSync,'\
|
700
|
+
' call authenticate first')
|
701
|
+
end
|
702
|
+
|
703
|
+
# Hashes the password field in a user object.
|
704
|
+
# If the password is Unicode, encode it to UTF-8 first
|
705
|
+
def hash_user_password(user)
|
706
|
+
# Only hash password if it is present
|
707
|
+
# Don't error out here so that internal methods can catch all missing
|
708
|
+
# fields later on and return a more meaningful error if necessary.
|
709
|
+
if user.key?('password')
|
710
|
+
password = user['password']
|
711
|
+
|
712
|
+
# Hash the password
|
713
|
+
hashed = BCrypt::Password.create('password')
|
714
|
+
|
715
|
+
user['password'] = hashed
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
# Convert response to native ruby list of objects
|
720
|
+
def response_to_ruby(body, code)
|
721
|
+
# Body should always be a string
|
722
|
+
# DELETE returns an empty body if successful
|
723
|
+
return Hash['status' => 200] if body.empty? && code == 200
|
724
|
+
# If body is valid JSON, it came from TimeSync. If it isn't
|
725
|
+
# and we got a ValueError, we know we are having trouble connecting to
|
726
|
+
# TimeSync because we are not getting a return from TimeSync.
|
727
|
+
begin
|
728
|
+
ruby_object = JSON.load(body)
|
729
|
+
rescue
|
730
|
+
# If we get a ValueError, body isn't a JSON object, and
|
731
|
+
# therefore didn't come from a TimeSync connection.
|
732
|
+
err_msg = format('connection to TimeSync failed at baseurl %s - ',
|
733
|
+
@baseurl)
|
734
|
+
err_msg += format('response status was %s', code)
|
735
|
+
return Hash[@error => err_msg]
|
736
|
+
end
|
737
|
+
ruby_object
|
738
|
+
end
|
739
|
+
|
740
|
+
# Format endpoints for GET projects and activities requests.
|
741
|
+
# Returns nil if invalid combination of slug and include_deleted
|
742
|
+
def format_endpoints(queries)
|
743
|
+
query_string = '?'
|
744
|
+
query_list = Array[]
|
745
|
+
|
746
|
+
# The following combination is not allowed
|
747
|
+
if queries.key?('slug') && queries.key?('include_deleted')
|
748
|
+
return nil
|
749
|
+
# slug goes first, then delete it so it doesn't show up after the ?
|
750
|
+
elsif queries.key?('slug')
|
751
|
+
# query_string = '/%s?' % queries['slug']
|
752
|
+
query_string = format('/%s?', queries['slug'])
|
753
|
+
queries.delete('slug')
|
754
|
+
end
|
755
|
+
|
756
|
+
# Convert true and false booleans to TimeSync compatible strings
|
757
|
+
queries.to_a.each do |k, v|
|
758
|
+
queries[k] = v ? 'true' : 'false'
|
759
|
+
query_list.push('%s=%s', k, queries[k])
|
760
|
+
end
|
761
|
+
|
762
|
+
query_list = query_list.sort
|
763
|
+
|
764
|
+
# Check for items in query_list after slug was removed, create
|
765
|
+
# query string
|
766
|
+
if query_list
|
767
|
+
# query_string += '%s&' % query_list.join('&')
|
768
|
+
query_string += format('%s&', query_list.join('&'))
|
769
|
+
end
|
770
|
+
# Authenticate and return
|
771
|
+
# query_string += 'token=%s' % @token
|
772
|
+
query_string += format('token=%s', @token)
|
773
|
+
query_string
|
774
|
+
end
|
775
|
+
|
776
|
+
# Construct the query string for filtering GET queries, such as get_times
|
777
|
+
def construct_filter_query(queries)
|
778
|
+
query_string = '?'
|
779
|
+
query_list = Array[]
|
780
|
+
|
781
|
+
# Format the include_* queries similarly to other queries for easier
|
782
|
+
# processing
|
783
|
+
if queries.key?('include_deleted')
|
784
|
+
queries['include_deleted'] = if queries['include_deleted']
|
785
|
+
['true']
|
786
|
+
else
|
787
|
+
['false']
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
if queries.key?('include_revisions')
|
792
|
+
queries['include_revisions'] = if queries['include_revisions']
|
793
|
+
['true']
|
794
|
+
else
|
795
|
+
['false']
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
# If uuid is included, the only other accepted queries are the
|
800
|
+
# include_*s
|
801
|
+
if queries.key?('uuid')
|
802
|
+
query_string = format('/%s?', queries['uuid'])
|
803
|
+
if queries.key?('include_deleted')
|
804
|
+
query_string += format('include_deleted=%s&',
|
805
|
+
queries['include_deleted'][0])
|
806
|
+
end
|
807
|
+
|
808
|
+
if queries.key?('include_revisions')
|
809
|
+
query_string += format('include_revisions=%s&',
|
810
|
+
queries['include_revisions'][0])
|
811
|
+
end
|
812
|
+
|
813
|
+
# Everthing is a list now, so iterate through and push
|
814
|
+
else
|
815
|
+
# Sort them into an alphabetized list for easier testing
|
816
|
+
# sorted_qs = sorted(queries.to_a, key = operator.itemgetter(0))
|
817
|
+
sorted_qs = queries.to_a.sort
|
818
|
+
sorted_qs.each do |query, param|
|
819
|
+
param.each do |slug|
|
820
|
+
# Format each query in the list
|
821
|
+
# query_list.push('%s=%s' % Array[query, slug])
|
822
|
+
query_list.push(format('%s=%s', query, slug))
|
823
|
+
end
|
824
|
+
end
|
825
|
+
|
826
|
+
# Construct the query_string using the list.
|
827
|
+
# Last character will be an & so we can push the token
|
828
|
+
query_list.each do |string|
|
829
|
+
query_string += format('%s&', string)
|
830
|
+
end
|
831
|
+
end
|
832
|
+
query_string
|
833
|
+
end
|
834
|
+
|
835
|
+
# Checks that ``actual`` parameter passed to POST method contains
|
836
|
+
# items in required or optional lists for that ``object_name``.
|
837
|
+
# Returns nil if no errors found or error string if error found. If
|
838
|
+
# ``create_object`` then ``actual`` gets checked for required fields
|
839
|
+
def get_field_errors(actual, object_name, create_object)
|
840
|
+
# Check that actual is a ruby dict
|
841
|
+
unless actual.is_a? Hash
|
842
|
+
return format('%s object: must be ruby hash', object_name)
|
843
|
+
end
|
844
|
+
|
845
|
+
# missing_list contains a list of all the required parameters that were
|
846
|
+
# not passed. It is initialized to all required parameters.
|
847
|
+
missing_list = @required_params[object_name].clone
|
848
|
+
|
849
|
+
# For each key, if it is not required or optional, it is not allowed
|
850
|
+
# If it is requried, remove that parameter from the missing_list, since
|
851
|
+
# it is no longer missing
|
852
|
+
# actual.each do |key, value|
|
853
|
+
|
854
|
+
actual.each do |key, _value|
|
855
|
+
if !@required_params[object_name].include?(key.to_s) && !@optional_params[object_name].include?(key.to_s)
|
856
|
+
return format('%s object: invalid field: %s', object_name, key)
|
857
|
+
end
|
858
|
+
|
859
|
+
# Remove field from copied list if the field is in required
|
860
|
+
if @required_params[object_name].include? key.to_s
|
861
|
+
# missing_list.delete(key.to_s)
|
862
|
+
missing_list.delete_at(missing_list.index(key.to_s))
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
# If there is anything in missing_list, it is an absent required field
|
867
|
+
# This only needs to be checked if the create_object flag is passed
|
868
|
+
if create_object && !missing_list.empty?
|
869
|
+
return format('%s object: missing required field(s): %s',
|
870
|
+
object_name, missing_list.join(', '))
|
871
|
+
end
|
872
|
+
# No errors if we made it this far
|
873
|
+
nil
|
874
|
+
end
|
875
|
+
|
876
|
+
# Create or update an object ``object_name`` at specified ``endpoint``.
|
877
|
+
# This method will return that object in the form of a list containing a
|
878
|
+
# single ruby hash. The hash will contain a representation
|
879
|
+
# of the JSON body returned by TimeSync if it was successful or error
|
880
|
+
# information if it was not. If ``create_object``, then ``parameters``
|
881
|
+
# gets checked for required fields.
|
882
|
+
def create_or_update(object_fields, identifier,
|
883
|
+
object_name, endpoint, create_object = true)
|
884
|
+
# Check that user has authenticated
|
885
|
+
@local_auth_error = local_auth_error
|
886
|
+
|
887
|
+
return Hash[@error => @local_auth_error] if @local_auth_error
|
888
|
+
|
889
|
+
# Check that object contains required fields and no bad fields
|
890
|
+
field_error = get_field_errors(object_fields, object_name, create_object)
|
891
|
+
|
892
|
+
return Hash[@error => field_error] if field_error
|
893
|
+
|
894
|
+
# Since this is a POST request, we need an auth and object objects
|
895
|
+
values = Hash['auth' => token_auth, 'object' => object_fields].to_json
|
896
|
+
|
897
|
+
# Reformat the identifier with the leading '/' so it can be added to
|
898
|
+
# the url. Do this here instead of the url assignment below so we can
|
899
|
+
# set it to ' if it wasn't passed.
|
900
|
+
identifier = identifier ? format('/%s', identifier) : ''
|
901
|
+
|
902
|
+
# Construct url to post to
|
903
|
+
url = format('%s/%s%s', @baseurl, endpoint, identifier)
|
904
|
+
|
905
|
+
# Test mode, remove leading '/' from identifier
|
906
|
+
identifier.slice!(0)
|
907
|
+
test_identifier = identifier
|
908
|
+
if @test
|
909
|
+
return test_handler(object_fields, test_identifier,
|
910
|
+
object_name, create_object)
|
911
|
+
end
|
912
|
+
|
913
|
+
# Attempt to POST to TimeSync, then convert the response to a ruby
|
914
|
+
# hash
|
915
|
+
begin
|
916
|
+
# Success!
|
917
|
+
response = RestClient.post(url, values, content_type: :json,
|
918
|
+
accept: :json)
|
919
|
+
return response_to_ruby(response.body, response.code)
|
920
|
+
rescue => e
|
921
|
+
# Request error
|
922
|
+
return Hash[@error => e]
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
# When a time_entry is created, a user will enter a time duration as
|
927
|
+
# one of the parameters of the object. This method will convert that
|
928
|
+
# entry (if it's entered as a string) into the appropriate integer
|
929
|
+
# equivalent (in seconds).
|
930
|
+
def duration_to_seconds(duration)
|
931
|
+
t = Time.strptime(duration, '%Hh%Mm')
|
932
|
+
hours_spent = t.hour
|
933
|
+
minutes_spent = t.min
|
934
|
+
|
935
|
+
temp = format('%sh%sm', hours_spent, minutes_spent)
|
936
|
+
|
937
|
+
if temp != duration
|
938
|
+
error_msg = [Hash[@error => 'time object: invalid duration string']]
|
939
|
+
return error_msg
|
940
|
+
end
|
941
|
+
|
942
|
+
# Convert duration to seconds
|
943
|
+
return (hours_spent * 3600) + (minutes_spent * 60)
|
944
|
+
rescue
|
945
|
+
error_msg = [Hash[@error => 'time object: invalid duration string']]
|
946
|
+
return error_msg
|
947
|
+
end
|
948
|
+
|
949
|
+
# Deletes object at ``endpoint`` identified by ``identifier``
|
950
|
+
def delete_object(endpoint, identifier)
|
951
|
+
# Construct url
|
952
|
+
url = format('%s/%s/%s?token=%s', @baseurl, endpoint, identifier, @token)
|
953
|
+
|
954
|
+
# Test mode
|
955
|
+
if @test
|
956
|
+
m = MockTimeSync.new
|
957
|
+
return m.delete_object
|
958
|
+
end
|
959
|
+
|
960
|
+
# Attempt to DELETE object
|
961
|
+
begin
|
962
|
+
# Success!
|
963
|
+
response = RestClient.delete url
|
964
|
+
return response_to_ruby(response.body, response.code)
|
965
|
+
rescue => e
|
966
|
+
# Request error
|
967
|
+
return Hash[@error => e]
|
968
|
+
end
|
969
|
+
end
|
970
|
+
|
971
|
+
# Handle test methods in test mode for creating or updating an object
|
972
|
+
def test_handler(parameters, identifier, obj_name, create_object)
|
973
|
+
m = MockTimeSync.new
|
974
|
+
case obj_name
|
975
|
+
|
976
|
+
when 'time'
|
977
|
+
if create_object
|
978
|
+
# return mock_rimesync.create_time(parameters)
|
979
|
+
return m.create_time(parameters)
|
980
|
+
else
|
981
|
+
# return mock_rimesync.update_time(parameters, identifier)
|
982
|
+
return m.update_time(parameters, identifier)
|
983
|
+
end
|
984
|
+
when 'project'
|
985
|
+
if create_object
|
986
|
+
# return mock_rimesync.create_project(parameters)
|
987
|
+
return m.create_project(parameters)
|
988
|
+
else
|
989
|
+
# return mock_rimesync.update_project(parameters, identifier)
|
990
|
+
return m.update_project(parameters, identifier)
|
991
|
+
end
|
992
|
+
when 'activity'
|
993
|
+
if create_object
|
994
|
+
# return mock_rimesync.create_activity(parameters)
|
995
|
+
return m.create_activity(parameters)
|
996
|
+
else
|
997
|
+
# return mock_rimesync.update_activity(parameters, identifier)
|
998
|
+
return m.update_activity(parameters, identifier)
|
999
|
+
end
|
1000
|
+
when 'user'
|
1001
|
+
if create_object
|
1002
|
+
# return mock_rimesync.create_user(parameters)
|
1003
|
+
return m.create_user(parameters)
|
1004
|
+
else
|
1005
|
+
# return mock_rimesync.update_user(parameters, identifier)
|
1006
|
+
return m.update_user(parameters, identifier)
|
1007
|
+
end
|
1008
|
+
end
|
1009
|
+
end
|
1010
|
+
end
|