10218: Merge branch 'master' into 10218-record-node-info
[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   prepend_before_filter :set_current_request_id, except: ERROR_ACTIONS
11   around_filter :thread_clear
12   around_filter :set_thread_api_token
13   # Methods that don't require login should
14   #   skip_around_filter :require_thread_api_token
15   around_filter :require_thread_api_token, except: ERROR_ACTIONS
16   before_filter :ensure_arvados_api_exists, only: [:index, :show]
17   before_filter :set_cache_buster
18   before_filter :accept_uuid_as_id_param, except: ERROR_ACTIONS
19   before_filter :check_user_agreements, except: ERROR_ACTIONS
20   before_filter :check_user_profile, except: ERROR_ACTIONS
21   before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
22   before_filter :find_object_by_uuid, except: [:create, :index, :choose] + ERROR_ACTIONS
23   theme :select_theme
24
25   begin
26     rescue_from(ActiveRecord::RecordNotFound,
27                 ActionController::RoutingError,
28                 ActionController::UnknownController,
29                 AbstractController::ActionNotFound,
30                 with: :render_not_found)
31     rescue_from(Exception,
32                 ActionController::UrlGenerationError,
33                 with: :render_exception)
34   end
35
36   def set_cache_buster
37     response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
38     response.headers["Pragma"] = "no-cache"
39     response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
40   end
41
42   def unprocessable(message=nil)
43     @errors ||= []
44
45     @errors << message if message
46     render_error status: 422
47   end
48
49   def render_error(opts={})
50     # Helpers can rely on the presence of @errors to know they're
51     # being used in an error page.
52     @errors ||= []
53     opts[:status] ||= 500
54     respond_to do |f|
55       # json must come before html here, so it gets used as the
56       # default format when js is requested by the client. This lets
57       # ajax:error callback parse the response correctly, even though
58       # the browser can't.
59       f.json { render opts.merge(json: {success: false, errors: @errors}) }
60       f.html { render({action: 'error'}.merge(opts)) }
61     end
62   end
63
64   def render_exception(e)
65     logger.error e.inspect
66     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
67     err_opts = {status: 422}
68     if e.is_a?(ArvadosApiClient::ApiError)
69       err_opts.merge!(action: 'api_error', locals: {api_error: e})
70       @errors = e.api_response[:errors]
71     elsif @object.andand.errors.andand.full_messages.andand.any?
72       @errors = @object.errors.full_messages
73     else
74       @errors = [e.to_s]
75     end
76     # Make user information available on the error page, falling back to the
77     # session cache if the API server is unavailable.
78     begin
79       load_api_token(session[:arvados_api_token])
80     rescue ArvadosApiClient::ApiError
81       unless session[:user].nil?
82         begin
83           Thread.current[:user] = User.new(session[:user])
84         rescue ArvadosApiClient::ApiError
85           # This can happen if User's columns are unavailable.  Nothing to do.
86         end
87       end
88     end
89     # Preload projects trees for the template.  If that's not doable, set empty
90     # trees so error page rendering can proceed.  (It's easier to rescue the
91     # exception here than in a template.)
92     unless current_user.nil?
93       begin
94         my_starred_projects current_user
95         build_my_wanted_projects_tree current_user
96       rescue ArvadosApiClient::ApiError
97         # Fall back to the default-setting code later.
98       end
99     end
100     @starred_projects ||= []
101     @my_wanted_projects_tree ||= []
102     render_error(err_opts)
103   end
104
105   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
106     logger.error e.inspect
107     @errors = ["Path not found"]
108     set_thread_api_token do
109       self.render_error(action: '404', status: 404)
110     end
111   end
112
113   # params[:order]:
114   #
115   # The order can be left empty to allow it to default.
116   # Or it can be a comma separated list of real database column names, one per model.
117   # Column names should always be qualified by a table name and a direction is optional, defaulting to asc
118   # (e.g. "collections.name" or "collections.name desc").
119   # If a column name is specified, that table will be sorted by that column.
120   # If there are objects from different models that will be shown (such as in Pipelines and processes tab),
121   # then a sort column name can optionally be specified for each model, passed as an comma-separated list (e.g. "jobs.script, pipeline_instances.name")
122   # Currently only one sort column name and direction can be specified for each model.
123   def load_filters_and_paging_params
124     if params[:order].blank?
125       @order = 'created_at desc'
126     elsif params[:order].is_a? Array
127       @order = params[:order]
128     else
129       begin
130         @order = JSON.load(params[:order])
131       rescue
132         @order = params[:order].split(',')
133       end
134     end
135     @order = [@order] unless @order.is_a? Array
136
137     @limit ||= 200
138     if params[:limit]
139       @limit = params[:limit].to_i
140     end
141
142     @offset ||= 0
143     if params[:offset]
144       @offset = params[:offset].to_i
145     end
146
147     @filters ||= []
148     if params[:filters]
149       filters = params[:filters]
150       if filters.is_a? String
151         filters = Oj.load filters
152       elsif filters.is_a? Array
153         filters = filters.collect do |filter|
154           if filter.is_a? String
155             # Accept filters[]=["foo","=","bar"]
156             Oj.load filter
157           else
158             # Accept filters=[["foo","=","bar"]]
159             filter
160           end
161         end
162       end
163       # After this, params[:filters] can be trusted to be an array of arrays:
164       params[:filters] = filters
165       @filters += filters
166     end
167   end
168
169   def find_objects_for_index
170     @objects ||= model_class
171     @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
172     @objects.fetch_multiple_pages(false)
173   end
174
175   def render_index
176     respond_to do |f|
177       f.json {
178         if params[:partial]
179           @next_page_href = next_page_href(partial: params[:partial], filters: @filters.to_json)
180           render json: {
181             content: render_to_string(partial: "show_#{params[:partial]}",
182                                       formats: [:html]),
183             next_page_href: @next_page_href
184           }
185         else
186           render json: @objects
187         end
188       }
189       f.html {
190         if params[:tab_pane]
191           render_pane params[:tab_pane]
192         else
193           render
194         end
195       }
196       f.js { render }
197     end
198   end
199
200   helper_method :render_pane
201   def render_pane tab_pane, opts={}
202     render_opts = {
203       partial: 'show_' + tab_pane.downcase,
204       locals: {
205         comparable: self.respond_to?(:compare),
206         objects: @objects,
207         tab_pane: tab_pane
208       }.merge(opts[:locals] || {})
209     }
210     if opts[:to_string]
211       render_to_string render_opts
212     else
213       render render_opts
214     end
215   end
216
217   def ensure_arvados_api_exists
218     if model_class.is_a?(Class) && model_class < ArvadosBase && !model_class.api_exists?(params['action'].to_sym)
219       @errors = ["#{params['action']} method is not supported for #{params['controller']}"]
220       return render_error(status: 404)
221     end
222   end
223
224   def index
225     find_objects_for_index if !@objects
226     render_index
227   end
228
229   helper_method :next_page_offset
230   def next_page_offset objects=nil
231     if !objects
232       objects = @objects
233     end
234     if objects.respond_to?(:result_offset) and
235         objects.respond_to?(:result_limit)
236       next_offset = objects.result_offset + objects.result_limit
237       if objects.respond_to?(:items_available) and (next_offset < objects.items_available)
238         next_offset
239       elsif @objects.results.size > 0 and (params[:count] == 'none' or
240            (params[:controller] == 'search' and params[:action] == 'choose'))
241         last_object_class = @objects.last.class
242         if params['last_object_class'].nil? or params['last_object_class'] == last_object_class.to_s
243           next_offset
244         else
245           @objects.select{|obj| obj.class == last_object_class}.size
246         end
247       else
248         nil
249       end
250     end
251   end
252
253   helper_method :next_page_href
254   def next_page_href with_params={}
255     if next_page_offset
256       url_for with_params.merge(offset: next_page_offset)
257     end
258   end
259
260   helper_method :next_page_filters
261   def next_page_filters nextpage_operator
262     next_page_filters = @filters.reject do |attr, op, val|
263       (attr == 'created_at' and op == nextpage_operator) or
264       (attr == 'uuid' and op == 'not in')
265     end
266
267     if @objects.any?
268       last_created_at = @objects.last.created_at
269
270       last_uuids = []
271       @objects.each do |obj|
272         last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
273       end
274
275       next_page_filters += [['created_at', nextpage_operator, last_created_at]]
276       next_page_filters += [['uuid', 'not in', last_uuids]]
277     end
278
279     next_page_filters
280   end
281
282   def show
283     if !@object
284       return render_not_found("object not found")
285     end
286     respond_to do |f|
287       f.json do
288         extra_attrs = { href: url_for(action: :show, id: @object) }
289         @object.textile_attributes.each do |textile_attr|
290           extra_attrs.merge!({ "#{textile_attr}Textile" => view_context.render_markup(@object.attributes[textile_attr]) })
291         end
292         render json: @object.attributes.merge(extra_attrs)
293       end
294       f.html {
295         if params['tab_pane']
296           render_pane(if params['tab_pane'].is_a? Hash then params['tab_pane']["name"] else params['tab_pane'] end)
297         elsif request.request_method.in? ['GET', 'HEAD']
298           render
299         else
300           redirect_to (params[:return_to] ||
301                        polymorphic_url(@object,
302                                        anchor: params[:redirect_to_anchor]))
303         end
304       }
305       f.js { render }
306     end
307   end
308
309   def redirect_to uri, *args
310     if request.xhr?
311       if not uri.is_a? String
312         uri = polymorphic_url(uri)
313       end
314       render json: {href: uri}
315     else
316       super
317     end
318   end
319
320   def choose
321     params[:limit] ||= 40
322     respond_to do |f|
323       if params[:partial]
324         f.json {
325           find_objects_for_index if !@objects
326           render json: {
327             content: render_to_string(partial: "choose_rows.html",
328                                       formats: [:html]),
329             next_page_href: next_page_href(partial: params[:partial])
330           }
331         }
332       end
333       f.js {
334         find_objects_for_index if !@objects
335         render partial: 'choose', locals: {multiple: params[:multiple]}
336       }
337     end
338   end
339
340   def render_content
341     if !@object
342       return render_not_found("object not found")
343     end
344   end
345
346   def new
347     @object = model_class.new
348   end
349
350   def update
351     @updates ||= params[@object.resource_param_name.to_sym]
352     @updates.keys.each do |attr|
353       if @object.send(attr).is_a? Hash
354         if @updates[attr].is_a? String
355           @updates[attr] = Oj.load @updates[attr]
356         end
357         if params[:merge] || params["merge_#{attr}".to_sym]
358           # Merge provided Hash with current Hash, instead of
359           # replacing.
360           @updates[attr] = @object.send(attr).with_indifferent_access.
361             deep_merge(@updates[attr].with_indifferent_access)
362         end
363       end
364     end
365     if @object.update_attributes @updates
366       show
367     else
368       self.render_error status: 422
369     end
370   end
371
372   def create
373     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
374     @new_resource_attrs ||= {}
375     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
376     @object ||= model_class.new @new_resource_attrs, params["options"]
377
378     if @object.save
379       show
380     else
381       render_error status: 422
382     end
383   end
384
385   # Clone the given object, merging any attribute values supplied as
386   # with a create action.
387   def copy
388     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
389     @new_resource_attrs ||= {}
390     @object = @object.dup
391     @object.update_attributes @new_resource_attrs
392     if not @new_resource_attrs[:name] and @object.respond_to? :name
393       if @object.name and @object.name != ''
394         @object.name = "Copy of #{@object.name}"
395       else
396         @object.name = ""
397       end
398     end
399     @object.save!
400     show
401   end
402
403   def destroy
404     if @object.destroy
405       respond_to do |f|
406         f.json { render json: @object }
407         f.html {
408           redirect_to(params[:return_to] || :back)
409         }
410         f.js { render }
411       end
412     else
413       self.render_error status: 422
414     end
415   end
416
417   def current_user
418     Thread.current[:user]
419   end
420
421   def model_class
422     controller_name.classify.constantize
423   end
424
425   def breadcrumb_page_name
426     (@breadcrumb_page_name ||
427      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
428      action_name)
429   end
430
431   def index_pane_list
432     %w(Recent)
433   end
434
435   def show_pane_list
436     %w(Attributes Advanced)
437   end
438
439   def set_share_links
440     @user_is_manager = false
441     @share_links = []
442
443     if @object.uuid != current_user.andand.uuid
444       begin
445         @share_links = Link.permissions_for(@object)
446         @user_is_manager = true
447       rescue ArvadosApiClient::AccessForbiddenException,
448         ArvadosApiClient::NotFoundException
449       end
450     end
451   end
452
453   def share_with
454     if not params[:uuids].andand.any?
455       @errors = ["No user/group UUIDs specified to share with."]
456       return render_error(status: 422)
457     end
458     results = {"success" => [], "errors" => []}
459     params[:uuids].each do |shared_uuid|
460       begin
461         Link.create(tail_uuid: shared_uuid, link_class: "permission",
462                     name: "can_read", head_uuid: @object.uuid)
463       rescue ArvadosApiClient::ApiError => error
464         error_list = error.api_response.andand[:errors]
465         if error_list.andand.any?
466           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
467         else
468           error_code = error.api_status || "Bad status"
469           results["errors"] << "#{shared_uuid}: #{error_code} response"
470         end
471       else
472         results["success"] << shared_uuid
473       end
474     end
475     if results["errors"].empty?
476       results.delete("errors")
477       status = 200
478     else
479       status = 422
480     end
481     respond_to do |f|
482       f.json { render(json: results, status: status) }
483     end
484   end
485
486   helper_method :is_starred
487   def is_starred
488     links = Link.where(tail_uuid: current_user.uuid,
489                head_uuid: @object.uuid,
490                link_class: 'star')
491
492     return links.andand.any?
493   end
494
495   protected
496
497   helper_method :strip_token_from_path
498   def strip_token_from_path(path)
499     path.sub(/([\?&;])api_token=[^&;]*[&;]?/, '\1')
500   end
501
502   def redirect_to_login
503     if request.xhr? or request.format.json?
504       @errors = ['You are not logged in. Most likely your session has timed out and you need to log in again.']
505       render_error status: 401
506     elsif request.method.in? ['GET', 'HEAD']
507       redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url))
508     else
509       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."
510       redirect_to :back
511     end
512     false  # For convenience to return from callbacks
513   end
514
515   def using_specific_api_token(api_token, opts={})
516     start_values = {}
517     [:arvados_api_token, :user].each do |key|
518       start_values[key] = Thread.current[key]
519     end
520     if opts.fetch(:load_user, true)
521       load_api_token(api_token)
522     else
523       Thread.current[:arvados_api_token] = api_token
524       Thread.current[:user] = nil
525     end
526     begin
527       yield
528     ensure
529       start_values.each_key { |key| Thread.current[key] = start_values[key] }
530     end
531   end
532
533
534   def accept_uuid_as_id_param
535     if params[:id] and params[:id].match /\D/
536       params[:uuid] = params.delete :id
537     end
538   end
539
540   def find_object_by_uuid
541     begin
542       if not model_class
543         @object = nil
544       elsif params[:uuid].nil? or params[:uuid].empty?
545         @object = nil
546       elsif not params[:uuid].is_a?(String)
547         @object = model_class.where(uuid: params[:uuid]).first
548       elsif (model_class != Link and
549              resource_class_for_uuid(params[:uuid]) == Link)
550         @name_link = Link.find(params[:uuid])
551         @object = model_class.find(@name_link.head_uuid)
552       else
553         @object = model_class.find(params[:uuid])
554         load_preloaded_objects [@object]
555       end
556     rescue ArvadosApiClient::NotFoundException, ArvadosApiClient::NotLoggedInException, RuntimeError => error
557       if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/)
558         raise
559       end
560       render_not_found(error)
561       return false
562     end
563   end
564
565   def thread_clear
566     load_api_token(nil)
567     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
568     yield
569     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
570   end
571
572   # Set up the thread with the given API token and associated user object.
573   def load_api_token(new_token)
574     Thread.current[:arvados_api_token] = new_token
575     if new_token.nil?
576       Thread.current[:user] = nil
577     else
578       Thread.current[:user] = User.current
579     end
580   end
581
582   # If there's a valid api_token parameter, set up the session with that
583   # user's information.  Return true if the method redirects the request
584   # (usually a post-login redirect); false otherwise.
585   def setup_user_session
586     return false unless params[:api_token]
587     Thread.current[:arvados_api_token] = params[:api_token]
588     begin
589       user = User.current
590     rescue ArvadosApiClient::NotLoggedInException
591       false  # We may redirect to login, or not, based on the current action.
592     else
593       session[:arvados_api_token] = params[:api_token]
594       # If we later have trouble contacting the API server, we still want
595       # to be able to render basic user information in the UI--see
596       # render_exception above.  We store that in the session here.  This is
597       # not intended to be used as a general-purpose cache.  See #2891.
598       session[:user] = {
599         uuid: user.uuid,
600         email: user.email,
601         first_name: user.first_name,
602         last_name: user.last_name,
603         is_active: user.is_active,
604         is_admin: user.is_admin,
605         prefs: user.prefs
606       }
607
608       if !request.format.json? and request.method.in? ['GET', 'HEAD']
609         # Repeat this request with api_token in the (new) session
610         # cookie instead of the query string.  This prevents API
611         # tokens from appearing in (and being inadvisedly copied
612         # and pasted from) browser Location bars.
613         redirect_to strip_token_from_path(request.fullpath)
614         true
615       else
616         false
617       end
618     ensure
619       Thread.current[:arvados_api_token] = nil
620     end
621   end
622
623   # Save the session API token in thread-local storage, and yield.
624   # This method also takes care of session setup if the request
625   # provides a valid api_token parameter.
626   # If a token is unavailable or expired, the block is still run, with
627   # a nil token.
628   def set_thread_api_token
629     if Thread.current[:arvados_api_token]
630       yield   # An API token has already been found - pass it through.
631       return
632     elsif setup_user_session
633       return  # A new session was set up and received a response.
634     end
635
636     begin
637       load_api_token(session[:arvados_api_token])
638       yield
639     rescue ArvadosApiClient::NotLoggedInException
640       # If we got this error with a token, it must've expired.
641       # Retry the request without a token.
642       unless Thread.current[:arvados_api_token].nil?
643         load_api_token(nil)
644         yield
645       end
646     ensure
647       # Remove token in case this Thread is used for anything else.
648       load_api_token(nil)
649     end
650   end
651
652   # Redirect to login/welcome if client provided expired API token (or
653   # none at all)
654   def require_thread_api_token
655     if Thread.current[:arvados_api_token]
656       yield
657     elsif session[:arvados_api_token]
658       # Expired session. Clear it before refreshing login so that,
659       # if this login procedure fails, we end up showing the "please
660       # log in" page instead of getting stuck in a redirect loop.
661       session.delete :arvados_api_token
662       redirect_to_login
663     elsif request.xhr?
664       # If we redirect to the welcome page, the browser will handle
665       # the 302 by itself and the client code will end up rendering
666       # the "welcome" page in some content area where it doesn't make
667       # sense. Instead, we send 401 ("authenticate and try again" or
668       # "display error", depending on how smart the client side is).
669       @errors = ['You are not logged in.']
670       render_error status: 401
671     else
672       redirect_to welcome_users_path(return_to: request.fullpath)
673     end
674   end
675
676   def ensure_current_user_is_admin
677     if not current_user
678       @errors = ['Not logged in']
679       render_error status: 401
680     elsif not current_user.is_admin
681       @errors = ['Permission denied']
682       render_error status: 403
683     end
684   end
685
686   helper_method :unsigned_user_agreements
687   def unsigned_user_agreements
688     @signed_ua_uuids ||= UserAgreement.signatures.map &:head_uuid
689     @unsigned_user_agreements ||= UserAgreement.all.map do |ua|
690       if not @signed_ua_uuids.index ua.uuid
691         Collection.find(ua.uuid)
692       end
693     end.compact
694   end
695
696   def check_user_agreements
697     if current_user && !current_user.is_active
698       if not current_user.is_invited
699         return redirect_to inactive_users_path(return_to: request.fullpath)
700       end
701       if unsigned_user_agreements.empty?
702         # No agreements to sign. Perhaps we just need to ask?
703         current_user.activate
704         if !current_user.is_active
705           logger.warn "#{current_user.uuid.inspect}: " +
706             "No user agreements to sign, but activate failed!"
707         end
708       end
709       if !current_user.is_active
710         redirect_to user_agreements_path(return_to: request.fullpath)
711       end
712     end
713     true
714   end
715
716   def check_user_profile
717     return true if !current_user
718     if request.method.downcase != 'get' || params[:partial] ||
719        params[:tab_pane] || params[:action_method] ||
720        params[:action] == 'setup_popup'
721       return true
722     end
723
724     if missing_required_profile?
725       redirect_to profile_user_path(current_user.uuid, return_to: request.fullpath)
726     end
727     true
728   end
729
730   helper_method :missing_required_profile?
731   def missing_required_profile?
732     missing_required = false
733
734     profile_config = Rails.configuration.user_profile_form_fields
735     if current_user && profile_config
736       current_user_profile = current_user.prefs[:profile]
737       profile_config.kind_of?(Array) && profile_config.andand.each do |entry|
738         if entry['required']
739           if !current_user_profile ||
740              !current_user_profile[entry['key'].to_sym] ||
741              current_user_profile[entry['key'].to_sym].empty?
742             missing_required = true
743             break
744           end
745         end
746       end
747     end
748
749     missing_required
750   end
751
752   def select_theme
753     return Rails.configuration.arvados_theme
754   end
755
756   @@notification_tests = []
757
758   @@notification_tests.push lambda { |controller, current_user|
759     return nil if Rails.configuration.shell_in_a_box_url
760     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
761       return nil
762     end
763     return lambda { |view|
764       view.render partial: 'notifications/ssh_key_notification'
765     }
766   }
767
768   @@notification_tests.push lambda { |controller, current_user|
769     Collection.limit(1).where(created_by: current_user.uuid).each do
770       return nil
771     end
772     return lambda { |view|
773       view.render partial: 'notifications/collections_notification'
774     }
775   }
776
777   @@notification_tests.push lambda { |controller, current_user|
778     if PipelineInstance.api_exists?(:index)
779       PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
780         return nil
781       end
782     else
783       return nil
784     end
785     return lambda { |view|
786       view.render partial: 'notifications/pipelines_notification'
787     }
788   }
789
790   helper_method :user_notifications
791   def user_notifications
792     return [] if @errors or not current_user.andand.is_active or not Rails.configuration.show_user_notifications
793     @notifications ||= @@notification_tests.map do |t|
794       t.call(self, current_user)
795     end.compact
796   end
797
798   helper_method :all_projects
799   def all_projects
800     @all_projects ||= Group.
801       filter([['group_class','=','project']]).order('name')
802   end
803
804   helper_method :my_projects
805   def my_projects
806     return @my_projects if @my_projects
807     @my_projects = []
808     root_of = {}
809     all_projects.each do |g|
810       root_of[g.uuid] = g.owner_uuid
811       @my_projects << g
812     end
813     done = false
814     while not done
815       done = true
816       root_of = root_of.each_with_object({}) do |(child, parent), h|
817         if root_of[parent]
818           h[child] = root_of[parent]
819           done = false
820         else
821           h[child] = parent
822         end
823       end
824     end
825     @my_projects = @my_projects.select do |g|
826       root_of[g.uuid] == current_user.uuid
827     end
828   end
829
830   helper_method :projects_shared_with_me
831   def projects_shared_with_me
832     my_project_uuids = my_projects.collect &:uuid
833     all_projects.reject { |x| x.uuid.in? my_project_uuids }
834   end
835
836   helper_method :recent_jobs_and_pipelines
837   def recent_jobs_and_pipelines
838     (Job.limit(10) |
839      PipelineInstance.limit(10)).
840       sort_by do |x|
841       (x.finished_at || x.started_at rescue nil) || x.modified_at || x.created_at
842     end.reverse
843   end
844
845   helper_method :running_pipelines
846   def running_pipelines
847     pi = PipelineInstance.order(["started_at asc", "created_at asc"]).filter([["state", "in", ["RunningOnServer", "RunningOnClient"]]])
848     jobs = {}
849     pi.each do |pl|
850       pl.components.each do |k,v|
851         if v.is_a? Hash and v[:job]
852           jobs[v[:job][:uuid]] = {}
853         end
854       end
855     end
856
857     if jobs.keys.any?
858       Job.filter([["uuid", "in", jobs.keys]]).each do |j|
859         jobs[j[:uuid]] = j
860       end
861
862       pi.each do |pl|
863         pl.components.each do |k,v|
864           if v.is_a? Hash and v[:job]
865             v[:job] = jobs[v[:job][:uuid]]
866           end
867         end
868       end
869     end
870
871     pi
872   end
873
874   helper_method :recent_processes
875   def recent_processes lim
876     lim = 12 if lim.nil?
877
878     procs = {}
879     if PipelineInstance.api_exists?(:index)
880       cols = %w(uuid owner_uuid created_at modified_at pipeline_template_uuid name state started_at finished_at)
881       pipelines = PipelineInstance.select(cols).limit(lim).order(["created_at desc"])
882       pipelines.results.each { |pi| procs[pi] = pi.created_at }
883     end
884
885     crs = ContainerRequest.limit(lim).order(["created_at desc"]).filter([["requesting_container_uuid", "=", nil]])
886     crs.results.each { |c| procs[c] = c.created_at }
887
888     Hash[procs.sort_by {|key, value| value}].keys.reverse.first(lim)
889   end
890
891   helper_method :recent_collections
892   def recent_collections lim
893     c = Collection.limit(lim).order(["modified_at desc"]).results
894     own = {}
895     Group.filter([["uuid", "in", c.map(&:owner_uuid)]]).each do |g|
896       own[g[:uuid]] = g
897     end
898     {collections: c, owners: own}
899   end
900
901   helper_method :my_starred_projects
902   def my_starred_projects user
903     return if @starred_projects
904     links = Link.filter([['tail_uuid', '=', user.uuid],
905                          ['link_class', '=', 'star'],
906                          ['head_uuid', 'is_a', 'arvados#group']]).select(%w(head_uuid))
907     uuids = links.collect { |x| x.head_uuid }
908     starred_projects = Group.filter([['uuid', 'in', uuids]]).order('name')
909     @starred_projects = starred_projects.results
910   end
911
912   # If there are more than 200 projects that are readable by the user,
913   # build the tree using only the top 200+ projects owned by the user,
914   # from the top three levels.
915   # That is: get toplevel projects under home, get subprojects of
916   # these projects, and so on until we hit the limit.
917   def my_wanted_projects(user, page_size=100)
918     return @my_wanted_projects if @my_wanted_projects
919
920     from_top = []
921     uuids = [user.uuid]
922     depth = 0
923     @too_many_projects = false
924     @reached_level_limit = false
925     while from_top.size <= page_size*2
926       current_level = Group.filter([['group_class','=','project'],
927                                     ['owner_uuid', 'in', uuids]])
928                       .order('name').limit(page_size*2)
929       break if current_level.results.size == 0
930       @too_many_projects = true if current_level.items_available > current_level.results.size
931       from_top.concat current_level.results
932       uuids = current_level.results.collect(&:uuid)
933       depth += 1
934       if depth >= 3
935         @reached_level_limit = true
936         break
937       end
938     end
939     @my_wanted_projects = from_top
940   end
941
942   helper_method :my_wanted_projects_tree
943   def my_wanted_projects_tree(user, page_size=100)
944     build_my_wanted_projects_tree(user, page_size)
945     [@my_wanted_projects_tree, @too_many_projects, @reached_level_limit]
946   end
947
948   def build_my_wanted_projects_tree(user, page_size=100)
949     return @my_wanted_projects_tree if @my_wanted_projects_tree
950
951     parent_of = {user.uuid => 'me'}
952     my_wanted_projects(user, page_size).each do |ob|
953       parent_of[ob.uuid] = ob.owner_uuid
954     end
955     children_of = {false => [], 'me' => [user]}
956     my_wanted_projects(user, page_size).each do |ob|
957       if ob.owner_uuid != user.uuid and
958           not parent_of.has_key? ob.owner_uuid
959         parent_of[ob.uuid] = false
960       end
961       children_of[parent_of[ob.uuid]] ||= []
962       children_of[parent_of[ob.uuid]] << ob
963     end
964     buildtree = lambda do |children_of, root_uuid=false|
965       tree = {}
966       children_of[root_uuid].andand.each do |ob|
967         tree[ob] = buildtree.call(children_of, ob.uuid)
968       end
969       tree
970     end
971     sorted_paths = lambda do |tree, depth=0|
972       paths = []
973       tree.keys.sort_by { |ob|
974         ob.is_a?(String) ? ob : ob.friendly_link_name
975       }.each do |ob|
976         paths << {object: ob, depth: depth}
977         paths += sorted_paths.call tree[ob], depth+1
978       end
979       paths
980     end
981     @my_wanted_projects_tree =
982       sorted_paths.call buildtree.call(children_of, 'me')
983   end
984
985   helper_method :get_object
986   def get_object uuid
987     if @get_object.nil? and @objects
988       @get_object = @objects.each_with_object({}) do |object, h|
989         h[object.uuid] = object
990       end
991     end
992     @get_object ||= {}
993     @get_object[uuid]
994   end
995
996   helper_method :project_breadcrumbs
997   def project_breadcrumbs
998     crumbs = []
999     current = @name_link || @object
1000     while current
1001       # Halt if a group ownership loop is detected. API should refuse
1002       # to produce this state, but it could still arise from a race
1003       # condition when group ownership changes between our find()
1004       # queries.
1005       break if crumbs.collect(&:uuid).include? current.uuid
1006
1007       if current.is_a?(Group) and current.group_class == 'project'
1008         crumbs.prepend current
1009       end
1010       if current.is_a? Link
1011         current = Group.find?(current.tail_uuid)
1012       else
1013         current = Group.find?(current.owner_uuid)
1014       end
1015     end
1016     crumbs
1017   end
1018
1019   helper_method :current_project_uuid
1020   def current_project_uuid
1021     if @object.is_a? Group and @object.group_class == 'project'
1022       @object.uuid
1023     elsif @name_link.andand.tail_uuid
1024       @name_link.tail_uuid
1025     elsif @object and resource_class_for_uuid(@object.owner_uuid) == Group
1026       @object.owner_uuid
1027     else
1028       nil
1029     end
1030   end
1031
1032   # helper method to get links for given object or uuid
1033   helper_method :links_for_object
1034   def links_for_object object_or_uuid
1035     raise ArgumentError, 'No input argument' unless object_or_uuid
1036     preload_links_for_objects([object_or_uuid])
1037     uuid = object_or_uuid.is_a?(String) ? object_or_uuid : object_or_uuid.uuid
1038     @all_links_for[uuid] ||= []
1039   end
1040
1041   # helper method to preload links for given objects and uuids
1042   helper_method :preload_links_for_objects
1043   def preload_links_for_objects objects_and_uuids
1044     @all_links_for ||= {}
1045
1046     raise ArgumentError, 'Argument is not an array' unless objects_and_uuids.is_a? Array
1047     return @all_links_for if objects_and_uuids.empty?
1048
1049     uuids = objects_and_uuids.collect { |x| x.is_a?(String) ? x : x.uuid }
1050
1051     # if already preloaded for all of these uuids, return
1052     if not uuids.select { |x| @all_links_for[x].nil? }.any?
1053       return @all_links_for
1054     end
1055
1056     uuids.each do |x|
1057       @all_links_for[x] = []
1058     end
1059
1060     # TODO: make sure we get every page of results from API server
1061     Link.filter([['head_uuid', 'in', uuids]]).each do |link|
1062       @all_links_for[link.head_uuid] << link
1063     end
1064     @all_links_for
1065   end
1066
1067   # helper method to get a certain number of objects of a specific type
1068   # this can be used to replace any uses of: "dataclass.limit(n)"
1069   helper_method :get_n_objects_of_class
1070   def get_n_objects_of_class dataclass, size
1071     @objects_map_for ||= {}
1072
1073     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class and dataclass < ArvadosBase
1074     raise ArgumentError, 'Argument is not a valid limit size' unless (size && size>0)
1075
1076     # if the objects_map_for has a value for this dataclass, and the
1077     # size used to retrieve those objects is equal, return it
1078     size_key = "#{dataclass.name}_size"
1079     if @objects_map_for[dataclass.name] && @objects_map_for[size_key] &&
1080         (@objects_map_for[size_key] == size)
1081       return @objects_map_for[dataclass.name]
1082     end
1083
1084     @objects_map_for[size_key] = size
1085     @objects_map_for[dataclass.name] = dataclass.limit(size)
1086   end
1087
1088   # helper method to get collections for the given uuid
1089   helper_method :collections_for_object
1090   def collections_for_object uuid
1091     raise ArgumentError, 'No input argument' unless uuid
1092     preload_collections_for_objects([uuid])
1093     @all_collections_for[uuid] ||= []
1094   end
1095
1096   # helper method to preload collections for the given uuids
1097   helper_method :preload_collections_for_objects
1098   def preload_collections_for_objects uuids
1099     @all_collections_for ||= {}
1100
1101     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
1102     return @all_collections_for if uuids.empty?
1103
1104     # if already preloaded for all of these uuids, return
1105     if not uuids.select { |x| @all_collections_for[x].nil? }.any?
1106       return @all_collections_for
1107     end
1108
1109     uuids.each do |x|
1110       @all_collections_for[x] = []
1111     end
1112
1113     # TODO: make sure we get every page of results from API server
1114     Collection.where(uuid: uuids).each do |collection|
1115       @all_collections_for[collection.uuid] << collection
1116     end
1117     @all_collections_for
1118   end
1119
1120   # helper method to get log collections for the given log
1121   helper_method :log_collections_for_object
1122   def log_collections_for_object log
1123     raise ArgumentError, 'No input argument' unless log
1124
1125     preload_log_collections_for_objects([log])
1126
1127     uuid = log
1128     fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
1129     if fixup && fixup.size>1
1130       uuid = fixup[1]
1131     end
1132
1133     @all_log_collections_for[uuid] ||= []
1134   end
1135
1136   # helper method to preload collections for the given uuids
1137   helper_method :preload_log_collections_for_objects
1138   def preload_log_collections_for_objects logs
1139     @all_log_collections_for ||= {}
1140
1141     raise ArgumentError, 'Argument is not an array' unless logs.is_a? Array
1142     return @all_log_collections_for if logs.empty?
1143
1144     uuids = []
1145     logs.each do |log|
1146       fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
1147       if fixup && fixup.size>1
1148         uuids << fixup[1]
1149       else
1150         uuids << log
1151       end
1152     end
1153
1154     # if already preloaded for all of these uuids, return
1155     if not uuids.select { |x| @all_log_collections_for[x].nil? }.any?
1156       return @all_log_collections_for
1157     end
1158
1159     uuids.each do |x|
1160       @all_log_collections_for[x] = []
1161     end
1162
1163     # TODO: make sure we get every page of results from API server
1164     Collection.where(uuid: uuids).each do |collection|
1165       @all_log_collections_for[collection.uuid] << collection
1166     end
1167     @all_log_collections_for
1168   end
1169
1170   # Helper method to get one collection for the given portable_data_hash
1171   # This is used to determine if a pdh is readable by the current_user
1172   helper_method :collection_for_pdh
1173   def collection_for_pdh pdh
1174     raise ArgumentError, 'No input argument' unless pdh
1175     preload_for_pdhs([pdh])
1176     @all_pdhs_for[pdh] ||= []
1177   end
1178
1179   # Helper method to preload one collection each for the given pdhs
1180   # This is used to determine if a pdh is readable by the current_user
1181   helper_method :preload_for_pdhs
1182   def preload_for_pdhs pdhs
1183     @all_pdhs_for ||= {}
1184
1185     raise ArgumentError, 'Argument is not an array' unless pdhs.is_a? Array
1186     return @all_pdhs_for if pdhs.empty?
1187
1188     # if already preloaded for all of these pdhs, return
1189     if not pdhs.select { |x| @all_pdhs_for[x].nil? }.any?
1190       return @all_pdhs_for
1191     end
1192
1193     pdhs.each do |x|
1194       @all_pdhs_for[x] = []
1195     end
1196
1197     Collection.select(%w(portable_data_hash)).where(portable_data_hash: pdhs).distinct().each do |collection|
1198       @all_pdhs_for[collection.portable_data_hash] << collection
1199     end
1200     @all_pdhs_for
1201   end
1202
1203   # helper method to get object of a given dataclass and uuid
1204   helper_method :object_for_dataclass
1205   def object_for_dataclass dataclass, uuid, by_attr=nil
1206     raise ArgumentError, 'No input argument dataclass' unless (dataclass && uuid)
1207     preload_objects_for_dataclass(dataclass, [uuid], by_attr)
1208     @objects_for[uuid]
1209   end
1210
1211   # helper method to preload objects for given dataclass and uuids
1212   helper_method :preload_objects_for_dataclass
1213   def preload_objects_for_dataclass dataclass, uuids, by_attr=nil
1214     @objects_for ||= {}
1215
1216     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class
1217     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
1218
1219     return @objects_for if uuids.empty?
1220
1221     # if already preloaded for all of these uuids, return
1222     if not uuids.select { |x| !@objects_for.include?(x) }.any?
1223       return @objects_for
1224     end
1225
1226     # preset all uuids to nil
1227     uuids.each do |x|
1228       @objects_for[x] = nil
1229     end
1230     if by_attr and ![:uuid, :name].include?(by_attr)
1231       raise ArgumentError, "Preloading only using lookups by uuid or name are supported: #{by_attr}"
1232     elsif by_attr and by_attr == :name
1233       dataclass.where(name: uuids).each do |obj|
1234         @objects_for[obj.name] = obj
1235       end
1236     else
1237       dataclass.where(uuid: uuids).each do |obj|
1238         @objects_for[obj.uuid] = obj
1239       end
1240     end
1241     @objects_for
1242   end
1243
1244   # helper method to load objects that are already preloaded
1245   helper_method :load_preloaded_objects
1246   def load_preloaded_objects objs
1247     @objects_for ||= {}
1248     objs.each do |obj|
1249       @objects_for[obj.uuid] = obj
1250     end
1251   end
1252
1253   def wiselinks_layout
1254     'body'
1255   end
1256
1257   def set_current_request_id
1258     # Request ID format: '<timestamp>-<9_digits_random_number>'
1259     current_request_id = "#{Time.new.to_i}-#{sprintf('%09d', rand(0..10**9-1))}"
1260     Thread.current[:current_request_id] = current_request_id
1261   end
1262 end