2872: Merge branch 'master' into 2872-folder-nav
[arvados.git] / apps / workbench / app / models / arvados_base.rb
1 class ArvadosBase < ActiveRecord::Base
2   self.abstract_class = true
3   attr_accessor :attribute_sortkey
4   attr_accessor :create_params
5
6   def self.arvados_api_client
7     ArvadosApiClient.new_or_current
8   end
9
10   def arvados_api_client
11     ArvadosApiClient.new_or_current
12   end
13
14   def self.uuid_infix_object_kind
15     @@uuid_infix_object_kind ||=
16       begin
17         infix_kind = {}
18         arvados_api_client.discovery[:schemas].each do |name, schema|
19           if schema[:uuidPrefix]
20             infix_kind[schema[:uuidPrefix]] =
21               'arvados#' + name.to_s.camelcase(:lower)
22           end
23         end
24
25         # Recognize obsolete types.
26         infix_kind.
27           merge('mxsvm' => 'arvados#pipelineTemplate', # Pipeline
28                 'uo14g' => 'arvados#pipelineInstance', # PipelineInvocation
29                 'ldvyl' => 'arvados#group') # Project
30       end
31   end
32
33   def initialize raw_params={}, create_params={}
34     super self.class.permit_attribute_params(raw_params)
35     @create_params = create_params
36     @attribute_sortkey ||= {
37       'id' => nil,
38       'name' => '000',
39       'owner_uuid' => '002',
40       'event_type' => '100',
41       'link_class' => '100',
42       'group_class' => '100',
43       'tail_uuid' => '101',
44       'head_uuid' => '102',
45       'object_uuid' => '102',
46       'summary' => '104',
47       'description' => '104',
48       'properties' => '150',
49       'info' => '150',
50       'created_at' => '200',
51       'modified_at' => '201',
52       'modified_by_user_uuid' => '202',
53       'modified_by_client_uuid' => '203',
54       'uuid' => '999',
55     }
56   end
57
58   def self.columns
59     return @columns unless @columns.nil?
60     @columns = []
61     @attribute_info ||= {}
62     schema = arvados_api_client.discovery[:schemas][self.to_s.to_sym]
63     return @columns if schema.nil?
64     schema[:properties].each do |k, coldef|
65       case k
66       when :etag, :kind
67         attr_reader k
68       else
69         if coldef[:type] == coldef[:type].downcase
70           # boolean, integer, etc.
71           @columns << column(k, coldef[:type].to_sym)
72         else
73           # Hash, Array
74           @columns << column(k, :text)
75           serialize k, coldef[:type].constantize
76         end
77         @attribute_info[k] = coldef
78       end
79     end
80     @columns
81   end
82
83   def self.column(name, sql_type = nil, default = nil, null = true)
84     ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
85   end
86
87   def self.attribute_info
88     self.columns
89     @attribute_info
90   end
91
92   def self.find(uuid, opts={})
93     if uuid.class != String or uuid.length < 27 then
94       raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx'
95     end
96
97     if self == ArvadosBase
98       # Determine type from uuid and defer to the appropriate subclass.
99       return resource_class_for_uuid(uuid).find(uuid, opts)
100     end
101
102     # Only do one lookup on the API side per {class, uuid, workbench
103     # request} unless {cache: false} is given via opts.
104     cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}"
105     if opts[:cache] == false
106       Rails.cache.write cache_key, arvados_api_client.api(self, '/' + uuid)
107     end
108     hash = Rails.cache.fetch cache_key do
109       arvados_api_client.api(self, '/' + uuid)
110     end
111     new.private_reload(hash)
112   end
113
114   def self.find?(*args)
115     find(*args) rescue nil
116   end
117
118   def self.order(*args)
119     ArvadosResourceList.new(self).order(*args)
120   end
121
122   def self.filter(*args)
123     ArvadosResourceList.new(self).filter(*args)
124   end
125
126   def self.where(*args)
127     ArvadosResourceList.new(self).where(*args)
128   end
129
130   def self.limit(*args)
131     ArvadosResourceList.new(self).limit(*args)
132   end
133
134   def self.eager(*args)
135     ArvadosResourceList.new(self).eager(*args)
136   end
137
138   def self.all(*args)
139     ArvadosResourceList.new(self).all(*args)
140   end
141
142   def self.permit_attribute_params raw_params
143     # strong_parameters does not provide security in Workbench: anyone
144     # who can get this far can just as well do a call directly to our
145     # database (Arvados) with the same credentials we use.
146     #
147     # The following permit! is necessary even with
148     # "ActionController::Parameters.permit_all_parameters = true",
149     # because permit_all does not permit nested attributes.
150     ActionController::Parameters.new(raw_params).permit!
151   end
152
153   def self.create raw_params={}, create_params={}
154     x = super(permit_attribute_params(raw_params))
155     x.create_params = create_params
156     x
157   end
158
159   def update_attributes raw_params={}
160     super(self.class.permit_attribute_params(raw_params))
161   end
162
163   def save
164     obdata = {}
165     self.class.columns.each do |col|
166       obdata[col.name.to_sym] = self.send(col.name.to_sym)
167     end
168     obdata.delete :id
169     postdata = { self.class.to_s.underscore => obdata }
170     if etag
171       postdata['_method'] = 'PUT'
172       obdata.delete :uuid
173       resp = arvados_api_client.api(self.class, '/' + uuid, postdata)
174     else
175       postdata.merge!(@create_params) if @create_params
176       resp = arvados_api_client.api(self.class, '', postdata)
177     end
178     return false if !resp[:etag] || !resp[:uuid]
179
180     # set read-only non-database attributes
181     @etag = resp[:etag]
182     @kind = resp[:kind]
183
184     # attributes can be modified during "save" -- we should update our copies
185     resp.keys.each do |attr|
186       if self.respond_to? "#{attr}=".to_sym
187         self.send(attr.to_s + '=', resp[attr.to_sym])
188       end
189     end
190
191     @new_record = false
192
193     self
194   end
195
196   def save!
197     self.save or raise Exception.new("Save failed")
198   end
199
200   def destroy
201     if etag || uuid
202       postdata = { '_method' => 'DELETE' }
203       resp = arvados_api_client.api(self.class, '/' + uuid, postdata)
204       resp[:etag] && resp[:uuid] && resp
205     else
206       true
207     end
208   end
209
210   def links(*args)
211     o = {}
212     o.merge!(args.pop) if args[-1].is_a? Hash
213     o[:link_class] ||= args.shift
214     o[:name] ||= args.shift
215     o[:tail_uuid] = self.uuid
216     if all_links
217       return all_links.select do |m|
218         ok = true
219         o.each do |k,v|
220           if !v.nil?
221             test_v = m.send(k)
222             if (v.respond_to?(:uuid) ? v.uuid : v.to_s) != (test_v.respond_to?(:uuid) ? test_v.uuid : test_v.to_s)
223               ok = false
224             end
225           end
226         end
227         ok
228       end
229     end
230     @links = arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true }
231     @links = arvados_api_client.unpack_api_response(@links)
232   end
233
234   def all_links
235     return @all_links if @all_links
236     res = arvados_api_client.api Link, '', {
237       _method: 'GET',
238       where: {
239         tail_kind: self.kind,
240         tail_uuid: self.uuid
241       },
242       eager: true
243     }
244     @all_links = arvados_api_client.unpack_api_response(res)
245   end
246
247   def reload
248     private_reload(self.uuid)
249   end
250
251   def private_reload(uuid_or_hash)
252     raise "No such object" if !uuid_or_hash
253     if uuid_or_hash.is_a? Hash
254       hash = uuid_or_hash
255     else
256       hash = arvados_api_client.api(self.class, '/' + uuid_or_hash)
257     end
258     hash.each do |k,v|
259       if self.respond_to?(k.to_s + '=')
260         self.send(k.to_s + '=', v)
261       else
262         # When ArvadosApiClient#schema starts telling us what to expect
263         # in API responses (not just the server side database
264         # columns), this sort of awfulness can be avoided:
265         self.instance_variable_set('@' + k.to_s, v)
266         if !self.respond_to? k
267           singleton = class << self; self end
268           singleton.send :define_method, k, lambda { instance_variable_get('@' + k.to_s) }
269         end
270       end
271     end
272     @all_links = nil
273     @new_record = false
274     self
275   end
276
277   def to_param
278     uuid
279   end
280
281   def initialize_copy orig
282     super
283     forget_uuid!
284   end
285
286   def attributes_for_display
287     self.attributes.reject { |k,v|
288       attribute_sortkey.has_key?(k) and !attribute_sortkey[k]
289     }.sort_by { |k,v|
290       attribute_sortkey[k] or k
291     }
292   end
293
294   def class_for_display
295     self.class.to_s.underscore.humanize
296   end
297
298   def self.creatable?
299     current_user
300   end
301
302   def self.goes_in_projects?
303     false
304   end
305
306   def editable?
307     (current_user and current_user.is_active and
308      (current_user.is_admin or
309       current_user.uuid == self.owner_uuid or
310       new_record? or
311       (writable_by.include? current_user.uuid rescue false)))
312   end
313
314   def attribute_editable?(attr, ever=nil)
315     if "created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at".index(attr.to_s)
316       false
317     elsif not (current_user.andand.is_active)
318       false
319     elsif attr == 'uuid'
320       current_user.is_admin
321     elsif ever
322       true
323     else
324       editable?
325     end
326   end
327
328   def self.resource_class_for_uuid(uuid, opts={})
329     if uuid.is_a? ArvadosBase
330       return uuid.class
331     end
332     unless uuid.is_a? String
333       return nil
334     end
335     if opts[:class].is_a? Class
336       return opts[:class]
337     end
338     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
339       return Collection
340     end
341     resource_class = nil
342     uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
343       resource_class ||= arvados_api_client.
344         kind_class(self.uuid_infix_object_kind[re[1]])
345     end
346     if opts[:referring_object] and
347         opts[:referring_attr] and
348         opts[:referring_attr].match /_uuid$/
349       resource_class ||= arvados_api_client.
350         kind_class(opts[:referring_object].
351                    attributes[opts[:referring_attr].
352                               sub(/_uuid$/, '_kind')])
353     end
354     resource_class
355   end
356
357   def resource_param_name
358     self.class.to_s.underscore
359   end
360
361   def friendly_link_name
362     (name if self.respond_to? :name) || default_name
363   end
364
365   def content_summary
366     self.class_for_display
367   end
368
369   def selection_label
370     friendly_link_name
371   end
372
373   def self.default_name
374     self.to_s.underscore.humanize
375   end
376
377   def controller
378     (self.class.to_s.pluralize + 'Controller').constantize
379   end
380
381   def controller_name
382     self.class.to_s.tableize
383   end
384
385   # Placeholder for name when name is missing or empty
386   def default_name
387     if self.respond_to? :name
388       "New #{class_for_display.downcase}"
389     else
390       uuid
391     end
392   end
393
394   def owner
395     ArvadosBase.find(owner_uuid) rescue nil
396   end
397
398   protected
399
400   def forget_uuid!
401     self.uuid = nil
402     @etag = nil
403     self
404   end
405
406   def self.current_user
407     Thread.current[:user] ||= User.current if Thread.current[:arvados_api_token]
408     Thread.current[:user]
409   end
410   def current_user
411     self.class.current_user
412   end
413 end