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