X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/010a56c1ac501c8ae18b71198d53c17e60f9d9e3..676dfa352dbf55a23e0123b47d1affe33c028908:/apps/workbench/app/controllers/application_controller.rb?ds=sidebyside diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb index 59ca3503c8..38ae911b5c 100644 --- a/apps/workbench/app/controllers/application_controller.rb +++ b/apps/workbench/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base include ArvadosApiClientHelper + include ApplicationHelper respond_to :html, :json, :js protect_from_forgery @@ -7,24 +8,24 @@ class ApplicationController < ActionController::Base ERROR_ACTIONS = [:render_error, :render_not_found] around_filter :thread_clear - around_filter :thread_with_mandatory_api_token, except: ERROR_ACTIONS - around_filter :thread_with_optional_api_token + around_filter :set_thread_api_token + # Methods that don't require login should + # skip_around_filter :require_thread_api_token + around_filter :require_thread_api_token, except: ERROR_ACTIONS before_filter :check_user_agreements, except: ERROR_ACTIONS before_filter :check_user_notifications, except: ERROR_ACTIONS - before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS + before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS theme :select_theme begin - rescue_from Exception, - :with => :render_exception - rescue_from ActiveRecord::RecordNotFound, - :with => :render_not_found - rescue_from ActionController::RoutingError, - :with => :render_not_found - rescue_from ActionController::UnknownController, - :with => :render_not_found - rescue_from ::AbstractController::ActionNotFound, - :with => :render_not_found + rescue_from(ActiveRecord::RecordNotFound, + ActionController::RoutingError, + ActionController::UnknownController, + AbstractController::ActionNotFound, + with: :render_not_found) + rescue_from(Exception, + ActionController::UrlGenerationError, + with: :render_exception) end def unprocessable(message=nil) @@ -54,16 +55,24 @@ class ApplicationController < ActionController::Base else @errors = [e.to_s] end - self.render_error status: 422 + if e.is_a? ArvadosApiClient::NotLoggedInException + self.render_error status: 422 + else + set_thread_api_token do + self.render_error status: 422 + end + end end def render_not_found(e=ActionController::RoutingError.new("Path not found")) logger.error e.inspect @errors = ["Path not found"] - self.render_error status: 404 + set_thread_api_token do + self.render_error status: 404 + end end - def index + def find_objects_for_index @limit ||= 200 if params[:limit] @limit = params[:limit].to_i @@ -84,14 +93,44 @@ class ApplicationController < ActionController::Base end @objects ||= model_class - @objects = @objects.filter(@filters).limit(@limit).offset(@offset).all + @objects = @objects.filter(@filters).limit(@limit).offset(@offset) + end + + def render_index respond_to do |f| f.json { render json: @objects } - f.html { render } + f.html { + if params['tab_pane'] + comparable = self.respond_to? :compare + render(partial: 'show_' + params['tab_pane'].downcase, + locals: { comparable: comparable, objects: @objects }) + else + render + end + } f.js { render } end end + def index + find_objects_for_index if !@objects + render_index + end + + helper_method :next_page_offset + def next_page_offset + if @objects.respond_to?(:result_offset) and + @objects.respond_to?(:result_limit) and + @objects.respond_to?(:items_available) + next_offset = @objects.result_offset + @objects.result_limit + if next_offset < @objects.items_available + next_offset + else + nil + end + end + end + def show if !@object return render_not_found("object not found") @@ -99,7 +138,11 @@ class ApplicationController < ActionController::Base respond_to do |f| f.json { render json: @object.attributes.merge(href: url_for(@object)) } f.html { - if request.method == 'GET' + if params['tab_pane'] + comparable = self.respond_to? :compare + render(partial: 'show_' + params['tab_pane'].downcase, + locals: { comparable: comparable, objects: @objects }) + elsif request.method.in? ['GET', 'HEAD'] render else redirect_to params[:return_to] || @object @@ -109,6 +152,37 @@ class ApplicationController < ActionController::Base end end + def choose + params[:limit] ||= 40 + if !@objects + if params[:project_uuid] and !params[:project_uuid].empty? + # We want the chooser to show objects of the controllers's model_class + # type within a specific project specified by project_uuid, so fetch the + # project and request the contents of the project filtered on the + # controllers's model_class kind. + @objects = Group.find(params[:project_uuid]).contents({:filters => [['uuid', 'is_a', "arvados\##{ArvadosApiClient.class_kind(model_class)}"]]}) + end + find_objects_for_index if !@objects + end + respond_to do |f| + if params[:partial] + f.json { + render json: { + content: render_to_string(partial: "choose_rows.html", + formats: [:html], + locals: { + multiple: params[:multiple] + }), + next_page_href: @next_page_href + } + } + end + f.js { + render partial: 'choose', locals: {multiple: params[:multiple]} + } + end + end + def render_content if !@object return render_not_found("object not found") @@ -120,7 +194,7 @@ class ApplicationController < ActionController::Base end def update - @updates ||= params[@object.class.to_s.underscore.singularize.to_sym] + @updates ||= params[@object.resource_param_name.to_sym] @updates.keys.each do |attr| if @object.send(attr).is_a? Hash if @updates[attr].is_a? String @@ -145,7 +219,34 @@ class ApplicationController < ActionController::Base @new_resource_attrs ||= params[model_class.to_s.underscore.singularize] @new_resource_attrs ||= {} @new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' } - @object ||= model_class.new @new_resource_attrs + @object ||= model_class.new @new_resource_attrs, params["options"] + if @object.save + respond_to do |f| + f.json { render json: @object.attributes.merge(href: url_for(@object)) } + f.html { + redirect_to @object + } + f.js { render } + end + else + self.render_error status: 422 + end + end + + # Clone the given object, merging any attribute values supplied as + # with a create action. + def copy + @new_resource_attrs ||= params[model_class.to_s.underscore.singularize] + @new_resource_attrs ||= {} + @object = @object.dup + @object.update_attributes @new_resource_attrs + if not @new_resource_attrs[:name] and @object.respond_to? :name + if @object.name and @object.name != '' + @object.name = "Copy of #{@object.name}" + else + @object.name = "Copy of unnamed #{@object.class_for_display.downcase}" + end + end @object.save! show end @@ -165,8 +266,18 @@ class ApplicationController < ActionController::Base end def current_user + return Thread.current[:user] if Thread.current[:user] + if Thread.current[:arvados_api_token] - Thread.current[:user] ||= User.current + if session[:user] + if session[:user][:is_active] != true + Thread.current[:user] = User.current + else + Thread.current[:user] = User.new(session[:user]) + end + else + Thread.current[:user] = User.current + end else logger.error "No API token in Thread" return nil @@ -188,16 +299,20 @@ class ApplicationController < ActionController::Base end def show_pane_list - %w(Attributes Metadata JSON API) + %w(Attributes Advanced) end protected + 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 == 'GET' - redirect_to arvados_api_client.arvados_login_url(return_to: request.url) + 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 @@ -235,7 +350,13 @@ class ApplicationController < ActionController::Base if params[:uuid].empty? @object = nil else - @object = model_class.find(params[:uuid]) + if (model_class != Link and + resource_class_for_uuid(params[:uuid]) == Link) + @name_link = Link.find(params[:uuid]) + @object = model_class.find(@name_link.head_uuid) + else + @object = model_class.find(params[:uuid]) + end end else @object = model_class.where(uuid: params[:uuid]).first @@ -250,48 +371,51 @@ class ApplicationController < ActionController::Base Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/) end - def thread_with_api_token(login_optional = false) + # 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. + # If a token is unavailable or expired, the block is still run, with + # a nil token. + def set_thread_api_token + # If an API token has already been found, pass it through. + if Thread.current[:arvados_api_token] + yield + return + end + begin - try_redirect_to_login = true - if params[:api_token] - try_redirect_to_login = false - Thread.current[:arvados_api_token] = params[:api_token] - # Before copying the token into session[], do a simple API - # call to verify its authenticity. - if verify_api_token - session[:arvados_api_token] = params[:api_token] - if !request.format.json? and request.method == 'GET' - # Repeat this request with api_token in the (new) session - # cookie instead of the query string. This prevents API - # tokens from appearing in (and being inadvisedly copied - # and pasted from) browser Location bars. - redirect_to request.fullpath.sub(%r{([&\?]api_token=)[^&\?]*}, '') - else - yield - end - else - @errors = ['Invalid API token'] - self.render_error status: 401 - end - elsif session[:arvados_api_token] - # In this case, the token must have already verified at some - # point, but it might have been revoked since. We'll try - # using it, and catch the exception if it doesn't work. - try_redirect_to_login = false - Thread.current[:arvados_api_token] = session[:arvados_api_token] - begin - yield - rescue ArvadosApiClient::NotLoggedInException - try_redirect_to_login = true + # If there's a valid api_token parameter, use it to set up the session. + if (Thread.current[:arvados_api_token] = params[:api_token]) and + verify_api_token + session[:arvados_api_token] = params[:api_token] + u = User.current + session[:user] = { + uuid: u.uuid, + email: u.email, + first_name: u.first_name, + last_name: u.last_name, + is_active: u.is_active, + is_admin: u.is_admin, + prefs: u.prefs + } + if !request.format.json? and request.method.in? ['GET', 'HEAD'] + # Repeat this request with api_token in the (new) session + # cookie instead of the query string. This prevents API + # tokens from appearing in (and being inadvisedly copied + # and pasted from) browser Location bars. + redirect_to strip_token_from_path(request.fullpath) + return end - else - logger.debug "No token received, session is #{session.inspect}" end - if try_redirect_to_login - unless login_optional - redirect_to_login - else - # login is optional for this route so go on to the regular controller + + # With setup done, handle the request using the session token. + Thread.current[:arvados_api_token] = session[:arvados_api_token] + begin + yield + rescue ArvadosApiClient::NotLoggedInException + # If we got this error with a token, it must've expired. + # Retry the request without a token. + unless Thread.current[:arvados_api_token].nil? Thread.current[:arvados_api_token] = nil yield end @@ -302,22 +426,18 @@ class ApplicationController < ActionController::Base end end - def thread_with_mandatory_api_token - thread_with_api_token do - yield - end - end - - # This runs after thread_with_mandatory_api_token in the filter chain. - def thread_with_optional_api_token + # Reroute this request if an API token is unavailable. + def require_thread_api_token if Thread.current[:arvados_api_token] - # We are already inside thread_with_mandatory_api_token. yield + elsif session[:arvados_api_token] + # Expired session. Clear it before refreshing login so that, + # if this login procedure fails, we end up showing the "please + # log in" page instead of getting stuck in a redirect loop. + session.delete :arvados_api_token + redirect_to_login else - # We skipped thread_with_mandatory_api_token. Use the optional version. - thread_with_api_token(true) do - yield - end + render 'users/welcome' end end @@ -338,7 +458,10 @@ class ApplicationController < ActionController::Base end def check_user_agreements - if current_user && !current_user.is_active && current_user.is_invited + if current_user && !current_user.is_active + if not current_user.is_invited + return render 'users/inactive' + end signatures = UserAgreement.signatures @signed_ua_uuids = UserAgreement.signatures.map &:head_uuid @required_user_agreements = UserAgreement.all.map do |ua| @@ -404,6 +527,8 @@ class ApplicationController < ActionController::Base } def check_user_notifications + return if params['tab_pane'] + @notification_count = 0 @notifications = [] @@ -422,4 +547,311 @@ class ApplicationController < ActionController::Base @notification_count = '' end end + + helper_method :all_projects + def all_projects + @all_projects ||= Group. + filter([['group_class','in',['project','folder']]]).order('name') + end + + helper_method :my_projects + def my_projects + return @my_projects if @my_projects + @my_projects = [] + root_of = {} + all_projects.each do |g| + root_of[g.uuid] = g.owner_uuid + @my_projects << g + end + done = false + while not done + done = true + root_of = root_of.each_with_object({}) do |(child, parent), h| + if root_of[parent] + h[child] = root_of[parent] + done = false + else + h[child] = parent + end + end + end + @my_projects = @my_projects.select do |g| + root_of[g.uuid] == current_user.uuid + end + end + + helper_method :projects_shared_with_me + def projects_shared_with_me + my_project_uuids = my_projects.collect &:uuid + all_projects.reject { |x| x.uuid.in? my_project_uuids } + end + + helper_method :recent_jobs_and_pipelines + def recent_jobs_and_pipelines + (Job.limit(10) | + PipelineInstance.limit(10)). + sort_by do |x| + x.finished_at || x.started_at || x.created_at rescue x.created_at + end + end + + helper_method :my_project_tree + def my_project_tree + build_project_trees + @my_project_tree + end + + helper_method :shared_project_tree + def shared_project_tree + build_project_trees + @shared_project_tree + end + + def build_project_trees + return if @my_project_tree and @shared_project_tree + parent_of = {current_user.uuid => 'me'} + all_projects.each do |ob| + parent_of[ob.uuid] = ob.owner_uuid + end + children_of = {false => [], 'me' => [current_user]} + all_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_project_tree = + sorted_paths.call buildtree.call(children_of, 'me') + @shared_project_tree = + sorted_paths.call({'Shared with me' => + buildtree.call(children_of, false)}) + end + + helper_method :get_object + def get_object uuid + if @get_object.nil? and @objects + @get_object = @objects.each_with_object({}) do |object, h| + h[object.uuid] = object + end + end + @get_object ||= {} + @get_object[uuid] + end + + helper_method :project_breadcrumbs + def project_breadcrumbs + crumbs = [] + current = @name_link || @object + while current + if current.is_a?(Group) and current.group_class.in?(['project','folder']) + crumbs.prepend current + end + if current.is_a? Link + current = Group.find?(current.tail_uuid) + else + current = Group.find?(current.owner_uuid) + end + end + crumbs + end + + helper_method :current_project_uuid + def current_project_uuid + if @object.is_a? Group and @object.group_class.in?(['project','folder']) + @object.uuid + elsif @name_link.andand.tail_uuid + @name_link.tail_uuid + elsif @object and resource_class_for_uuid(@object.owner_uuid) == Group + @object.owner_uuid + else + nil + end + end + + # helper method to get links for given object or uuid + helper_method :links_for_object + def links_for_object object_or_uuid + raise ArgumentError, 'No input argument' unless object_or_uuid + preload_links_for_objects([object_or_uuid]) + uuid = object_or_uuid.is_a?(String) ? object_or_uuid : object_or_uuid.uuid + @all_links_for[uuid] ||= [] + end + + # helper method to preload links for given objects and uuids + helper_method :preload_links_for_objects + def preload_links_for_objects objects_and_uuids + @all_links_for ||= {} + + raise ArgumentError, 'Argument is not an array' unless objects_and_uuids.is_a? Array + return @all_links_for if objects_and_uuids.empty? + + uuids = objects_and_uuids.collect { |x| x.is_a?(String) ? x : x.uuid } + + # if already preloaded for all of these uuids, return + if not uuids.select { |x| @all_links_for[x].nil? }.any? + return @all_links_for + end + + uuids.each do |x| + @all_links_for[x] = [] + end + + # TODO: make sure we get every page of results from API server + Link.filter([['head_uuid', 'in', uuids]]).each do |link| + @all_links_for[link.head_uuid] << link + end + @all_links_for + end + + # helper method to get a certain number of objects of a specific type + # this can be used to replace any uses of: "dataclass.limit(n)" + helper_method :get_n_objects_of_class + def get_n_objects_of_class dataclass, size + @objects_map_for ||= {} + + raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? ArvadosBase + raise ArgumentError, 'Argument is not a valid limit size' unless (size && size>0) + + # if the objects_map_for has a value for this dataclass, and the + # size used to retrieve those objects is equal, return it + size_key = "#{dataclass.name}_size" + if @objects_map_for[dataclass.name] && @objects_map_for[size_key] && + (@objects_map_for[size_key] == size) + return @objects_map_for[dataclass.name] + end + + @objects_map_for[size_key] = size + @objects_map_for[dataclass.name] = dataclass.limit(size) + end + + # helper method to get collections for the given uuid + helper_method :collections_for_object + def collections_for_object uuid + raise ArgumentError, 'No input argument' unless uuid + preload_collections_for_objects([uuid]) + @all_collections_for[uuid] ||= [] + end + + # helper method to preload collections for the given uuids + helper_method :preload_collections_for_objects + def preload_collections_for_objects uuids + @all_collections_for ||= {} + + raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array + return @all_collections_for if uuids.empty? + + # if already preloaded for all of these uuids, return + if not uuids.select { |x| @all_collections_for[x].nil? }.any? + return @all_collections_for + end + + uuids.each do |x| + @all_collections_for[x] = [] + end + + # TODO: make sure we get every page of results from API server + Collection.where(uuid: uuids).each do |collection| + @all_collections_for[collection.uuid] << collection + end + @all_collections_for + end + + # helper method to get log collections for the given log + helper_method :log_collections_for_object + def log_collections_for_object log + raise ArgumentError, 'No input argument' unless log + + preload_log_collections_for_objects([log]) + + uuid = log + fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log) + if fixup && fixup.size>1 + uuid = fixup[1] + end + + @all_log_collections_for[uuid] ||= [] + end + + # helper method to preload collections for the given uuids + helper_method :preload_log_collections_for_objects + def preload_log_collections_for_objects logs + @all_log_collections_for ||= {} + + raise ArgumentError, 'Argument is not an array' unless logs.is_a? Array + return @all_log_collections_for if logs.empty? + + uuids = [] + logs.each do |log| + fixup = /([a-f0-9]{32}\+\d+)(\+?.*)/.match(log) + if fixup && fixup.size>1 + uuids << fixup[1] + else + uuids << log + end + end + + # if already preloaded for all of these uuids, return + if not uuids.select { |x| @all_log_collections_for[x].nil? }.any? + return @all_log_collections_for + end + + uuids.each do |x| + @all_log_collections_for[x] = [] + end + + # TODO: make sure we get every page of results from API server + Collection.where(uuid: uuids).each do |collection| + @all_log_collections_for[collection.uuid] << collection + end + @all_log_collections_for + end + + # helper method to get object of a given dataclass and uuid + helper_method :object_for_dataclass + def object_for_dataclass dataclass, uuid + raise ArgumentError, 'No input argument dataclass' unless (dataclass && uuid) + preload_objects_for_dataclass(dataclass, [uuid]) + @objects_for[uuid] + end + + # helper method to preload objects for given dataclass and uuids + helper_method :preload_objects_for_dataclass + def preload_objects_for_dataclass dataclass, uuids + @objects_for ||= {} + + raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class + raise ArgumentError, 'Argument is not an array' unless uuids.is_a? Array + + 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? + return @objects_for + end + + dataclass.where(uuid: uuids).each do |obj| + @objects_for[obj.uuid] = obj + end + @objects_for + end + end