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