Merge branch '3444-no-folders' closes #3444
[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 :set_thread_api_token
12   # Methods that don't require login should
13   #   skip_around_filter :require_thread_api_token
14   around_filter :require_thread_api_token, except: ERROR_ACTIONS
15   before_filter :check_user_agreements, except: ERROR_ACTIONS
16   before_filter :check_user_notifications, except: ERROR_ACTIONS
17   before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
18   before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS
19   theme :select_theme
20
21   begin
22     rescue_from(ActiveRecord::RecordNotFound,
23                 ActionController::RoutingError,
24                 ActionController::UnknownController,
25                 AbstractController::ActionNotFound,
26                 with: :render_not_found)
27     rescue_from(Exception,
28                 ActionController::UrlGenerationError,
29                 with: :render_exception)
30   end
31
32   def unprocessable(message=nil)
33     @errors ||= []
34
35     @errors << message if message
36     render_error status: 422
37   end
38
39   def render_error(opts={})
40     opts[:status] ||= 500
41     respond_to do |f|
42       # json must come before html here, so it gets used as the
43       # default format when js is requested by the client. This lets
44       # ajax:error callback parse the response correctly, even though
45       # the browser can't.
46       f.json { render opts.merge(json: {success: false, errors: @errors}) }
47       f.html { render({action: 'error'}.merge(opts)) }
48     end
49   end
50
51   def render_exception(e)
52     logger.error e.inspect
53     logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
54     err_opts = {status: 422}
55     if e.is_a?(ArvadosApiClient::ApiError)
56       err_opts.merge!(action: 'api_error', locals: {api_error: e})
57       @errors = e.api_response[:errors]
58     elsif @object.andand.errors.andand.full_messages.andand.any?
59       @errors = @object.errors.full_messages
60     else
61       @errors = [e.to_s]
62     end
63     # If the user has an active session, and the API server is available,
64     # make user information available on the error page.
65     begin
66       load_api_token(session[:arvados_api_token])
67     rescue ArvadosApiClient::ApiError
68       load_api_token(nil)
69     end
70     # Preload projects trees for the template.  If that fails, set empty
71     # trees so error page rendering can proceed.  (It's easier to rescue the
72     # exception here than in a template.)
73     begin
74       build_project_trees
75     rescue ArvadosApiClient::ApiError
76       @my_project_tree ||= []
77       @shared_project_tree ||= []
78     end
79     render_error(err_opts)
80   end
81
82   def render_not_found(e=ActionController::RoutingError.new("Path not found"))
83     logger.error e.inspect
84     @errors = ["Path not found"]
85     set_thread_api_token do
86       self.render_error(action: '404', status: 404)
87     end
88   end
89
90   def load_filters_and_paging_params
91     @limit ||= 200
92     if params[:limit]
93       @limit = params[:limit].to_i
94     end
95
96     @offset ||= 0
97     if params[:offset]
98       @offset = params[:offset].to_i
99     end
100
101     @filters ||= []
102     if params[:filters]
103       filters = params[:filters]
104       if filters.is_a? String
105         filters = Oj.load filters
106       elsif filters.is_a? Array
107         filters = filters.collect do |filter|
108           if filter.is_a? String
109             # Accept filters[]=["foo","=","bar"]
110             Oj.load filter
111           else
112             # Accept filters=[["foo","=","bar"]]
113             filter
114           end
115         end
116       end
117       @filters += filters
118     end
119   end
120
121   def find_objects_for_index
122     @objects ||= model_class
123     @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
124   end
125
126   def render_index
127     respond_to do |f|
128       f.json { render json: @objects }
129       f.html {
130         if params['tab_pane']
131           comparable = self.respond_to? :compare
132           render(partial: 'show_' + params['tab_pane'].downcase,
133                  locals: { comparable: comparable, objects: @objects })
134         else
135           render
136         end
137       }
138       f.js { render }
139     end
140   end
141
142   def index
143     find_objects_for_index if !@objects
144     render_index
145   end
146
147   helper_method :next_page_offset
148   def next_page_offset objects=nil
149     if !objects
150       objects = @objects
151     end
152     if objects.respond_to?(:result_offset) and
153         objects.respond_to?(:result_limit) and
154         objects.respond_to?(:items_available)
155       next_offset = objects.result_offset + objects.result_limit
156       if next_offset < objects.items_available
157         next_offset
158       else
159         nil
160       end
161     end
162   end
163
164   helper_method :next_page_href
165   def next_page_href with_params={}
166     if next_page_offset
167       url_for with_params.merge(offset: next_page_offset)
168     end
169   end
170
171   def show
172     if !@object
173       return render_not_found("object not found")
174     end
175     respond_to do |f|
176       f.json { render json: @object.attributes.merge(href: url_for(@object)) }
177       f.html {
178         if params['tab_pane']
179           comparable = self.respond_to? :compare
180           render(partial: 'show_' + params['tab_pane'].downcase,
181                  locals: { comparable: comparable, objects: @objects })
182         elsif request.method.in? ['GET', 'HEAD']
183           render
184         else
185           redirect_to params[:return_to] || @object
186         end
187       }
188       f.js { render }
189     end
190   end
191
192   def choose
193     params[:limit] ||= 40
194     find_objects_for_index if !@objects
195     respond_to do |f|
196       if params[:partial]
197         f.json {
198           render json: {
199             content: render_to_string(partial: "choose_rows.html",
200                                       formats: [:html]),
201             next_page_href: next_page_href(partial: params[:partial])
202           }
203         }
204       end
205       f.js {
206         render partial: 'choose', locals: {multiple: params[:multiple]}
207       }
208     end
209   end
210
211   def render_content
212     if !@object
213       return render_not_found("object not found")
214     end
215   end
216
217   def new
218     @object = model_class.new
219   end
220
221   def update
222     @updates ||= params[@object.resource_param_name.to_sym]
223     @updates.keys.each do |attr|
224       if @object.send(attr).is_a? Hash
225         if @updates[attr].is_a? String
226           @updates[attr] = Oj.load @updates[attr]
227         end
228         if params[:merge] || params["merge_#{attr}".to_sym]
229           # Merge provided Hash with current Hash, instead of
230           # replacing.
231           @updates[attr] = @object.send(attr).with_indifferent_access.
232             deep_merge(@updates[attr].with_indifferent_access)
233         end
234       end
235     end
236     if @object.update_attributes @updates
237       show
238     else
239       self.render_error status: 422
240     end
241   end
242
243   def create
244     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
245     @new_resource_attrs ||= {}
246     @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
247     @object ||= model_class.new @new_resource_attrs, params["options"]
248     if @object.save
249       respond_to do |f|
250         f.json { render json: @object.attributes.merge(href: url_for(@object)) }
251         f.html {
252           redirect_to @object
253         }
254         f.js { render }
255       end
256     else
257       self.render_error status: 422
258     end
259   end
260
261   # Clone the given object, merging any attribute values supplied as
262   # with a create action.
263   def copy
264     @new_resource_attrs ||= params[model_class.to_s.underscore.singularize]
265     @new_resource_attrs ||= {}
266     @object = @object.dup
267     @object.update_attributes @new_resource_attrs
268     if not @new_resource_attrs[:name] and @object.respond_to? :name
269       if @object.name and @object.name != ''
270         @object.name = "Copy of #{@object.name}"
271       else
272         @object.name = ""
273       end
274     end
275     @object.save!
276     show
277   end
278
279   def destroy
280     if @object.destroy
281       respond_to do |f|
282         f.json { render json: @object }
283         f.html {
284           redirect_to(params[:return_to] || :back)
285         }
286         f.js { render }
287       end
288     else
289       self.render_error status: 422
290     end
291   end
292
293   def current_user
294     Thread.current[:user]
295   end
296
297   def model_class
298     controller_name.classify.constantize
299   end
300
301   def breadcrumb_page_name
302     (@breadcrumb_page_name ||
303      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
304      action_name)
305   end
306
307   def index_pane_list
308     %w(Recent)
309   end
310
311   def show_pane_list
312     %w(Attributes Advanced)
313   end
314
315   protected
316
317   def strip_token_from_path(path)
318     path.sub(/([\?&;])api_token=[^&;]*[&;]?/, '\1')
319   end
320
321   def redirect_to_login
322     respond_to do |f|
323       f.html {
324         if request.method.in? ['GET', 'HEAD']
325           redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url))
326         else
327           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."
328           redirect_to :back
329         end
330       }
331       f.json {
332         @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.']
333         self.render_error status: 422
334       }
335     end
336     false  # For convenience to return from callbacks
337   end
338
339   def using_specific_api_token(api_token)
340     start_values = {}
341     [:arvados_api_token, :user].each do |key|
342       start_values[key] = Thread.current[key]
343     end
344     load_api_token(api_token)
345     begin
346       yield
347     ensure
348       start_values.each_key { |key| Thread.current[key] = start_values[key] }
349     end
350   end
351
352   def find_object_by_uuid
353     if params[:id] and params[:id].match /\D/
354       params[:uuid] = params.delete :id
355     end
356     begin
357       if not model_class
358         @object = nil
359       elsif not params[:uuid].is_a?(String)
360         @object = model_class.where(uuid: params[:uuid]).first
361       elsif params[:uuid].empty?
362         @object = nil
363       elsif (model_class != Link and
364              resource_class_for_uuid(params[:uuid]) == Link)
365         @name_link = Link.find(params[:uuid])
366         @object = model_class.find(@name_link.head_uuid)
367       else
368         @object = model_class.find(params[:uuid])
369       end
370     rescue ArvadosApiClient::NotFoundException, RuntimeError => error
371       if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/)
372         raise
373       end
374       render_not_found(error)
375       return false
376     end
377   end
378
379   def thread_clear
380     load_api_token(nil)
381     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
382     yield
383     Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
384   end
385
386   # Set up the thread with the given API token and associated user object.
387   def load_api_token(new_token)
388     Thread.current[:arvados_api_token] = new_token
389     if new_token.nil?
390       Thread.current[:user] = nil
391     elsif (new_token == session[:arvados_api_token]) and
392         session[:user].andand[:is_active]
393       Thread.current[:user] = User.new(session[:user])
394     else
395       Thread.current[:user] = User.current
396     end
397   end
398
399   # If there's a valid api_token parameter, set up the session with that
400   # user's information.  Return true if the method redirects the request
401   # (usually a post-login redirect); false otherwise.
402   def setup_user_session
403     return false unless params[:api_token]
404     Thread.current[:arvados_api_token] = params[:api_token]
405     begin
406       user = User.current
407     rescue ArvadosApiClient::NotLoggedInException
408       false  # We may redirect to login, or not, based on the current action.
409     else
410       session[:arvados_api_token] = params[:api_token]
411       session[:user] = {
412         uuid: user.uuid,
413         email: user.email,
414         first_name: user.first_name,
415         last_name: user.last_name,
416         is_active: user.is_active,
417         is_admin: user.is_admin,
418         prefs: user.prefs
419       }
420       if !request.format.json? and request.method.in? ['GET', 'HEAD']
421         # Repeat this request with api_token in the (new) session
422         # cookie instead of the query string.  This prevents API
423         # tokens from appearing in (and being inadvisedly copied
424         # and pasted from) browser Location bars.
425         redirect_to strip_token_from_path(request.fullpath)
426         true
427       else
428         false
429       end
430     ensure
431       Thread.current[:arvados_api_token] = nil
432     end
433   end
434
435   # Save the session API token in thread-local storage, and yield.
436   # This method also takes care of session setup if the request
437   # provides a valid api_token parameter.
438   # If a token is unavailable or expired, the block is still run, with
439   # a nil token.
440   def set_thread_api_token
441     if Thread.current[:arvados_api_token]
442       yield   # An API token has already been found - pass it through.
443       return
444     elsif setup_user_session
445       return  # A new session was set up and received a response.
446     end
447
448     begin
449       load_api_token(session[:arvados_api_token])
450       yield
451     rescue ArvadosApiClient::NotLoggedInException
452       # If we got this error with a token, it must've expired.
453       # Retry the request without a token.
454       unless Thread.current[:arvados_api_token].nil?
455         load_api_token(nil)
456         yield
457       end
458     ensure
459       # Remove token in case this Thread is used for anything else.
460       load_api_token(nil)
461     end
462   end
463
464   # Reroute this request if an API token is unavailable.
465   def require_thread_api_token
466     if Thread.current[:arvados_api_token]
467       yield
468     elsif session[:arvados_api_token]
469       # Expired session. Clear it before refreshing login so that,
470       # if this login procedure fails, we end up showing the "please
471       # log in" page instead of getting stuck in a redirect loop.
472       session.delete :arvados_api_token
473       redirect_to_login
474     else
475       render 'users/welcome'
476     end
477   end
478
479   def ensure_current_user_is_admin
480     unless current_user and current_user.is_admin
481       @errors = ['Permission denied']
482       self.render_error status: 401
483     end
484   end
485
486   def check_user_agreements
487     if current_user && !current_user.is_active
488       if not current_user.is_invited
489         return render 'users/inactive'
490       end
491       signatures = UserAgreement.signatures
492       @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid
493       @required_user_agreements = UserAgreement.all.map do |ua|
494         if not @signed_ua_uuids.index ua.uuid
495           Collection.find(ua.uuid)
496         end
497       end.compact
498       if @required_user_agreements.empty?
499         # No agreements to sign. Perhaps we just need to ask?
500         current_user.activate
501         if !current_user.is_active
502           logger.warn "#{current_user.uuid.inspect}: " +
503             "No user agreements to sign, but activate failed!"
504         end
505       end
506       if !current_user.is_active
507         render 'user_agreements/index'
508       end
509     end
510     true
511   end
512
513   def select_theme
514     return Rails.configuration.arvados_theme
515   end
516
517   @@notification_tests = []
518
519   @@notification_tests.push lambda { |controller, current_user|
520     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
521       return nil
522     end
523     return lambda { |view|
524       view.render partial: 'notifications/ssh_key_notification'
525     }
526   }
527
528   #@@notification_tests.push lambda { |controller, current_user|
529   #  Job.limit(1).where(created_by: current_user.uuid).each do
530   #    return nil
531   #  end
532   #  return lambda { |view|
533   #    view.render partial: 'notifications/jobs_notification'
534   #  }
535   #}
536
537   @@notification_tests.push lambda { |controller, current_user|
538     Collection.limit(1).where(created_by: current_user.uuid).each do
539       return nil
540     end
541     return lambda { |view|
542       view.render partial: 'notifications/collections_notification'
543     }
544   }
545
546   @@notification_tests.push lambda { |controller, current_user|
547     PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
548       return nil
549     end
550     return lambda { |view|
551       view.render partial: 'notifications/pipelines_notification'
552     }
553   }
554
555   def check_user_notifications
556     return if params['tab_pane']
557
558     @notification_count = 0
559     @notifications = []
560
561     if current_user
562       @showallalerts = false
563       @@notification_tests.each do |t|
564         a = t.call(self, current_user)
565         if a
566           @notification_count += 1
567           @notifications.push a
568         end
569       end
570     end
571
572     if @notification_count == 0
573       @notification_count = ''
574     end
575   end
576
577   helper_method :all_projects
578   def all_projects
579     @all_projects ||= Group.
580       filter([['group_class','=','project']]).order('name')
581   end
582
583   helper_method :my_projects
584   def my_projects
585     return @my_projects if @my_projects
586     @my_projects = []
587     root_of = {}
588     all_projects.each do |g|
589       root_of[g.uuid] = g.owner_uuid
590       @my_projects << g
591     end
592     done = false
593     while not done
594       done = true
595       root_of = root_of.each_with_object({}) do |(child, parent), h|
596         if root_of[parent]
597           h[child] = root_of[parent]
598           done = false
599         else
600           h[child] = parent
601         end
602       end
603     end
604     @my_projects = @my_projects.select do |g|
605       root_of[g.uuid] == current_user.uuid
606     end
607   end
608
609   helper_method :projects_shared_with_me
610   def projects_shared_with_me
611     my_project_uuids = my_projects.collect &:uuid
612     all_projects.reject { |x| x.uuid.in? my_project_uuids }
613   end
614
615   helper_method :recent_jobs_and_pipelines
616   def recent_jobs_and_pipelines
617     (Job.limit(10) |
618      PipelineInstance.limit(10)).
619       sort_by do |x|
620       (x.finished_at || x.started_at rescue nil) || x.modified_at || x.created_at
621     end.reverse
622   end
623
624   helper_method :my_project_tree
625   def my_project_tree
626     build_project_trees
627     @my_project_tree
628   end
629
630   helper_method :shared_project_tree
631   def shared_project_tree
632     build_project_trees
633     @shared_project_tree
634   end
635
636   def build_project_trees
637     return if @my_project_tree and @shared_project_tree
638     parent_of = {current_user.uuid => 'me'}
639     all_projects.each do |ob|
640       parent_of[ob.uuid] = ob.owner_uuid
641     end
642     children_of = {false => [], 'me' => [current_user]}
643     all_projects.each do |ob|
644       if ob.owner_uuid != current_user.uuid and
645           not parent_of.has_key? ob.owner_uuid
646         parent_of[ob.uuid] = false
647       end
648       children_of[parent_of[ob.uuid]] ||= []
649       children_of[parent_of[ob.uuid]] << ob
650     end
651     buildtree = lambda do |children_of, root_uuid=false|
652       tree = {}
653       children_of[root_uuid].andand.each do |ob|
654         tree[ob] = buildtree.call(children_of, ob.uuid)
655       end
656       tree
657     end
658     sorted_paths = lambda do |tree, depth=0|
659       paths = []
660       tree.keys.sort_by { |ob|
661         ob.is_a?(String) ? ob : ob.friendly_link_name
662       }.each do |ob|
663         paths << {object: ob, depth: depth}
664         paths += sorted_paths.call tree[ob], depth+1
665       end
666       paths
667     end
668     @my_project_tree =
669       sorted_paths.call buildtree.call(children_of, 'me')
670     @shared_project_tree =
671       sorted_paths.call({'Shared with me' =>
672                           buildtree.call(children_of, false)})
673   end
674
675   helper_method :get_object
676   def get_object uuid
677     if @get_object.nil? and @objects
678       @get_object = @objects.each_with_object({}) do |object, h|
679         h[object.uuid] = object
680       end
681     end
682     @get_object ||= {}
683     @get_object[uuid]
684   end
685
686   helper_method :project_breadcrumbs
687   def project_breadcrumbs
688     crumbs = []
689     current = @name_link || @object
690     while current
691       if current.is_a?(Group) and current.group_class == 'project'
692         crumbs.prepend current
693       end
694       if current.is_a? Link
695         current = Group.find?(current.tail_uuid)
696       else
697         current = Group.find?(current.owner_uuid)
698       end
699     end
700     crumbs
701   end
702
703   helper_method :current_project_uuid
704   def current_project_uuid
705     if @object.is_a? Group and @object.group_class == 'project'
706       @object.uuid
707     elsif @name_link.andand.tail_uuid
708       @name_link.tail_uuid
709     elsif @object and resource_class_for_uuid(@object.owner_uuid) == Group
710       @object.owner_uuid
711     else
712       nil
713     end
714   end
715
716   # helper method to get links for given object or uuid
717   helper_method :links_for_object
718   def links_for_object object_or_uuid
719     raise ArgumentError, 'No input argument' unless object_or_uuid
720     preload_links_for_objects([object_or_uuid])
721     uuid = object_or_uuid.is_a?(String) ? object_or_uuid : object_or_uuid.uuid
722     @all_links_for[uuid] ||= []
723   end
724
725   # helper method to preload links for given objects and uuids
726   helper_method :preload_links_for_objects
727   def preload_links_for_objects objects_and_uuids
728     @all_links_for ||= {}
729
730     raise ArgumentError, 'Argument is not an array' unless objects_and_uuids.is_a? Array
731     return @all_links_for if objects_and_uuids.empty?
732
733     uuids = objects_and_uuids.collect { |x| x.is_a?(String) ? x : x.uuid }
734
735     # if already preloaded for all of these uuids, return
736     if not uuids.select { |x| @all_links_for[x].nil? }.any?
737       return @all_links_for
738     end
739
740     uuids.each do |x|
741       @all_links_for[x] = []
742     end
743
744     # TODO: make sure we get every page of results from API server
745     Link.filter([['head_uuid', 'in', uuids]]).each do |link|
746       @all_links_for[link.head_uuid] << link
747     end
748     @all_links_for
749   end
750
751   # helper method to get a certain number of objects of a specific type
752   # this can be used to replace any uses of: "dataclass.limit(n)"
753   helper_method :get_n_objects_of_class
754   def get_n_objects_of_class dataclass, size
755     @objects_map_for ||= {}
756
757     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class and dataclass < ArvadosBase
758     raise ArgumentError, 'Argument is not a valid limit size' unless (size && size>0)
759
760     # if the objects_map_for has a value for this dataclass, and the
761     # size used to retrieve those objects is equal, return it
762     size_key = "#{dataclass.name}_size"
763     if @objects_map_for[dataclass.name] && @objects_map_for[size_key] &&
764         (@objects_map_for[size_key] == size)
765       return @objects_map_for[dataclass.name]
766     end
767
768     @objects_map_for[size_key] = size
769     @objects_map_for[dataclass.name] = dataclass.limit(size)
770   end
771
772   # helper method to get collections for the given uuid
773   helper_method :collections_for_object
774   def collections_for_object uuid
775     raise ArgumentError, 'No input argument' unless uuid
776     preload_collections_for_objects([uuid])
777     @all_collections_for[uuid] ||= []
778   end
779
780   # helper method to preload collections for the given uuids
781   helper_method :preload_collections_for_objects
782   def preload_collections_for_objects uuids
783     @all_collections_for ||= {}
784
785     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
786     return @all_collections_for if uuids.empty?
787
788     # if already preloaded for all of these uuids, return
789     if not uuids.select { |x| @all_collections_for[x].nil? }.any?
790       return @all_collections_for
791     end
792
793     uuids.each do |x|
794       @all_collections_for[x] = []
795     end
796
797     # TODO: make sure we get every page of results from API server
798     Collection.where(uuid: uuids).each do |collection|
799       @all_collections_for[collection.uuid] << collection
800     end
801     @all_collections_for
802   end
803
804   # helper method to get log collections for the given log
805   helper_method :log_collections_for_object
806   def log_collections_for_object log
807     raise ArgumentError, 'No input argument' unless log
808
809     preload_log_collections_for_objects([log])
810
811     uuid = log
812     fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
813     if fixup && fixup.size>1
814       uuid = fixup[1]
815     end
816
817     @all_log_collections_for[uuid] ||= []
818   end
819
820   # helper method to preload collections for the given uuids
821   helper_method :preload_log_collections_for_objects
822   def preload_log_collections_for_objects logs
823     @all_log_collections_for ||= {}
824
825     raise ArgumentError, 'Argument is not an array' unless logs.is_a? Array
826     return @all_log_collections_for if logs.empty?
827
828     uuids = []
829     logs.each do |log|
830       fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log)
831       if fixup && fixup.size>1
832         uuids << fixup[1]
833       else
834         uuids << log
835       end
836     end
837
838     # if already preloaded for all of these uuids, return
839     if not uuids.select { |x| @all_log_collections_for[x].nil? }.any?
840       return @all_log_collections_for
841     end
842
843     uuids.each do |x|
844       @all_log_collections_for[x] = []
845     end
846
847     # TODO: make sure we get every page of results from API server
848     Collection.where(uuid: uuids).each do |collection|
849       @all_log_collections_for[collection.uuid] << collection
850     end
851     @all_log_collections_for
852   end
853
854   # helper method to get object of a given dataclass and uuid
855   helper_method :object_for_dataclass
856   def object_for_dataclass dataclass, uuid
857     raise ArgumentError, 'No input argument dataclass' unless (dataclass && uuid)
858     preload_objects_for_dataclass(dataclass, [uuid])
859     @objects_for[uuid]
860   end
861
862   # helper method to preload objects for given dataclass and uuids
863   helper_method :preload_objects_for_dataclass
864   def preload_objects_for_dataclass dataclass, uuids
865     @objects_for ||= {}
866
867     raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class
868     raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array
869
870     return @objects_for if uuids.empty?
871
872     # if already preloaded for all of these uuids, return
873     if not uuids.select { |x| @objects_for[x].nil? }.any?
874       return @objects_for
875     end
876
877     dataclass.where(uuid: uuids).each do |obj|
878       @objects_for[obj.uuid] = obj
879     end
880     @objects_for
881   end
882
883   def wiselinks_layout
884     'body'
885   end
886 end