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