Merge branch 'master' into 2257-inequality-conditions
[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
5   def self.uuid_infix_object_kind
6     @@uuid_infix_object_kind ||=
7       begin
8         infix_kind = {}
9         $arvados_api_client.discovery[:schemas].each do |name, schema|
10           if schema[:uuidPrefix]
11             infix_kind[schema[:uuidPrefix]] =
12               'arvados#' + name.to_s.camelcase(:lower)
13           end
14         end
15
16         # Recognize obsolete types.
17         infix_kind.
18           merge('mxsvm' => 'arvados#pipelineTemplate', # Pipeline
19                 'uo14g' => 'arvados#pipelineInstance', # PipelineInvocation
20                 'ldvyl' => 'arvados#group') # Project
21       end
22   end
23
24   def initialize(*args)
25     super(*args)
26     @attribute_sortkey ||= {
27       'id' => nil,
28       'uuid' => '000',
29       'owner_uuid' => '001',
30       'created_at' => '002',
31       'modified_at' => '003',
32       'modified_by_user_uuid' => '004',
33       'modified_by_client_uuid' => '005',
34       'name' => '050',
35       'tail_kind' => '100',
36       'tail_uuid' => '100',
37       'head_kind' => '101',
38       'head_uuid' => '101',
39       'info' => 'zzz-000',
40       'updated_at' => 'zzz-999'
41     }
42   end
43
44   def self.columns
45     return @columns unless @columns.nil?
46     @columns = []
47     @attribute_info ||= {}
48     return @columns if $arvados_api_client.arvados_schema[self.to_s.to_sym].nil?
49     $arvados_api_client.arvados_schema[self.to_s.to_sym].each do |coldef|
50       k = coldef[:name].to_sym
51       if coldef[:type] == coldef[:type].downcase
52         @columns << column(k, coldef[:type].to_sym)
53       else
54         @columns << column(k, :text)
55         serialize k, coldef[:type].constantize
56       end
57       attr_accessible k
58       @attribute_info[k] = coldef
59     end
60     attr_reader :etag
61     attr_reader :kind
62     @columns
63   end
64
65   def self.column(name, sql_type = nil, default = nil, null = true)
66     ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
67   end
68
69   def self.attribute_info
70     self.columns
71     @attribute_info
72   end
73
74   def self.find(uuid, opts={})
75     if uuid.class != String or uuid.length < 27 then
76       raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx'
77     end
78
79     # Only do one lookup on the API side per {class, uuid, workbench
80     # request} unless {cache: false} is given via opts.
81     cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}"
82     if opts[:cache] == false
83       Rails.cache.write cache_key, $arvados_api_client.api(self, '/' + uuid)
84     end
85     hash = Rails.cache.fetch cache_key do
86       $arvados_api_client.api(self, '/' + uuid)
87     end
88     new.private_reload(hash)
89   end
90
91   def self.order(*args)
92     ArvadosResourceList.new(self).order(*args)
93   end
94
95   def self.where(*args)
96     ArvadosResourceList.new(self).where(*args)
97   end
98
99   def self.limit(*args)
100     ArvadosResourceList.new(self).limit(*args)
101   end
102
103   def self.eager(*args)
104     ArvadosResourceList.new(self).eager(*args)
105   end
106
107   def self.all(*args)
108     ArvadosResourceList.new(self).all(*args)
109   end
110
111   def save
112     obdata = {}
113     self.class.columns.each do |col|
114       obdata[col.name.to_sym] = self.send(col.name.to_sym)
115     end
116     obdata.delete :id
117     postdata = { self.class.to_s.underscore => obdata }
118     if etag
119       postdata['_method'] = 'PUT'
120       obdata.delete :uuid
121       resp = $arvados_api_client.api(self.class, '/' + uuid, postdata)
122     else
123       resp = $arvados_api_client.api(self.class, '', postdata)
124     end
125     return false if !resp[:etag] || !resp[:uuid]
126
127     # set read-only non-database attributes
128     @etag = resp[:etag]
129     @kind = resp[:kind]
130
131     # these attrs can be modified by "save" -- we should update our copies
132     %w(uuid owner_uuid created_at
133        modified_at modified_by_user_uuid modified_by_client_uuid
134       ).each do |attr|
135       if self.respond_to? "#{attr}=".to_sym
136         self.send(attr + '=', resp[attr.to_sym])
137       end
138     end
139
140     @new_record = false
141
142     self
143   end
144
145   def save!
146     self.save or raise Exception.new("Save failed")
147   end
148
149   def destroy
150     if etag || uuid
151       postdata = { '_method' => 'DELETE' }
152       resp = $arvados_api_client.api(self.class, '/' + uuid, postdata)
153       resp[:etag] && resp[:uuid] && resp
154     else
155       true
156     end
157   end
158       
159   def links(*args)
160     o = {}
161     o.merge!(args.pop) if args[-1].is_a? Hash
162     o[:link_class] ||= args.shift
163     o[:name] ||= args.shift
164     o[:head_kind] ||= args.shift
165     o[:tail_kind] = self.kind
166     o[:tail_uuid] = self.uuid
167     if all_links
168       return all_links.select do |m|
169         ok = true
170         o.each do |k,v|
171           if !v.nil?
172             test_v = m.send(k)
173             if (v.respond_to?(:uuid) ? v.uuid : v.to_s) != (test_v.respond_to?(:uuid) ? test_v.uuid : test_v.to_s)
174               ok = false
175             end
176           end
177         end
178         ok
179       end
180     end
181     @links = $arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true }
182     @links = $arvados_api_client.unpack_api_response(@links)
183   end
184
185   def all_links
186     return @all_links if @all_links
187     res = $arvados_api_client.api Link, '', {
188       _method: 'GET',
189       where: {
190         tail_kind: self.kind,
191         tail_uuid: self.uuid
192       },
193       eager: true
194     }
195     @all_links = $arvados_api_client.unpack_api_response(res)
196   end
197
198   def reload
199     private_reload(self.uuid)
200   end
201
202   def private_reload(uuid_or_hash)
203     raise "No such object" if !uuid_or_hash
204     if uuid_or_hash.is_a? Hash
205       hash = uuid_or_hash
206     else
207       hash = $arvados_api_client.api(self.class, '/' + uuid_or_hash)
208     end
209     hash.each do |k,v|
210       if self.respond_to?(k.to_s + '=')
211         self.send(k.to_s + '=', v)
212       else
213         # When ArvadosApiClient#schema starts telling us what to expect
214         # in API responses (not just the server side database
215         # columns), this sort of awfulness can be avoided:
216         self.instance_variable_set('@' + k.to_s, v)
217         if !self.respond_to? k
218           singleton = class << self; self end
219           singleton.send :define_method, k, lambda { instance_variable_get('@' + k.to_s) }
220         end
221       end
222     end
223     @all_links = nil
224     @new_record = false
225     self
226   end
227
228   def to_param
229     uuid
230   end
231
232   def dup
233     super.forget_uuid!
234   end
235
236   def attributes_for_display
237     self.attributes.reject { |k,v|
238       attribute_sortkey.has_key?(k) and !attribute_sortkey[k]
239     }.sort_by { |k,v|
240       attribute_sortkey[k] or k
241     }
242   end
243
244   def self.creatable?
245     current_user
246   end
247
248   def editable?
249     (current_user and current_user.is_active and
250      (current_user.is_admin or
251       current_user.uuid == self.owner_uuid))
252   end
253
254   def attribute_editable?(attr)
255     if "created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at".index(attr.to_s)
256       false
257     elsif not (current_user.andand.is_active)
258       false
259     elsif "uuid owner_uuid".index(attr.to_s) or current_user.is_admin
260       current_user.is_admin
261     else
262       current_user.uuid == self.owner_uuid or current_user.uuid == self.uuid
263     end
264   end
265
266   def self.resource_class_for_uuid(uuid, opts={})
267     if uuid.is_a? ArvadosBase
268       return uuid.class
269     end
270     unless uuid.is_a? String
271       return nil
272     end
273     if opts[:class].is_a? Class
274       return opts[:class]
275     end
276     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
277       return Collection
278     end
279     resource_class = nil
280     uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
281       resource_class ||= $arvados_api_client.
282         kind_class(self.uuid_infix_object_kind[re[1]])
283     end
284     if opts[:referring_object] and
285         opts[:referring_attr] and
286         opts[:referring_attr].match /_uuid$/
287       resource_class ||= $arvados_api_client.
288         kind_class(opts[:referring_object].
289                    attributes[opts[:referring_attr].
290                               sub(/_uuid$/, '_kind')])
291     end
292     resource_class
293   end
294
295   def friendly_link_name
296     (name if self.respond_to? :name) || uuid
297   end
298
299   def selection_label
300     friendly_link_name
301   end
302
303   protected
304
305   def forget_uuid!
306     self.uuid = nil
307     @etag = nil
308     self
309   end
310
311   def self.current_user
312     Thread.current[:user] ||= User.current if Thread.current[:arvados_api_token]
313     Thread.current[:user]
314   end
315   def current_user
316     self.class.current_user
317   end
318 end