dgrid 0.0.2

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OWM5ODE0NDQwYTg3YmI3OGI3YTI4NTk1ZGI5MzU3OTg4YWM3OWVlNA==
5
+ data.tar.gz: !binary |-
6
+ NjExMjkyNjY5ZGIxOTQyMWRhY2Q4M2Y1NDZlMTk1YzVjNDk4NjYwOA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OWJhZTA5MzQzN2ZkNDVhYjg2ZTAzZDNhYjQ0NDRjYjFkNzJiNzM2MzhhNjQw
10
+ YzMwYzJlMDY4N2JmMWMwMDA2YjAzYjE5M2I1YjAyYjhjYTk4OTI2MGFjYjE4
11
+ MjY1Yzk4ZjkwYTljMzE1YWUyYmU5NjM2MTMwNzU1YjQyOWQ5NzU=
12
+ data.tar.gz: !binary |-
13
+ OWNhY2NiYTgxYzRiYTJkOGQ0MDIyZjA2Y2E1Yzk5MmYxZDE0MGY5N2U3Zjdh
14
+ NDc0MTI3NDI4NzZhOTEwZGRlNzJhZjdjYjAxNzJmOTIyNzg4NjNhMDBjNWQy
15
+ ZThkZmIzMjVhZDkyNmM3MDY2OTUxMzMzZjkyZDM2NjE0ODAwMGI=
@@ -0,0 +1 @@
1
+ require 'dgrid/api'
@@ -0,0 +1,20 @@
1
+ # gem dependencies
2
+ require 'set'
3
+ require 'date'
4
+ require 'dgrid/argument_validation'
5
+ require 'dgrid/set_members_from_hash'
6
+
7
+ # gem components
8
+ require 'dgrid/api/gps_coordinates'
9
+ require 'dgrid/api/connection'
10
+ require 'dgrid/api/entity'
11
+ require 'dgrid/api/named_entity'
12
+ require 'dgrid/api/person'
13
+ require 'dgrid/api/place'
14
+ require 'dgrid/api/organization'
15
+ require 'dgrid/api/incident'
16
+ require 'dgrid/api/item'
17
+ require 'dgrid/api/keyword'
18
+ require 'dgrid/api/lens'
19
+ require 'dgrid/api/link'
20
+ require 'dgrid/api/workspace'
@@ -0,0 +1,444 @@
1
+
2
+ require 'yaml'
3
+ require 'net/http/post/multipart'
4
+
5
+ module Dgrid
6
+ module API
7
+ require 'json'
8
+ class Workspace # just a forward declaration
9
+ end
10
+
11
+ class HTTPError < Exception
12
+ end
13
+
14
+ class NotFoundError < HTTPError
15
+ end
16
+
17
+ class ServerError < HTTPError
18
+ end
19
+
20
+ class AuthError < HTTPError
21
+ end
22
+
23
+ # Decorates URI::* with a params method that parses the query
24
+ # into a hash of parameters.
25
+ # Frankly this should be in URI::* and I am very tempted to
26
+ # monkey-patch it in there but Erik held me back. :)
27
+ class AugmentedURI
28
+ def initialize(url_string)
29
+ @delegate = URI.parse(url_string)
30
+ end
31
+
32
+ def method_missing(*args, &block)
33
+ @delegate.send(*args,&block)
34
+ end
35
+
36
+ def params
37
+ Hash[ @delegate.query.split('&').map {|x| x.split('=')} ]
38
+ end
39
+ end
40
+
41
+ class RestAdapter
42
+ def rest_post(path, params = {})
43
+ raise "you must override rest_post in subclasses"
44
+ end
45
+
46
+ def rest_get(path, params = {})
47
+ raise "you must override rest_get in subclasses"
48
+ end
49
+ end
50
+
51
+ class ServerRestAdapter < RestAdapter
52
+ require 'rest-client'
53
+
54
+
55
+ @@BASE_URLS = { :development => "http://localhost:3000",
56
+ :preview => "https://bridge-preview.decisiongrid.com/",
57
+ :production => "https://app.decisiongrid.com/" }
58
+
59
+ def initialize
60
+ @environment = select_environment
61
+ # FIXME: Remove this once the server has been made less picky.
62
+ # HACK
63
+ # Prior to 12/2013, the server rejected all requests from any
64
+ # unsupported browser. Forging chrome identity was a hack
65
+ # workaround for this ill-advised "feature" (bug).
66
+ @user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36'
67
+ end
68
+
69
+ def rest_post(path, params = {})
70
+ full_url = "#{base_url}/#{path}"
71
+ debug_puts "about to post to #{full_url} with #{params.to_json}"
72
+ response = ::RestClient.post(full_url, params.to_json,
73
+ { :content_type => :json,
74
+ :accept => :json,
75
+ :user_agent => @user_agent},
76
+ &method(:permit_redirects))
77
+ debug_puts "post response was #{response}"
78
+ process_rest_response(response)
79
+ end
80
+
81
+ def rest_get(path, params = {})
82
+ url_params_list = []
83
+ params.each { |k,v| url_params_list << "#{k}=#{v}" }
84
+ url_params = url_params_list.join('&')
85
+ full_url = (path =~ /^https?:\/\//) ? path : "#{base_url}/#{path}"
86
+ full_url += ("?#{url_params}") if url_params.length > 0
87
+ debug_puts "about to get #{full_url}"
88
+ response = RestClient.get(full_url,
89
+ { :content_type => :json,
90
+ :accept => :json, :user_agent => @user_agent},
91
+ &method(:permit_redirects))
92
+ debug_puts "get response was #{response}"
93
+ process_rest_response(response)
94
+ end
95
+
96
+ def rest_delete(path, params = {})
97
+ url_params_list = []
98
+ params.each { |k,v| url_params_list << "#{k}=#{v}" }
99
+ url_params = url_params_list.join('&')
100
+ full_url = "#{base_url}/#{path}"
101
+ full_url += ("?#{url_params}") if url_params.length > 0
102
+ debug_puts "about to delete #{full_url}"
103
+ response = RestClient.delete(full_url,
104
+ { :content_type => :json,
105
+ :accept => :json,
106
+ :user_agent => @user_agent},
107
+ &method(:permit_redirects))
108
+ debug_puts "delete response was #{response}"
109
+ process_rest_response(response, expect_json = false)
110
+ end
111
+
112
+ protected
113
+ attr_accessor :environment
114
+
115
+ # A block to pass to RestClient transactions that prevents them from freaking out
116
+ # about redirect responses.
117
+ def permit_redirects(response,request,result,&block)
118
+ redirect_response_codes = [301, 302, 307]
119
+ if redirect_response_codes.include? response.code
120
+ return response # do not throw exception just because of redirect
121
+ else
122
+ response.return!(request,result,&block) # do the usual thing for all other cases
123
+ end
124
+ end
125
+
126
+
127
+ def select_environment
128
+ env_str = ENV.include?('DGRID_ENV') ? ENV['DGRID_ENV'] : 'development'
129
+ case env_str.downcase
130
+ when 'development'
131
+ return :development
132
+ when 'preview'
133
+ return :preview
134
+ when 'production'
135
+ return :production
136
+ else
137
+ raise "invalid DGRID_ENV: #{env_str}"
138
+ end
139
+ end
140
+
141
+ def base_url
142
+ @@BASE_URLS[@environment]
143
+ end
144
+
145
+ def process_rest_response(response, expect_json = true)
146
+ case response.code
147
+ # TODO enumerate more codes if needed
148
+ when 400
149
+ raise NotFoundError
150
+ when 401
151
+ raise AuthError
152
+ when 500..599
153
+ raise ServerError
154
+ when 200..299
155
+ # various success codes
156
+ when 300..308
157
+ # TODO: should we really be accepting all of these
158
+ # redirects as success?
159
+ return Hash.new # avoid json parsing errors below
160
+ else
161
+ raise HTTPError
162
+ end
163
+ if expect_json
164
+ json = response.body
165
+ obj = JSON.parse(json)
166
+ obj
167
+ else
168
+ response.body
169
+ end
170
+ end
171
+
172
+ def debug_puts(message)
173
+ STDERR.puts message
174
+ end
175
+
176
+ end
177
+
178
+ class Connection
179
+ include ::Dgrid::ArgumentValidation
180
+ include ::Dgrid::SetMembersFromHash
181
+
182
+ @@default_rest_adapter = ServerRestAdapter
183
+ def self.default_rest_adapter=(new_default)
184
+ @@default_rest_adapter = new_default
185
+ end
186
+
187
+ option :username, String, :required
188
+ option :password, String, :required
189
+ option :rest_adapter, RestAdapter
190
+ def initialize(options, &block)
191
+ @auth, other_members = split_hash(options,[:username,:password])
192
+ set_members_from_hash(other_members)
193
+ @rest_adapter ||= @@default_rest_adapter.new
194
+
195
+ confirm_authentication
196
+ yield self if block_given?
197
+ end
198
+
199
+
200
+ argument :name, String
201
+ def create_workspace(name, &block)
202
+ workspace = Workspace.new(self,name)
203
+ if has_multi_workspace?
204
+ workspace_params = rest_post('/workspaces', :name => name)
205
+ workspace.id = workspace_params['id']
206
+ @workspaces_response = nil # clear cache of workspaces_response
207
+ else
208
+ workspace.id = '0'
209
+ end
210
+ yield workspace if block_given?
211
+ workspace
212
+ end
213
+
214
+ #argument :entity, Entity
215
+ def create_entity(entity, workspace_id)
216
+ singular_name = entity.class.name.split('::').last.downcase # e.g Dgrid::API::Person => 'person'
217
+ plural_name = entity.class.pluralized
218
+ path_parts =[plural_name]
219
+ if has_multi_workspace?
220
+ path_parts = ['workspaces',workspace_id ] + path_parts
221
+ end
222
+ path = path_parts.join('/')
223
+ params = entity.to_hash
224
+ returned_params = rest_post(path,params)
225
+ entity_params = returned_params[singular_name]
226
+ raise "Did not get an id for new #{singular_name} #{entity.to_s} in #{entity_params.to_s}" unless entity_params.include?("id") && entity_params["id"]
227
+ entity.id = entity_params["id"]
228
+ entity
229
+ end
230
+
231
+ #argument :entity, Entity
232
+ #argument :workspace_id, String
233
+ def delete_entity_from_workspace(entity,workspace_id)
234
+ raise "Entity must have id to be deleted" unless entity.id
235
+ plural_name = entity.class.pluralized
236
+ path_parts =[plural_name, entity.id]
237
+ if has_multi_workspace?
238
+ path_parts = ['workspaces',workspace_id ] + path_parts
239
+ end
240
+ path = path_parts.join('/')
241
+ returned_params = rest_delete(path)
242
+ end
243
+
244
+
245
+ #argument :entity1, Entity
246
+ #argument :entity2, Entity
247
+ #argument :workspace_id, String
248
+ #option :link_type, String
249
+ def create_link(entity1, entity2, workspace_id, options)
250
+ # FIXME It is completely unknown if this is the correct url structure
251
+ raise UnimplementedFunctionality
252
+ path_parts =['links']
253
+ params = {:left_guid => left_entity.id, :right_guid => right_entity.id, :description => options[:link_type]}
254
+ if has_multi_workspace?
255
+ path_parts = ['workspaces',workspace_id ] + path_parts
256
+ end
257
+ path = path_parts.join('/')
258
+ returned_params = rest_post(path,params)
259
+ end
260
+
261
+ def attach_file_to_entity_in_workspace(entity,filename,workspace_id)
262
+ # TODO Need to support other entity types than incident
263
+ raise "Only attaching to incidents supported at this time" unless entity.is_a?(Incident)
264
+
265
+ raise "File #{filename} not found" unless File.exists?(filename)
266
+ raise "Cannot attach files to an unsaved #{entity.class.name} " if entity.new_record?
267
+
268
+ # TODO Use workspace-independent route when it becomes available in the server
269
+ new_attachment_path = "/workspaces/#{workspace_id}/"
270
+ new_attachment_path += "#{entity.class.pluralized}/#{entity.id}/attachments/new"
271
+
272
+ presigned_post = rest_get(new_attachment_path)
273
+ post_response = post_form_with_file(presigned_post, filename)
274
+ redirected_to = post_response.header['location']
275
+ # Need to parse the redirect url so we can augment the params with
276
+ # auth info in rest_get.
277
+ redirected_url = AugmentedURI.new(redirected_to)
278
+ redirection_path = redirected_url.path
279
+ redirection_params = redirected_url.params
280
+ redirection_response = rest_get(redirection_path,redirection_params)
281
+ end
282
+
283
+
284
+ # Make entity subordinate to another within the specified workspace
285
+ # argument :entity, Entity
286
+ # argument :other, Entity
287
+ # argument :workspace_id, String
288
+ def subordinate_entity_to_other_entity_in_workspace(entity, other, workspace_id)
289
+ raise "Cannot subordiante unsaved #{entity} to #{other.type} #{other}" if entity.new_record?
290
+ raise "Cannot subordiante #{entity} to unsaved #{other.type} #{other}" if other.new_record?
291
+ entity_type = entity.class.pluralized
292
+ path_parts =[other.class.pluralized, other.id, entity_type, entity.id,'add']
293
+
294
+ if has_multi_workspace?
295
+ path_parts = ['workspaces',workspace_id ] + path_parts
296
+ end
297
+
298
+ path = path_parts.join('/')
299
+ returned_params = rest_get(path)
300
+ end
301
+
302
+ # list of current workspace objects
303
+ def workspaces
304
+ result = []
305
+ if has_multi_workspace?
306
+ workspaces_response = workspaces_rest_call
307
+ workspaces_list = workspaces_response['workspaces']
308
+ workspaces_list.each do |ws_params|
309
+ ws = Workspace.new(self, ws_params['name'], ws_params['description'])
310
+ ws.id = ws_params['id']
311
+ result << ws
312
+ end
313
+ else
314
+ ws = Workspace.new(self,'Default Workspace');
315
+ ws.id = '0';
316
+ result << ws
317
+ end
318
+ result
319
+ end
320
+
321
+ def get_workspace(name)
322
+ workspace = self.workspaces.detect {|ws| ws.name.downcase == name.downcase}
323
+ yield workspace if block_given?
324
+ workspace
325
+ end
326
+
327
+ def get_in_workspace(workspace_id, type, base_path_parts = [] )
328
+ params = type == 'links'? {:flat => 1} : {}
329
+ path_parts = base_path_parts + [type]
330
+ if has_multi_workspace?
331
+ path_parts = ['workspaces', workspace_id] + path_parts
332
+ end
333
+ path = path_parts.join('/')
334
+ returned_params = rest_get(path, params)
335
+
336
+ # FIXME Remove this once the production bug is fixed.
337
+ # HACK
338
+ # This is a workaround for a production bug that existed for
339
+ # a few weeks in November of 2013
340
+ hack_to_work_around_lenses_items_index_change(returned_params)
341
+
342
+
343
+ # FIXME
344
+ # HACK
345
+ # This is an ugly hack to deal with inconsistency in REST results
346
+ # We should probably fix the REST routes and undo this
347
+ if returned_params.include?('item_ids') && base_path_parts.include?('lenses')
348
+ type = 'item_ids'
349
+ elsif returned_params.include?('incident_ids') && base_path_parts.include?('items')
350
+ type = 'incident_ids'
351
+ end
352
+ returned_params[type]
353
+ end
354
+
355
+ %w(items links people places organizations incidents keywords).each do |type|
356
+ define_method("get_#{type}_in_workspace") { |workspace_id|
357
+ get_in_workspace(workspace_id, type)
358
+ }
359
+ end
360
+
361
+ def get_incidents_in_item(workspace_id, item_id)
362
+ get_in_workspace(workspace_id, 'incidents', ['items', item_id])
363
+ end
364
+
365
+ def get_items_in_lens(workspace_id, lens_id)
366
+ get_in_workspace(workspace_id, 'items', ['lenses',lens_id])
367
+ end
368
+
369
+ protected
370
+ attr_accessor :auth, :rest_adapter
371
+ def workspaces_rest_call
372
+ if @workspaces_response.nil?
373
+ weird_required_params = {as_selection: 1}
374
+ @workspaces_response = rest_get('/workspaces', weird_required_params)
375
+ end
376
+ @workspaces_response
377
+ end
378
+
379
+ def post_form_with_file(post_form_params, filename)
380
+ raise "File #{filename} not found" unless File.exists?(filename)
381
+ post_params = post_form_params.clone
382
+ post_action = post_params.delete('action')
383
+ post_url = URI.parse(post_action)
384
+ mime_type = 'application/octet-stream'
385
+ post_params['file'] = UploadIO.new(File.new(filename), mime_type ,filename)
386
+ req = Net::HTTP::Post::Multipart.new(post_url.path, post_params)
387
+ n = Net::HTTP.new(post_url.host,post_url.port)
388
+ n.use_ssl = ('https' == post_url.scheme.downcase )
389
+ response = n.start do |http|
390
+ http.request(req)
391
+ end
392
+ end
393
+
394
+ def rest_post(path,params = {})
395
+ full_params = params.merge(@auth)
396
+ rest_adapter.rest_post(path,full_params)
397
+ end
398
+
399
+ def rest_get(path,params = {})
400
+ full_params = params.merge(@auth)
401
+ rest_adapter.rest_get(path,full_params)
402
+ end
403
+
404
+ def rest_delete(path,params = {})
405
+ full_params = params.merge(@auth)
406
+ rest_adapter.rest_delete(path,full_params)
407
+ end
408
+
409
+ def confirm_authentication
410
+ rest_get('/profile') # confirm proper credentials by trying to do something
411
+ end
412
+
413
+ def has_multi_workspace?
414
+ @workspace_mode ||= determine_workspace_mode
415
+ return :multi_workspace == @workspace_mode
416
+ end
417
+
418
+ def determine_workspace_mode
419
+ begin
420
+ workspaces_rest_call
421
+ return :multi_workspace
422
+ rescue NotFoundError => e
423
+ return :single_workspace
424
+ rescue RestClient::ResourceNotFound => e
425
+ return :single_workspace
426
+ end
427
+ end
428
+
429
+ # Transform data returned by one rest call back to its rightful
430
+ # form.
431
+ # There was a bug introduced in November 2013 which changed
432
+ # the structure of data returned by the /workspaces/id/lenses/id/items
433
+ # route. This method modifies the returned_params to look the way
434
+ # they should ( {'items_ids' => {....} } )
435
+ def hack_to_work_around_lenses_items_index_change(returned_params)
436
+ if returned_params.include?('lensings') && !returned_params.include?('item_ids')
437
+ returned_params['item_ids'] = returned_params['lensings'].map {|lensing| lensing['item_id']}
438
+ # STDERR.puts "transformed this: #{returned_params['lensings'].inspect} into this: #{returned_params['item_ids']}"
439
+ end
440
+ end
441
+
442
+ end
443
+ end
444
+ end # module Dgrid