Add ArvadosModel.filter() to support filters API
[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   def self.column(name, sql_type = nil, default = nil, null = true)
65     ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
66   end
67   def self.attribute_info
68     self.columns
69     @attribute_info
70   end
71   def self.find(uuid, opts={})
72     if uuid.class != String or uuid.length < 27 then
73       raise 'argument to find() must be a uuid string. Acceptable formats: warehouse locator or string with format xxxxx-xxxxx-xxxxxxxxxxxxxxx'
74     end
75
76     # Only do one lookup on the API side per {class, uuid, workbench
77     # request} unless {cache: false} is given via opts.
78     cache_key = "request_#{Thread.current.object_id}_#{self.to_s}_#{uuid}"
79     if opts[:cache] == false
80       Rails.cache.write cache_key, $arvados_api_client.api(self, '/' + uuid)
81     end
82     hash = Rails.cache.fetch cache_key do
83       $arvados_api_client.api(self, '/' + uuid)
84     end
85     new.private_reload(hash)
86   end
87   def self.order(*args)
88     ArvadosResourceList.new(self).order(*args)
89   end
90   def self.filter(*args)
91     ArvadosResourceList.new(self).filter(*args)
92   end
93   def self.where(*args)
94     ArvadosResourceList.new(self).where(*args)
95   end
96   def self.limit(*args)
97     ArvadosResourceList.new(self).limit(*args)
98   end
99   def self.eager(*args)
100     ArvadosResourceList.new(self).eager(*args)
101   end
102   def self.all(*args)
103     ArvadosResourceList.new(self).all(*args)
104   end
105   def save
106     obdata = {}
107     self.class.columns.each do |col|
108       obdata[col.name.to_sym] = self.send(col.name.to_sym)
109     end
110     obdata.delete :id
111     postdata = { self.class.to_s.underscore => obdata }
112     if etag
113       postdata['_method'] = 'PUT'
114       obdata.delete :uuid
115       resp = $arvados_api_client.api(self.class, '/' + uuid, postdata)
116     else
117       resp = $arvados_api_client.api(self.class, '', postdata)
118     end
119     return false if !resp[:etag] || !resp[:uuid]
120
121     # set read-only non-database attributes
122     @etag = resp[:etag]
123     @kind = resp[:kind]
124
125     # these attrs can be modified by "save" -- we should update our copies
126     %w(uuid owner_uuid created_at
127        modified_at modified_by_user_uuid modified_by_client_uuid
128       ).each do |attr|
129       if self.respond_to? "#{attr}=".to_sym
130         self.send(attr + '=', resp[attr.to_sym])
131       end
132     end
133
134     self
135   end
136   def save!
137     self.save or raise Exception.new("Save failed")
138   end
139
140   def destroy
141     if etag || uuid
142       postdata = { '_method' => 'DELETE' }
143       resp = $arvados_api_client.api(self.class, '/' + uuid, postdata)
144       resp[:etag] && resp[:uuid] && resp
145     else
146       true
147     end
148   end
149       
150   def links(*args)
151     o = {}
152     o.merge!(args.pop) if args[-1].is_a? Hash
153     o[:link_class] ||= args.shift
154     o[:name] ||= args.shift
155     o[:head_kind] ||= args.shift
156     o[:tail_kind] = self.kind
157     o[:tail_uuid] = self.uuid
158     if all_links
159       return all_links.select do |m|
160         ok = true
161         o.each do |k,v|
162           if !v.nil?
163             test_v = m.send(k)
164             if (v.respond_to?(:uuid) ? v.uuid : v.to_s) != (test_v.respond_to?(:uuid) ? test_v.uuid : test_v.to_s)
165               ok = false
166             end
167           end
168         end
169         ok
170       end
171     end
172     @links = $arvados_api_client.api Link, '', { _method: 'GET', where: o, eager: true }
173     @links = $arvados_api_client.unpack_api_response(@links)
174   end
175   def all_links
176     return @all_links if @all_links
177     res = $arvados_api_client.api Link, '', {
178       _method: 'GET',
179       where: {
180         tail_kind: self.kind,
181         tail_uuid: self.uuid
182       },
183       eager: true
184     }
185     @all_links = $arvados_api_client.unpack_api_response(res)
186   end
187   def reload
188     private_reload(self.uuid)
189   end
190   def private_reload(uuid_or_hash)
191     raise "No such object" if !uuid_or_hash
192     if uuid_or_hash.is_a? Hash
193       hash = uuid_or_hash
194     else
195       hash = $arvados_api_client.api(self.class, '/' + uuid_or_hash)
196     end
197     hash.each do |k,v|
198       if self.respond_to?(k.to_s + '=')
199         self.send(k.to_s + '=', v)
200       else
201         # When ArvadosApiClient#schema starts telling us what to expect
202         # in API responses (not just the server side database
203         # columns), this sort of awfulness can be avoided:
204         self.instance_variable_set('@' + k.to_s, v)
205         if !self.respond_to? k
206           singleton = class << self; self end
207           singleton.send :define_method, k, lambda { instance_variable_get('@' + k.to_s) }
208         end
209       end
210     end
211     @all_links = nil
212     self
213   end
214   def dup
215     super.forget_uuid!
216   end
217
218   def attributes_for_display
219     self.attributes.reject { |k,v|
220       attribute_sortkey.has_key?(k) and !attribute_sortkey[k]
221     }.sort_by { |k,v|
222       attribute_sortkey[k] or k
223     }
224   end
225
226   def self.creatable?
227     current_user
228   end
229
230   def editable?
231     (current_user and current_user.is_active and
232      (current_user.is_admin or
233       current_user.uuid == self.owner_uuid))
234   end
235
236   def attribute_editable?(attr)
237     if "created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at".index(attr.to_s)
238       false
239     elsif not (current_user.andand.is_active)
240       false
241     elsif "uuid owner_uuid".index(attr.to_s) or current_user.is_admin
242       current_user.is_admin
243     else
244       current_user.uuid == self.owner_uuid or current_user.uuid == self.uuid
245     end
246   end
247
248   def self.resource_class_for_uuid(uuid, opts={})
249     if uuid.is_a? ArvadosBase
250       return uuid.class
251     end
252     unless uuid.is_a? String
253       return nil
254     end
255     if opts[:class].is_a? Class
256       return opts[:class]
257     end
258     if uuid.match /^[0-9a-f]{32}(\+[^,]+)*(,[0-9a-f]{32}(\+[^,]+)*)*$/
259       return Collection
260     end
261     resource_class = nil
262     uuid.match /^[0-9a-z]{5}-([0-9a-z]{5})-[0-9a-z]{15}$/ do |re|
263       resource_class ||= $arvados_api_client.
264         kind_class(self.uuid_infix_object_kind[re[1]])
265     end
266     if opts[:referring_object] and
267         opts[:referring_attr] and
268         opts[:referring_attr].match /_uuid$/
269       resource_class ||= $arvados_api_client.
270         kind_class(opts[:referring_object].
271                    attributes[opts[:referring_attr].
272                               sub(/_uuid$/, '_kind')])
273     end
274     resource_class
275   end
276
277   def friendly_link_name
278     (name if self.respond_to? :name) || uuid
279   end
280
281   protected
282
283   def forget_uuid!
284     self.uuid = nil
285     @etag = nil
286     self
287   end
288
289   def self.current_user
290     Thread.current[:user] ||= User.current if Thread.current[:arvados_api_token]
291     Thread.current[:user]
292   end
293   def current_user
294     self.class.current_user
295   end
296 end