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 :accept_uuid_as_id_param, except: ERROR_ACTIONS
before_filter :check_user_agreements, except: ERROR_ACTIONS
+ before_filter :check_user_profile, except: [:update_profile] + 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: [:index, :choose] + ERROR_ACTIONS
theme :select_theme
render_error status: 422
end
- def render_error(opts)
- opts = {status: 500}.merge opts
+ def render_error(opts={})
+ opts[:status] ||= 500
respond_to do |f|
# json must come before html here, so it gets used as the
# default format when js is requested by the client. This lets
# ajax:error callback parse the response correctly, even though
# the browser can't.
f.json { render opts.merge(json: {success: false, errors: @errors}) }
- f.html { render opts.merge(controller: 'application', action: 'error') }
+ f.html { render({action: 'error'}.merge(opts)) }
end
end
def render_exception(e)
logger.error e.inspect
logger.error e.backtrace.collect { |x| x + "\n" }.join('') if e.backtrace
- if @object.andand.errors.andand.full_messages.andand.any?
+ err_opts = {status: 422}
+ if e.is_a?(ArvadosApiClient::ApiError)
+ err_opts.merge!(action: 'api_error', locals: {api_error: e})
+ @errors = e.api_response[:errors]
+ elsif @object.andand.errors.andand.full_messages.andand.any?
@errors = @object.errors.full_messages
else
@errors = [e.to_s]
end
- if e.is_a? ArvadosApiClient::NotLoggedInException
- self.render_error status: 422
- else
- thread_with_optional_api_token do
- self.render_error status: 422
- end
+ # If the user has an active session, and the API server is available,
+ # make user information available on the error page.
+ begin
+ load_api_token(session[:arvados_api_token])
+ rescue ArvadosApiClient::ApiError
+ load_api_token(nil)
+ end
+ # Preload projects trees for the template. If that fails, set empty
+ # trees so error page rendering can proceed. (It's easier to rescue the
+ # exception here than in a template.)
+ begin
+ build_project_trees
+ rescue ArvadosApiClient::ApiError
+ @my_project_tree ||= []
+ @shared_project_tree ||= []
end
+ render_error(err_opts)
end
def render_not_found(e=ActionController::RoutingError.new("Path not found"))
logger.error e.inspect
@errors = ["Path not found"]
- thread_with_optional_api_token do
- self.render_error status: 404
+ set_thread_api_token do
+ self.render_error(action: '404', status: 404)
end
end
- def find_objects_for_index
+ def load_filters_and_paging_params
+ @order = params[:order] || 'created_at desc'
+ @order = [@order] unless @order.is_a? Array
+
@limit ||= 200
if params[:limit]
@limit = params[:limit].to_i
filters = params[:filters]
if filters.is_a? String
filters = Oj.load filters
+ elsif filters.is_a? Array
+ filters = filters.collect do |filter|
+ if filter.is_a? String
+ # Accept filters[]=["foo","=","bar"]
+ Oj.load filter
+ else
+ # Accept filters=[["foo","=","bar"]]
+ filter
+ end
+ end
end
@filters += filters
end
+ end
+ def find_objects_for_index
@objects ||= model_class
@objects = @objects.filter(@filters).limit(@limit).offset(@offset)
end
respond_to do |f|
f.json { render json: @objects }
f.html {
- if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ if params[:tab_pane]
+ render_pane params[:tab_pane]
else
render
end
end
end
+ helper_method :render_pane
+ def render_pane tab_pane, opts={}
+ render_opts = {
+ partial: 'show_' + tab_pane.downcase,
+ locals: {
+ comparable: self.respond_to?(:compare),
+ objects: @objects,
+ tab_pane: tab_pane
+ }.merge(opts[:locals] || {})
+ }
+ if opts[:to_string]
+ render_to_string render_opts
+ else
+ render render_opts
+ 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
+ def next_page_offset objects=nil
+ if !objects
+ objects = @objects
+ end
+ 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
+ helper_method :next_page_href
+ def next_page_href with_params={}
+ if next_page_offset
+ url_for with_params.merge(offset: next_page_offset)
+ end
+ end
+
def show
if !@object
return render_not_found("object not found")
f.json { render json: @object.attributes.merge(href: url_for(@object)) }
f.html {
if params['tab_pane']
- comparable = self.respond_to? :compare
- render(partial: 'show_' + params['tab_pane'].downcase,
- locals: { comparable: comparable, objects: @objects })
+ render_pane params['tab_pane']
elsif request.method.in? ['GET', 'HEAD']
render
else
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
+ find_objects_for_index if !@objects
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
+ formats: [:html]),
+ next_page_href: next_page_href(partial: params[:partial])
}
}
end
if @object.name and @object.name != ''
@object.name = "Copy of #{@object.name}"
else
- @object.name = "Copy of unnamed #{@object.class_for_display.downcase}"
+ @object.name = ""
end
end
@object.save!
end
def current_user
- return Thread.current[:user] if Thread.current[:user]
-
- if Thread.current[:arvados_api_token]
- 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
- end
+ Thread.current[:user]
end
def model_class
[:arvados_api_token, :user].each do |key|
start_values[key] = Thread.current[key]
end
- Thread.current[:arvados_api_token] = api_token
- Thread.current[:user] = nil
+ load_api_token(api_token)
begin
yield
ensure
end
end
- def find_object_by_uuid
+
+ def accept_uuid_as_id_param
if params[:id] and params[:id].match /\D/
params[:uuid] = params.delete :id
end
- if not model_class
- @object = nil
- elsif params[:uuid].is_a? String
- if params[:uuid].empty?
+ end
+
+ def find_object_by_uuid
+ begin
+ if not model_class
+ @object = nil
+ elsif not params[:uuid].is_a?(String)
+ @object = model_class.where(uuid: params[:uuid]).first
+ elsif params[:uuid].empty?
@object = nil
+ elsif (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
- 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
+ @object = model_class.find(params[:uuid])
end
- else
- @object = model_class.where(uuid: params[:uuid]).first
+ rescue ArvadosApiClient::NotFoundException, RuntimeError => error
+ if error.is_a?(RuntimeError) and (error.message !~ /^argument to find\(/)
+ raise
+ end
+ render_not_found(error)
+ return false
end
end
def thread_clear
- Thread.current[:arvados_api_token] = nil
- Thread.current[:user] = nil
+ load_api_token(nil)
Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
yield
Rails.cache.delete_matched(/^request_#{Thread.current.object_id}_/)
end
- def thread_with_api_token(login_optional = false)
+ # Set up the thread with the given API token and associated user object.
+ def load_api_token(new_token)
+ Thread.current[:arvados_api_token] = new_token
+ if new_token.nil?
+ Thread.current[:user] = nil
+ else
+ Thread.current[:user] = User.current
+ end
+ end
+
+ # If there's a valid api_token parameter, set up the session with that
+ # user's information. Return true if the method redirects the request
+ # (usually a post-login redirect); false otherwise.
+ def setup_user_session
+ return false unless params[:api_token]
+ Thread.current[:arvados_api_token] = params[:api_token]
begin
- try_redirect_to_login = true
- if params[:api_token]
- 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
- try_redirect_to_login = false
- 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)
- else
- yield
- end
- 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.
- Thread.current[:arvados_api_token] = session[:arvados_api_token]
- begin
- yield
- rescue ArvadosApiClient::NotLoggedInException
- # We'll try to redirect to login later.
- else
- try_redirect_to_login = false
- end
+ user = User.current
+ rescue ArvadosApiClient::NotLoggedInException
+ false # We may redirect to login, or not, based on the current action.
+ else
+ session[:arvados_api_token] = params[:api_token]
+
+ 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)
+ true
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
- Thread.current[:arvados_api_token] = nil
- yield
- end
+ false
end
ensure
- # Remove token in case this Thread is used for anything else.
Thread.current[:arvados_api_token] = nil
end
end
- def thread_with_mandatory_api_token
- thread_with_api_token(true) do
- if Thread.current[:arvados_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
- render 'users/welcome'
- 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.
+ # If a token is unavailable or expired, the block is still run, with
+ # a nil token.
+ def set_thread_api_token
+ if Thread.current[:arvados_api_token]
+ yield # An API token has already been found - pass it through.
+ return
+ elsif setup_user_session
+ return # A new session was set up and received a response.
end
- end
- # This runs after thread_with_mandatory_api_token in the filter chain.
- def thread_with_optional_api_token
- if Thread.current[:arvados_api_token]
- # We are already inside thread_with_mandatory_api_token.
+ begin
+ load_api_token(session[:arvados_api_token])
yield
- else
- # We skipped thread_with_mandatory_api_token. Use the optional version.
- thread_with_api_token(true) do
+ 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?
+ load_api_token(nil)
yield
end
+ ensure
+ # Remove token in case this Thread is used for anything else.
+ load_api_token(nil)
end
end
- def verify_api_token
- begin
- Link.where(uuid: 'just-verifying-my-api-token')
- true
- rescue ArvadosApiClient::NotLoggedInException
- false
+ # Reroute this request if an API token is unavailable.
+ def require_thread_api_token
+ if Thread.current[:arvados_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
+ render 'users/welcome'
end
end
true
end
+ def check_user_profile
+ if request.method.downcase != 'get' || params[:partial] ||
+ params[:tab_pane] || params[:action_method] ||
+ params[:action] == 'setup_popup'
+ return true
+ end
+
+ if missing_required_profile?
+ render 'users/profile'
+ end
+ true
+ end
+
+ helper_method :missing_required_profile?
+ def missing_required_profile?
+ missing_required = false
+
+ profile_config = Rails.configuration.user_profile_form_fields
+ if current_user && profile_config
+ current_user_profile = current_user.prefs[:profile]
+ profile_config.kind_of?(Array) && profile_config.andand.each do |entry|
+ if entry['required']
+ if !current_user_profile ||
+ !current_user_profile[entry['key'].to_sym] ||
+ current_user_profile[entry['key'].to_sym].empty?
+ missing_required = true
+ break
+ end
+ end
+ end
+ end
+
+ missing_required
+ end
+
def select_theme
return Rails.configuration.arvados_theme
end
helper_method :all_projects
def all_projects
@all_projects ||= Group.
- filter([['group_class','in',['project','folder']]]).order('name')
+ filter([['group_class','=','project']]).order('name')
end
helper_method :my_projects
(Job.limit(10) |
PipelineInstance.limit(10)).
sort_by do |x|
- x.finished_at || x.started_at || x.created_at rescue x.created_at
- end
+ (x.finished_at || x.started_at rescue nil) || x.modified_at || x.created_at
+ end.reverse
end
helper_method :my_project_tree
crumbs = []
current = @name_link || @object
while current
- if current.is_a?(Group) and current.group_class.in?(['project','folder'])
+ if current.is_a?(Group) and current.group_class == 'project'
crumbs.prepend current
end
if current.is_a? Link
helper_method :current_project_uuid
def current_project_uuid
- if @object.is_a? Group and @object.group_class.in?(['project','folder'])
+ if @object.is_a? Group and @object.group_class == 'project'
@object.uuid
elsif @name_link.andand.tail_uuid
@name_link.tail_uuid
def get_n_objects_of_class dataclass, size
@objects_map_for ||= {}
- raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class
+ raise ArgumentError, 'Argument is not a data class' unless dataclass.is_a? Class and dataclass < 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
@objects_for
end
+ def wiselinks_layout
+ 'body'
+ end
end