8183: When there are more than 200 readable projects, build the tree in steps;
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
index 5d097c1a0886fad3dead9e811fa2f6420a472c4a..ab018d7151d01dbe6c6c195689d83fcf1caa4518 100644 (file)
@@ -8,7 +8,6 @@ class ApplicationController < ActionController::Base
   ERROR_ACTIONS = [:render_error, :render_not_found]
 
   around_filter :thread_clear
-  before_filter :permit_anonymous_browsing_for_public_data
   around_filter :set_thread_api_token
   # Methods that don't require login should
   #   skip_around_filter :require_thread_api_token
@@ -90,13 +89,12 @@ class ApplicationController < ActionController::Base
     # exception here than in a template.)
     unless current_user.nil?
       begin
-        build_project_trees
+        build_my_wanted_projects_tree
       rescue ArvadosApiClient::ApiError
         # Fall back to the default-setting code later.
       end
     end
-    @my_project_tree ||= []
-    @shared_project_tree ||= []
+    @my_wanted_projects_tree ||= []
     render_error(err_opts)
   end
 
@@ -268,6 +266,17 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  def redirect_to uri, *args
+    if request.xhr?
+      if not uri.is_a? String
+        uri = polymorphic_url(uri)
+      end
+      render json: {href: uri}
+    else
+      super
+    end
+  end
+
   def choose
     params[:limit] ||= 40
     respond_to do |f|
@@ -391,7 +400,7 @@ class ApplicationController < ActionController::Base
     @user_is_manager = false
     @share_links = []
 
-    if @object.uuid != current_user.uuid
+    if @object.uuid != current_user.andand.uuid
       begin
         @share_links = Link.permissions_for(@object)
         @user_is_manager = true
@@ -436,24 +445,20 @@ class ApplicationController < ActionController::Base
 
   protected
 
+  helper_method :strip_token_from_path
   def strip_token_from_path(path)
     path.sub(/([\?&;])api_token=[^&;]*[&;]?/, '\1')
   end
 
   def redirect_to_login
-    respond_to do |f|
-      f.html {
-        if request.method.in? ['GET', 'HEAD']
-          redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url))
-        else
-          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."
-          redirect_to :back
-        end
-      }
-      f.json {
-        @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.']
-        self.render_error status: 422
-      }
+    if request.xhr? or request.format.json?
+      @errors = ['You are not logged in. Most likely your session has timed out and you need to log in again.']
+      render_error status: 401
+    elsif request.method.in? ['GET', 'HEAD']
+      redirect_to arvados_api_client.arvados_login_url(return_to: strip_token_from_path(request.url))
+    else
+      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."
+      redirect_to :back
     end
     false  # For convenience to return from callbacks
   end
@@ -498,7 +503,7 @@ class ApplicationController < ActionController::Base
       else
         @object = model_class.find(params[:uuid])
       end
-    rescue ArvadosApiClient::NotFoundException, RuntimeError => error
+    rescue ArvadosApiClient::NotFoundException, ArvadosApiClient::NotLoggedInException, RuntimeError => error
       if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/)
         raise
       end
@@ -565,17 +570,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  # Anonymous allowed paths:
-  #   /projects/#{uuid}?public_data=true
-  def permit_anonymous_browsing_for_public_data
-    if !Thread.current[:arvados_api_token] && !params[:api_token] && !session[:arvados_api_token]
-      public_project_accessed = /\/projects\/([0-9a-z]{5}-j7d0g-[0-9a-z]{15})(.*)public_data\=true/.match(request.fullpath)
-      if public_project_accessed
-        params[:api_token] = Rails.configuration.anonymous_user_token
-      end
-    end
-  end
-
   # Save the session API token in thread-local storage, and yield.
   # This method also takes care of session setup if the request
   # provides a valid api_token parameter.
@@ -605,7 +599,8 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  # Redirect to login/welcome if client provided expired API token (or none at all)
+  # Redirect to login/welcome if client provided expired API token (or
+  # none at all)
   def require_thread_api_token
     if Thread.current[:arvados_api_token]
       yield
@@ -615,15 +610,26 @@ class ApplicationController < ActionController::Base
       # log in" page instead of getting stuck in a redirect loop.
       session.delete :arvados_api_token
       redirect_to_login
+    elsif request.xhr?
+      # If we redirect to the welcome page, the browser will handle
+      # the 302 by itself and the client code will end up rendering
+      # the "welcome" page in some content area where it doesn't make
+      # sense. Instead, we send 401 ("authenticate and try again" or
+      # "display error", depending on how smart the client side is).
+      @errors = ['You are not logged in.']
+      render_error status: 401
     else
       redirect_to welcome_users_path(return_to: request.fullpath)
     end
   end
 
   def ensure_current_user_is_admin
-    unless current_user and current_user.is_admin
+    if not current_user
+      @errors = ['Not logged in']
+      render_error status: 401
+    elsif not current_user.is_admin
       @errors = ['Permission denied']
-      self.render_error status: 401
+      render_error status: 403
     end
   end
 
@@ -639,8 +645,6 @@ class ApplicationController < ActionController::Base
 
   def check_user_agreements
     if current_user && !current_user.is_active
-      return true if is_anonymous
-
       if not current_user.is_invited
         return redirect_to inactive_users_path(return_to: request.fullpath)
       end
@@ -660,9 +664,10 @@ class ApplicationController < ActionController::Base
   end
 
   def check_user_profile
+    return true if !current_user
     if request.method.downcase != 'get' || params[:partial] ||
        params[:tab_pane] || params[:action_method] ||
-       params[:action] == 'setup_popup' || is_anonymous
+       params[:action] == 'setup_popup'
       return true
     end
 
