rimesync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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