ERROR_ACTIONS = [:render_error, :render_not_found]
+ prepend_before_filter :set_current_request_id, except: ERROR_ACTIONS
around_filter :thread_clear
around_filter :set_thread_api_token
# Methods that don't require login should
before_filter :accept_uuid_as_id_param, except: ERROR_ACTIONS
before_filter :check_user_agreements, except: ERROR_ACTIONS
before_filter :check_user_profile, except: ERROR_ACTIONS
- before_filter :check_user_notifications, except: ERROR_ACTIONS
before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
before_filter :find_object_by_uuid, except: [:create, :index, :choose] + ERROR_ACTIONS
theme :select_theme
end
def render_error(opts={})
+ # Helpers can rely on the presence of @errors to know they're
+ # being used in an error page.
+ @errors ||= []
opts[:status] ||= 500
respond_to do |f|
# json must come before html here, so it gets used as the
# exception here than in a template.)
unless current_user.nil?
begin
- build_project_trees
+ my_starred_projects current_user
+ build_my_wanted_projects_tree current_user
rescue ArvadosApiClient::ApiError
# Fall back to the default-setting code later.
end
end
- @my_project_tree ||= []
- @shared_project_tree ||= []
+ @starred_projects ||= []
+ @my_wanted_projects_tree ||= []
render_error(err_opts)
end
# Column names should always be qualified by a table name and a direction is optional, defaulting to asc
# (e.g. "collections.name" or "collections.name desc").
# If a column name is specified, that table will be sorted by that column.
- # If there are objects from different models that will be shown (such as in Jobs and Pipelines tab),
+ # If there are objects from different models that will be shown (such as in Pipelines and processes tab),
# 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")
# Currently only one sort column name and direction can be specified for each model.
def load_filters_and_paging_params
end
end
+ helper_method :next_page_filters
+ def next_page_filters nextpage_operator
+ next_page_filters = @filters.reject do |attr, op, val|
+ (attr == 'created_at' and op == nextpage_operator) or
+ (attr == 'uuid' and op == 'not in')
+ end
+
+ if @objects.any?
+ last_created_at = @objects.last.created_at
+
+ last_uuids = []
+ @objects.each do |obj|
+ last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
+ end
+
+ next_page_filters += [['created_at', nextpage_operator, last_created_at]]
+ next_page_filters += [['uuid', 'not in', last_uuids]]
+ end
+
+ next_page_filters
+ end
+
def show
if !@object
return render_not_found("object not found")
f.html {
if params['tab_pane']
render_pane(if params['tab_pane'].is_a? Hash then params['tab_pane']["name"] else params['tab_pane'] end)
- elsif request.method.in? ['GET', 'HEAD']
+ elsif request.request_method.in? ['GET', 'HEAD']
render
else
- redirect_to params[:return_to] || @object
+ redirect_to (params[:return_to] ||
+ polymorphic_url(@object,
+ anchor: params[:redirect_to_anchor]))
end
}
f.js { render }
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|
@new_resource_attrs ||= {}
@new_resource_attrs.reject! { |k,v| k.to_s == 'uuid' }
@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(action: :show, id: @object)) }
- f.html {
- redirect_to @object
- }
- f.js { render }
- end
+ show
else
- self.render_error status: 422
+ render_error status: 422
end
end
%w(Attributes Advanced)
end
+ def set_share_links
+ @user_is_manager = false
+ @share_links = []
+
+ if @object.uuid != current_user.andand.uuid
+ begin
+ @share_links = Link.permissions_for(@object)
+ @user_is_manager = true
+ rescue ArvadosApiClient::AccessForbiddenException,
+ ArvadosApiClient::NotFoundException
+ end
+ end
+ end
+
+ def share_with
+ if not params[:uuids].andand.any?
+ @errors = ["No user/group UUIDs specified to share with."]
+ return render_error(status: 422)
+ end
+ results = {"success" => [], "errors" => []}
+ params[:uuids].each do |shared_uuid|
+ begin
+ Link.create(tail_uuid: shared_uuid, link_class: "permission",
+ name: "can_read", head_uuid: @object.uuid)
+ rescue ArvadosApiClient::ApiError => error
+ error_list = error.api_response.andand[:errors]
+ if error_list.andand.any?
+ results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
+ else
+ error_code = error.api_status || "Bad status"
+ results["errors"] << "#{shared_uuid}: #{error_code} response"
+ end
+ else
+ results["success"] << shared_uuid
+ end
+ end
+ if results["errors"].empty?
+ results.delete("errors")
+ status = 200
+ else
+ status = 422
+ end
+ respond_to do |f|
+ f.json { render(json: results, status: status) }
+ end
+ end
+
+ helper_method :is_starred
+ def is_starred
+ links = Link.where(tail_uuid: current_user.uuid,
+ head_uuid: @object.uuid,
+ link_class: 'star')
+
+ return links.andand.any?
+ end
+
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
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
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
# 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
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'
@@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
}
}
- def check_user_notifications
- return if params['tab_pane']
-
- @notification_count = 0
- @notifications = []
-
- if current_user.andand.is_active
- @showallalerts = false
- @@notification_tests.each do |t|
- a = t.call(self, current_user)
- if a
- @notification_count += 1
- @notifications.push a
- end
- end
- end
-
- if @notification_count == 0
- @notification_count = ''
- end
+ helper_method :user_notifications
+ def user_notifications
+ return [] if @errors or not current_user.andand.is_active or not Rails.configuration.show_user_notifications_in_dashboard
+ @notifications ||= @@notification_tests.map do |t|
+ t.call(self, current_user)
+ end.compact
end
helper_method :all_projects
pi
end
- helper_method :finished_pipelines
- def finished_pipelines lim
- PipelineInstance.limit(lim).order(["finished_at desc"]).filter([["state", "in", ["Complete", "Failed", "Paused"]], ["finished_at", "!=", nil]])
+ helper_method :recent_processes
+ def recent_processes lim
+ lim = 12 if lim.nil?
+
+ cols = %w(uuid owner_uuid created_at modified_at pipeline_template_uuid name state started_at finished_at)
+ pipelines = PipelineInstance.select(cols).limit(lim).order(["created_at desc"])
+
+ crs = ContainerRequest.limit(lim).order(["created_at desc"]).filter([["requesting_container_uuid", "=", nil]])
+ procs = {}
+ pipelines.results.each { |pi| procs[pi] = pi.created_at }
+ crs.results.each { |c| procs[c] = c.created_at }
+
+ Hash[procs.sort_by {|key, value| value}].keys.reverse.first(lim)
end
helper_method :recent_collections
def recent_collections lim
- c = Collection.limit(lim).order(["modified_at desc"]).filter([["owner_uuid", "is_a", "arvados#group"]])
+ c = Collection.limit(lim).order(["modified_at desc"]).results
own = {}
Group.filter([["uuid", "in", c.map(&:owner_uuid)]]).each do |g|
own[g[:uuid]] = g
{collections: c, owners: own}
end
- helper_method :my_project_tree
- def my_project_tree
- build_project_trees
- @my_project_tree
+ helper_method :my_starred_projects
+ def my_starred_projects user
+ return if @starred_projects
+ links = Link.filter([['tail_uuid', '=', user.uuid],
+ ['link_class', '=', 'star'],
+ ['head_uuid', 'is_a', 'arvados#group']]).select(%w(head_uuid))
+ uuids = links.collect { |x| x.head_uuid }
+ starred_projects = Group.filter([['uuid', 'in', uuids]]).order('name')
+ @starred_projects = starred_projects.results
+ end
+
+ # If there are more than 200 projects that are readable by the user,
+ # build the tree using only the top 200+ projects owned by the user,
+ # from the top three levels.
+ # That is: get toplevel projects under home, get subprojects of
+ # these projects, and so on until we hit the limit.
+ def my_wanted_projects user, page_size=100
+ return @my_wanted_projects if @my_wanted_projects
+
+ from_top = []
+ uuids = [user.uuid]
+ depth = 0
+ @too_many_projects = false
+ @reached_level_limit = false
+ while from_top.size <= page_size*2
+ current_level = Group.filter([['group_class','=','project'],
+ ['owner_uuid', 'in', uuids]])
+ .order('name').limit(page_size*2)
+ break if current_level.results.size == 0
+ @too_many_projects = true if current_level.items_available > current_level.results.size
+ from_top.concat current_level.results
+ uuids = current_level.results.collect { |x| x.uuid }
+ depth += 1
+ if depth >= 3
+ @reached_level_limit = true
+ break
+ end
+ end
+ @my_wanted_projects = from_top
end
- helper_method :shared_project_tree
- def shared_project_tree
- build_project_trees
- @shared_project_tree
+ helper_method :my_wanted_projects_tree
+ def my_wanted_projects_tree user, page_size=100
+ build_my_wanted_projects_tree user, page_size
+ [@my_wanted_projects_tree, @too_many_projects, @reached_level_limit]
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|
+ def build_my_wanted_projects_tree user, page_size=100
+ return @my_wanted_projects_tree if @my_wanted_projects_tree
+
+ parent_of = {user.uuid => 'me'}
+ my_wanted_projects(user, page_size).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
+ children_of = {false => [], 'me' => [user]}
+ my_wanted_projects(user, page_size).each do |ob|
+ if ob.owner_uuid != user.uuid and
not parent_of.has_key? ob.owner_uuid
parent_of[ob.uuid] = false
end
end
paths
end
- @my_project_tree =
+ @my_wanted_projects_tree =
sorted_paths.call buildtree.call(children_of, 'me')
- @shared_project_tree =
- sorted_paths.call({'Projects shared with me' =>
- buildtree.call(children_of, false)})
end
helper_method :get_object
@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
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
def wiselinks_layout
'body'
end
+
+ def set_current_request_id
+ # Request ID format: '<timestamp>-<9_digits_random_number>'
+ current_request_id = "#{Time.new.to_i}-#{sprintf('%09d', rand(0..10**9-1))}"
+ Thread.current[:current_request_id] = current_request_id
+ end
end