2872: Add infinite scroll to chooser modal.
[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   helper_method :next_page_offset
92   def next_page_offset
93     if @objects.respond_to?(:result_offset) and
94         @objects.respond_to?(:result_limit) and
95         @objects.respond_to?(:items_available)
96       next_offset = @objects.result_offset + @objects.result_limit
97       if next_offset < @objects.items_available
98         next_offset
99       else
100         nil
101       end
102     end
103   end
104
105   def index
106     find_objects_for_index if !@objects
107     respond_to do |f|
108       f.json { render json: @objects }
109       f.html { render }
110       f.js { render }
111     end
112   end
113
114   def show
115     if !@object
116       return render_not_found("object not found")
117     end
118     respond_to do |f|
119       f.json { render json: @object.attributes.merge(href: url_for(@object)) }
120       f.html {
121         if request.method == 'GET'
122           render
123         else
124           redirect_to params[:return_to] || @object
125         end
126       }
127       f.js { render }
128     end
129   end
130
131   def choose
132     params[:limit] ||= 20
133     find_objects_for_index if !@objects
134     respond_to do |f|
135       if params[:partial]
136         f.json {
137           render json: {
138             content: render_to_string(partial: "choose_rows.html",
139                                       formats: [:html],
140                                       locals: {
141                                         multiple: params[:multiple]
142                                       }),
143             next_page_href: @next_page_href
144           }
145         }
146       end
147       f.js {
148         render partial: 'choose', locals: {multiple: params[:multiple]}
149       }
150     end
151   end
152
153   def render_content
154     if !@object
155       return render_not_found("object not found")
156     end
157   end
158
159   def new
160     @object = model_class.new
161   end
162
163   def update
164     @updates ||= params[@object.resource_param_name.to_sym]
165     @updates.keys.each do |attr|
166       if @object.send(attr).is_a? Hash
167         if @updates[attr].is_a? String
168           @updates[attr] = Oj.load @updates[attr]
169         end
170         if params[:merge] || params["merge_#{attr}".to_sym]
171           # Merge provided Hash with current Hash, instead of
172           # replacing.
173           @updates[attr] = @object.send(attr).with_indifferent_access.
174             deep_merge(@updates[attr].with_indifferent_access)
175         end
176       end
177     end
178     if @object.update_attributes @updates
179       show
180     else
181       self.render_error status: 422
182     end
183   end
184
185   def create
186     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
187     @new_resource_attrs ||= {}
188     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
189     @object ||= model_class.new @new_resource_attrs
190     @object.save!
191     show
192   end
193
194   def destroy
195     if @object.destroy
196       respond_to do |f|
197         f.json { render json: @object }
198         f.html {
199           redirect_to(params[:return_to] || :back)
200         }
201         f.js { render }
202       end
203     else
204       self.render_error status: 422
205     end
206   end
207
208   def current_user
209     if Thread.current[:arvados_api_token]
210       Thread.current[:user] ||= User.current
211     else
212       logger.error "No API token in Thread"
213       return nil
214     end
215   end
216
217   def model_class
218     controller_name.classify.constantize
219   end
220
221   def breadcrumb_page_name
222     (@breadcrumb_page_name ||
223      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
224      action_name)
225   end
226
227   def index_pane_list
228     %w(Recent)
229   end
230
231   def show_pane_list
232     %w(Attributes Advanced)
233   end
234
235   protected
236
237   def redirect_to_login
238     respond_to do |f|
239       f.html {
240         if request.method == 'GET'
241           redirect_to arvados_api_client.arvados_login_url(return_to: request.url)
242         else
243           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."
244           redirect_to :back
245         end
246       }
247       f.json {
248         @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.']
249         self.render_error status: 422
250       }
251     end
252     false  # For convenience to return from callbacks
253   end
254
255   def using_specific_api_token(api_token)
256     start_values = {}
257     [:arvados_api_token, :user].each do |key|
258       start_values[key] = Thread.current[key]
259     end
260     Thread.current[:arvados_api_token] = api_token
261     Thread.current[:user] = nil
262     begin
263       yield
264     ensure
265       start_values.each_key { |key| Thread.current[key] = start_values[key] }
266     end
267   end
268
269   def find_object_by_uuid
270     if params[:id] and params[:id].match /\D/
271       params[:uuid] = params.delete :id
272     end
273     if not model_class
274       @object = nil
275     elsif params[:uuid].is_a? String
276       if params[:uuid].empty?
277         @object = nil
278       else
279         if (model_class != Link and
280             resource_class_for_uuid(params[:uuid]) == Link)
281           @name_link = Link.find(params[:uuid])
282           @object = model_class.find(@name_link.head_uuid)
283         else
284           @object = model_class.find(params[:uuid])
285         end
286       end
287     else
288       @object = model_class.where(uuid: params[:uuid]).first
289     end
290   end
291
292   def thread_clear
293     Thread.current[:arvados_api_token] = nil
294     Thread.current[:user] = nil
295     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
296     yield
297     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
298   end
299
300   def thread_with_api_token(login_optional = false)
301     begin
302       try_redirect_to_login = true
303       if params[:api_token]
304         try_redirect_to_login = false
305         Thread.current[:arvados_api_token] = params[:api_token]
306         # Before copying the token into session[], do a simple API
307         # call to verify its authenticity.
308         if verify_api_token
309           session[:arvados_api_token] = params[:api_token]
310           if !request.format.json? and request.method == 'GET'
311             # Repeat this request with api_token in the (new) session
312             # cookie instead of the query string.  This prevents API
313             # tokens from appearing in (and being inadvisedly copied
314             # and pasted from) browser Location bars.
315             redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '')
316           else
317             yield
318           end
319         else
320           @errors = ['Invalid API token']
321           self.render_error status: 401
322         end
323       elsif session[:arvados_api_token]
324         # In this case, the token must have already verified at some
325         # point, but it might have been revoked since.  We'll try
326         # using it, and catch the exception if it doesn't work.
327         try_redirect_to_login = false
328         Thread.current[:arvados_api_token] = session[:arvados_api_token]
329         begin
330           yield
331         rescue ArvadosApiClient::NotLoggedInException
332           try_redirect_to_login = true
333         end
334       else
335         logger.debug "No token received, session is #{session.inspect}"
336       end
337       if try_redirect_to_login
338         unless login_optional
339           redirect_to_login
340         else
341           # login is optional for this route so go on to the regular controller
342           Thread.current[:arvados_api_token] = nil
343           yield
344         end
345       end
346     ensure
347       # Remove token in case this Thread is used for anything else.
348       Thread.current[:arvados_api_token] = nil
349     end
350   end
351
352   def thread_with_mandatory_api_token
353     thread_with_api_token do
354       yield
355     end
356   end
357
358   # This runs after thread_with_mandatory_api_token in the filter chain.
359   def thread_with_optional_api_token
360     if Thread.current[:arvados_api_token]
361       # We are already inside thread_with_mandatory_api_token.
362       yield
363     else
364       # We skipped thread_with_mandatory_api_token. Use the optional version.
365       thread_with_api_token(true) do
366         yield
367       end
368     end
369   end
370
371   def verify_api_token
372     begin
373       Link.where(uuid: 'just-verifying-my-api-token')
374       true
375     rescue ArvadosApiClient::NotLoggedInException
376       false
377     end
378   end
379
380   def ensure_current_user_is_admin
381     unless current_user and current_user.is_admin
382       @errors = ['Permission denied']
383       self.render_error status: 401
384     end
385   end
386
387   def check_user_agreements
388     if current_user && !current_user.is_active && current_user.is_invited
389       signatures = UserAgreement.signatures
390       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
391       @required_user_agreements = UserAgreement.all.map do |ua|
392         if not @signed_ua_uuids.index ua.uuid
393           Collection.find(ua.uuid)
394         end
395       end.compact
396       if @required_user_agreements.empty?
397         # No agreements to sign. Perhaps we just need to ask?
398         current_user.activate
399         if !current_user.is_active
400           logger.warn "#{current_user.uuid.inspect}: " +
401             "No user agreements to sign, but activate failed!"
402         end
403       end
404       if !current_user.is_active
405         render 'user_agreements/index'
406       end
407     end
408     true
409   end
410
411   def select_theme
412     return Rails.configuration.arvados_theme
413   end
414
415   @@notification_tests = []
416
417   @@notification_tests.push lambda { |controller, current_user|
418     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
419       return nil
420     end
421     return lambda { |view|
422       view.render partial: 'notifications/ssh_key_notification'
423     }
424   }
425
426   #@@notification_tests.push lambda { |controller, current_user|
427   #  Job.limit(1).where(created_by: current_user.uuid).each do
428   #    return nil
429   #  end
430   #  return lambda { |view|
431   #    view.render partial: 'notifications/jobs_notification'
432   #  }
433   #}
434
435   @@notification_tests.push lambda { |controller, current_user|
436     Collection.limit(1).where(created_by: current_user.uuid).each do
437       return nil
438     end
439     return lambda { |view|
440       view.render partial: 'notifications/collections_notification'
441     }
442   }
443
444   @@notification_tests.push lambda { |controller, current_user|
445     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
446       return nil
447     end
448     return lambda { |view|
449       view.render partial: 'notifications/pipelines_notification'
450     }
451   }
452
453   def check_user_notifications
454     @notification_count = 0
455     @notifications = []
456
457     if current_user
458       @showallalerts = false
459       @@notification_tests.each do |t|
460         a = t.call(self, current_user)
461         if a
462           @notification_count += 1
463           @notifications.push a
464         end
465       end
466     end
467
468     if @notification_count == 0
469       @notification_count = ''
470     end
471   end
472
473   helper_method :my_folders
474   def my_folders
475     return @my_folders if @my_folders
476     @my_folders = []
477     root_of = {}
478     Group.filter([['group_class','=','folder']]).each do |g|
479       root_of[g.uuid] = g.owner_uuid
480       @my_folders << g
481     end
482     done = false
483     while not done
484       done = true
485       root_of = root_of.each_with_object({}) do |(child, parent), h|
486         if root_of[parent]
487           h[child] = root_of[parent]
488           done = false
489         else
490           h[child] = parent
491         end
492       end
493     end
494     @my_folders = @my_folders.select do |g|
495       root_of[g.uuid] == current_user.uuid
496     end
497   end
498 end