2872: Use data chooser for running pipelines. Many presentation fixes.
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   include ArvadosApiClientHelper
3   include ApplicationHelper
4
5   respond_to :html, :json, :js
6   protect_from_forgery
7
8   ERROR_ACTIONS = [:render_error, :render_not_found]
9
10   around_filter :thread_clear
11   around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS
12   around_filter :thread_with_optional_api_token
13   before_filter :check_user_agreements, except: ERROR_ACTIONS
14   before_filter :check_user_notifications, except: ERROR_ACTIONS
15   before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS
16   theme :select_theme
17
18   begin
19     rescue_from Exception,
20     :with => :render_exception
21     rescue_from ActiveRecord::RecordNotFound,
22     :with => :render_not_found
23     rescue_from ActionController::RoutingError,
24     :with => :render_not_found
25     rescue_from ActionController::UnknownController,
26     :with => :render_not_found
27     rescue_from ::AbstractController::ActionNotFound,
28     :with => :render_not_found
29   end
30
31   def unprocessable(message=nil)
32     @errors ||= []
33
34     @errors << message if message
35     render_error status: 422
36   end
37
38   def render_error(opts)
39     opts = {status: 500}.merge opts
40     respond_to do |f|
41       # json must come before html here, so it gets used as the
42       # default format when js is requested by the client. This lets
43       # ajax:error callback parse the response correctly, even though
44       # the browser can't.
45       f.json { render opts.merge(json: {success: false, errors: @errors}) }
46       f.html { render opts.merge(controller: 'application', action: 'error') }
47     end
48   end
49
50   def render_exception(e)
51     logger.error e.inspect
52     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
53     if @object.andand.errors.andand.full_messages.andand.any?
54       @errors = @object.errors.full_messages
55     else
56       @errors = [e.to_s]
57     end
58     self.render_error status: 422
59   end
60
61   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
62     logger.error e.inspect
63     @errors = ["Path not found"]
64     self.render_error status: 404
65   end
66
67   def find_objects_for_index
68     @limit ||= 200
69     if params[:limit]
70       @limit = params[:limit].to_i
71     end
72
73     @offset ||= 0
74     if params[:offset]
75       @offset = params[:offset].to_i
76     end
77
78     @filters ||= []
79     if params[:filters]
80       filters = params[:filters]
81       if filters.is_a? String
82         filters = Oj.load filters
83       end
84       @filters += filters
85     end
86
87     @objects ||= model_class
88     @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
89   end
90
91   def index
92     find_objects_for_index if !@objects
93     respond_to do |f|
94       f.json { render json: @objects }
95       f.html { render }
96       f.js { render }
97     end
98   end
99
100   def show
101     if !@object
102       return render_not_found("object not found")
103     end
104     respond_to do |f|
105       f.json { render json: @object.attributes.merge(href: url_for(@object)) }
106       f.html {
107         if request.method == 'GET'
108           render
109         else
110           redirect_to params[:return_to] || @object
111         end
112       }
113       f.js { render }
114     end
115   end
116
117   def choose
118     find_objects_for_index
119     render partial: 'choose', locals: {multiple: params[:multiple]}
120   end
121
122   def render_content
123     if !@object
124       return render_not_found("object not found")
125     end
126   end
127
128   def new
129     @object = model_class.new
130   end
131
132   def update
133     @updates ||= params[@object.class.to_s.underscore.singularize.to_sym]
134     @updates.keys.each do |attr|
135       if @object.send(attr).is_a? Hash
136         if @updates[attr].is_a? String
137           @updates[attr] = Oj.load @updates[attr]
138         end
139         if params[:merge] || params["merge_#{attr}".to_sym]
140           # Merge provided Hash with current Hash, instead of
141           # replacing.
142           @updates[attr] = @object.send(attr).with_indifferent_access.
143             deep_merge(@updates[attr].with_indifferent_access)
144         end
145       end
146     end
147     if @object.update_attributes @updates
148       show
149     else
150       self.render_error status: 422
151     end
152   end
153
154   def create
155     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
156     @new_resource_attrs ||= {}
157     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
158     @object ||= model_class.new @new_resource_attrs
159     @object.save!
160     show
161   end
162
163   def destroy
164     if @object.destroy
165       respond_to do |f|
166         f.json { render json: @object }
167         f.html {
168           redirect_to(params[:return_to] || :back)
169         }
170         f.js { render }
171       end
172     else
173       self.render_error status: 422
174     end
175   end
176
177   def current_user
178     if Thread.current[:arvados_api_token]
179       Thread.current[:user] ||= User.current
180     else
181       logger.error "No API token in Thread"
182       return nil
183     end
184   end
185
186   def model_class
187     controller_name.classify.constantize
188   end
189
190   def breadcrumb_page_name
191     (@breadcrumb_page_name ||
192      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
193      action_name)
194   end
195
196   def index_pane_list
197     %w(Recent)
198   end
199
200   def show_pane_list
201     %w(Attributes Metadata Advanced)
202   end
203
204   protected
205
206   def redirect_to_login
207     respond_to do |f|
208       f.html {
209         if request.method == 'GET'
210           redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
211         else
212           flash[:error] = "Either you are not logged in, or your session has timed out. I can't automatically log you in and re-attempt this request."
213           redirect_to :back
214         end
215       }
216       f.json {
217         @errors = ['You do not seem to be logged in. You did not supply an API token with this request, and your session (if any) has timed out.']
218         self.render_error status: 422
219       }
220     end
221     false  # For convenience to return from callbacks
222   end
223
224   def using_specific_api_token(api_token)
225     start_values = {}
226     [:arvados_api_token, :user].each do |key|
227       start_values[key] = Thread.current[key]
228     end
229     Thread.current[:arvados_api_token] = api_token
230     Thread.current[:user] = nil
231     begin
232       yield
233     ensure
234       start_values.each_key { |key| Thread.current[key] = start_values[key] }
235     end
236   end
237
238   def find_object_by_uuid
239     if params[:id] and params[:id].match /\D/
240       params[:uuid] = params.delete :id
241     end
242     if not model_class
243       @object = nil
244     elsif params[:uuid].is_a? String
245       if params[:uuid].empty?
246         @object = nil
247       else
248         if (model_class != Link and
249             resource_class_for_uuid(params[:uuid]) == Link)
250           @name_link = Link.find(params[:uuid])
251           @object = model_class.find(@name_link.head_uuid)
252         else
253           @object = model_class.find(params[:uuid])
254         end
255       end
256     else
257       @object = model_class.where(uuid: params[:uuid]).first
258     end
259   end
260
261   def thread_clear
262     Thread.current[:arvados_api_token] = nil
263     Thread.current[:user] = nil
264     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
265     yield
266     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
267   end
268
269   def thread_with_api_token(login_optional = false)
270     begin
271       try_redirect_to_login = true
272       if params[:api_token]
273         try_redirect_to_login = false
274         Thread.current[:arvados_api_token] = params[:api_token]
275         # Before copying the token into session[], do a simple API
276         # call to verify its authenticity.
277         if verify_api_token
278           session[:arvados_api_token] = params[:api_token]
279           if !request.format.json? and request.method == 'GET'
280             # Repeat this request with api_token in the (new) session
281             # cookie instead of the query string.  This prevents API
282             # tokens from appearing in (and being inadvisedly copied
283             # and pasted from) browser Location bars.
284             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
285           else
286             yield
287           end
288         else
289           @errors = ['Invalid API token']
290           self.render_error status: 401
291         end
292       elsif session[:arvados_api_token]
293         # In this case, the token must have already verified at some
294         # point, but it might have been revoked since.  We'll try
295         # using it, and catch the exception if it doesn't work.
296         try_redirect_to_login = false
297         Thread.current[:arvados_api_token] = session[:arvados_api_token]
298         begin
299           yield
300         rescue ArvadosApiClient::NotLoggedInException
301           try_redirect_to_login = true
302         end
303       else
304         logger.debug "No token received, session is #{session.inspect}"
305       end
306       if try_redirect_to_login
307         unless login_optional
308           redirect_to_login
309         else
310           # login is optional for this route so go on to the regular controller
311           Thread.current[:arvados_api_token] = nil
312           yield
313         end
314       end
315     ensure
316       # Remove token in case this Thread is used for anything else.
317       Thread.current[:arvados_api_token] = nil
318     end
319   end
320
321   def thread_with_mandatory_api_token
322     thread_with_api_token do
323       yield
324     end
325   end
326
327   # This runs after thread_with_mandatory_api_token in the filter chain.
328   def thread_with_optional_api_token
329     if Thread.current[:arvados_api_token]
330       # We are already inside thread_with_mandatory_api_token.
331       yield
332     else
333       # We skipped thread_with_mandatory_api_token. Use the optional version.
334       thread_with_api_token(true) do
335         yield
336       end
337     end
338   end
339
340   def verify_api_token
341     begin
342       Link.where(uuid: 'just-verifying-my-api-token')
343       true
344     rescue ArvadosApiClient::NotLoggedInException
345       false
346     end
347   end
348
349   def ensure_current_user_is_admin
350     unless current_user and current_user.is_admin
351       @errors = ['Permission denied']
352       self.render_error status: 401
353     end
354   end
355
356   def check_user_agreements
357     if current_user && !current_user.is_active && current_user.is_invited
358       signatures = UserAgreement.signatures
359       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
360       @required_user_agreements = UserAgreement.all.map do |ua|
361         if not @signed_ua_uuids.index ua.uuid
362           Collection.find(ua.uuid)
363         end
364       end.compact
365       if @required_user_agreements.empty?
366         # No agreements to sign. Perhaps we just need to ask?
367         current_user.activate
368         if !current_user.is_active
369           logger.warn "#{current_user.uuid.inspect}: " +
370             "No user agreements to sign, but activate failed!"
371         end
372       end
373       if !current_user.is_active
374         render 'user_agreements/index'
375       end
376     end
377     true
378   end
379
380   def select_theme
381     return Rails.configuration.arvados_theme
382   end
383
384   @@notification_tests = []
385
386   @@notification_tests.push lambda { |controller, current_user|
387     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
388       return nil
389     end
390     return lambda { |view|
391       view.render partial: 'notifications/ssh_key_notification'
392     }
393   }
394
395   #@@notification_tests.push lambda { |controller, current_user|
396   #  Job.limit(1).where(created_by: current_user.uuid).each do
397   #    return nil
398   #  end
399   #  return lambda { |view|
400   #    view.render partial: 'notifications/jobs_notification'
401   #  }
402   #}
403
404   @@notification_tests.push lambda { |controller, current_user|
405     Collection.limit(1).where(created_by: current_user.uuid).each do
406       return nil
407     end
408     return lambda { |view|
409       view.render partial: 'notifications/collections_notification'
410     }
411   }
412
413   @@notification_tests.push lambda { |controller, current_user|
414     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
415       return nil
416     end
417     return lambda { |view|
418       view.render partial: 'notifications/pipelines_notification'
419     }
420   }
421
422   def check_user_notifications
423     @notification_count = 0
424     @notifications = []
425
426     if current_user
427       @showallalerts = false
428       @@notification_tests.each do |t|
429         a = t.call(self, current_user)
430         if a
431           @notification_count += 1
432           @notifications.push a
433         end
434       end
435     end
436
437     if @notification_count == 0
438       @notification_count = ''
439     end
440   end
441 end