@@ -701,6 +706,7 @@ class ApplicationController < ActionController::Base
   @@notification_tests = []
 
   @@notification_tests.push lambda { |controller, current_user|
+    return nil if Rails.configuration.shell_in_a_box_url
     AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
       return nil
     end
@@ -826,6 +832,77 @@ class ApplicationController < ActionController::Base
     {collections: c, owners: own}
   end
 
+  # If there are more than 200 projects that are readable by the user,
+  # build the tree using only the top 200+ projects. That is:
+  # get toplevel projects under home, get subprojects under these
+  # projects, and so on until we hit the limit
+  def my_wanted_projects
+    return @my_wanted_projects if @my_wanted_projects
+
+    all = Group.filter([['group_class','=','project']]).order('name').limit(100)
+    if all.items_available > 200
+      @total_projects = all.items_available
+      from_top = []
+      uuids = [current_user.uuid]
+      while from_top.size <= 200
+        current_level = Group.filter([['group_class','=','project'], ['owner_uuid', 'in', uuids]]).order('name').limit(200)
+        break if current_level.results.size == 0
+        from_top.concat current_level.results
+        uuids = current_level.results.collect { |x| x.uuid }
+      end
+      @my_wanted_projects = from_top
+    else
+      if all.results.size == all.items_available
+        @my_wanted_projects = all
+      else
+        @my_wanted_projects = Group.filter([['group_class','=','project']]).order('name')
+      end
+    end
+  end
+
+  helper_method :my_wanted_projects_tree
+  def my_wanted_projects_tree
+    build_my_wanted_projects_tree
+    [@my_wanted_projects_tree, @total_projects]
+  end
+
+  def build_my_wanted_projects_tree
+    return @my_wanted_projects_tree if @my_wanted_projects_tree
+
+    parent_of = {current_user.uuid => 'me'}
+    my_wanted_projects.each do |ob|
+      parent_of[ob.uuid] = ob.owner_uuid
+    end
+    children_of = {false => [], 'me' => [current_user]}
+    my_wanted_projects.each do |ob|
+      if ob.owner_uuid != current_user.uuid and
+          not parent_of.has_key? ob.owner_uuid
+        parent_of[ob.uuid] = false
+      end
+      children_of[parent_of[ob.uuid]] ||= []
+      children_of[parent_of[ob.uuid]] << ob
+    end
+    buildtree = lambda do |children_of, root_uuid=false|
+      tree = {}
+      children_of[root_uuid].andand.each do |ob|
+        tree[ob] = buildtree.call(children_of, ob.uuid)
+      end
+      tree
+    end
+    sorted_paths = lambda do |tree, depth=0|
+      paths = []
+      tree.keys.sort_by { |ob|
+        ob.is_a?(String) ? ob : ob.friendly_link_name
+      }.each do |ob|
+        paths << {object: ob, depth: depth}
+        paths += sorted_paths.call tree[ob], depth+1
+      end
+      paths
+    end
+    @my_wanted_projects_tree =
+      sorted_paths.call buildtree.call(children_of, 'me')
+  end
+
   helper_method :my_project_tree
   def my_project_tree
     build_project_trees
@@ -1062,6 +1139,39 @@ class ApplicationController < ActionController::Base
     @all_log_collections_for
   end
 
+  # Helper method to get one collection for the given portable_data_hash
+  # This is used to determine if a pdh is readable by the current_user
+  helper_method :collection_for_pdh
+  def collection_for_pdh pdh
+    raise ArgumentError, 'No input argument' unless pdh
+    preload_for_pdhs([pdh])
+    @all_pdhs_for[pdh] ||= []
+  end
+
+  # Helper method to preload one collection each for the given pdhs
+  # This is used to determine if a pdh is readable by the current_user
+  helper_method :preload_for_pdhs
+  def preload_for_pdhs pdhs
+    @all_pdhs_for ||= {}
+
+    raise ArgumentError, 'Argument is not an array' unless pdhs.is_a? Array
+    return @all_pdhs_for if pdhs.empty?
+
+    # if already preloaded for all of these pdhs, return
+    if not pdhs.select { |x| @all_pdhs_for[x].nil? }.any?
+      return @all_pdhs_for
+    end
+
+    pdhs.each do |x|
+      @all_pdhs_for[x] = []
+    end
+
+    Collection.select(%w(portable_data_hash)).where(portable_data_hash: pdhs).distinct().each do |collection|
+      @all_pdhs_for[collection.portable_data_hash] << collection
+    end
+    @all_pdhs_for
+  end
+
   # helper method to get object of a given dataclass and uuid
   helper_method :object_for_dataclass
   def object_for_dataclass dataclass, uuid
@@ -1081,10 +1191,14 @@ class ApplicationController < ActionController::Base
     return @objects_for if uuids.empty?
 
     # if already preloaded for all of these uuids, return
-    if not uuids.select { |x| @objects_for[x].nil? }.any?
+    if not uuids.select { |x| !@objects_for.include?(x) }.any?
       return @objects_for
     end
 
+    # preset all uuids to nil
+    uuids.each do |x|
+      @objects_for[x] = nil
+    end
     dataclass.where(uuid: uuids).each do |obj|
       @objects_for[obj.uuid] = obj
     end
@@ -1094,10 +1208,4 @@ class ApplicationController < ActionController::Base
   def wiselinks_layout
     'body'
   end
-
-  helper_method :is_anonymous
-  def is_anonymous
-    return Rails.configuration.anonymous_user_token &&
-          (Thread.current[:arvados_api_token] == Rails.configuration.anonymous_user_token)
-  end
 end