rimesync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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