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