Merge branch 'master' into 2798-go-keep-client
[arvados.git] / apps / workbench / app / controllers / application_controller.rb
index 8d23c71a739f6c62c1ac02ce5ea1aec0ca415488..2fbd8c560f6dd081ef49230151d91c9d10ed03ec 100644 (file)
@@ -1,9 +1,18 @@
 class ApplicationController < ActionController::Base
+  respond_to :html, :json, :js
   protect_from_forgery
+
+  ERROR_ACTIONS = [:render_error, :render_not_found]
+
   around_filter :thread_clear
-  around_filter :thread_with_api_token, :except => [:render_exception, :render_not_found]
-  before_filter :find_object_by_uuid, :except => [:index, :render_exception, :render_not_found]
-  before_filter :check_user_agreements, :except => [:render_exception, :render_not_found]
+  around_filter(:thread_with_mandatory_api_token,
+                except: [:index, :show] + ERROR_ACTIONS)
+  around_filter :thread_with_optional_api_token
+  before_filter :check_user_agreements, except: ERROR_ACTIONS
+  before_filter :check_user_notifications, except: ERROR_ACTIONS
+  around_filter :using_reader_tokens, only: [:index, :show]
+  before_filter :find_object_by_uuid, except: [:index] + ERROR_ACTIONS
+  before_filter :check_my_folders, :except => ERROR_ACTIONS
   theme :select_theme
 
   begin
@@ -21,11 +30,13 @@ class ApplicationController < ActionController::Base
 
   def unprocessable(message=nil)
     @errors ||= []
+
     @errors << message if message
     render_error status: 422
   end
 
   def render_error(opts)
+    opts = {status: 500}.merge opts
     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
@@ -53,12 +64,32 @@ class ApplicationController < ActionController::Base
     self.render_error status: 404
   end
 
-
   def index
-    @objects ||= model_class.limit(1000).all
+    @limit ||= 200
+    if params[:limit]
+      @limit = params[:limit].to_i
+    end
+
+    @offset ||= 0
+    if params[:offset]
+      @offset = params[:offset].to_i
+    end
+
+    @filters ||= []
+    if params[:filters]
+      filters = params[:filters]
+      if filters.is_a? String
+        filters = Oj.load filters
+      end
+      @filters += filters
+    end
+
+    @objects ||= model_class
+    @objects = @objects.filter(@filters).limit(@limit).offset(@offset).all
     respond_to do |f|
       f.json { render json: @objects }
       f.html { render }
+      f.js { render }
     end
   end
 
@@ -67,7 +98,7 @@ class ApplicationController < ActionController::Base
       return render_not_found("object not found")
     end
     respond_to do |f|
-      f.json { render json: @object }
+      f.json { render json: @object.attributes.merge(href: url_for(@object)) }
       f.html {
         if request.method == 'GET'
           render
@@ -75,6 +106,7 @@ class ApplicationController < ActionController::Base
           redirect_to params[:return_to] || @object
         end
       }
+      f.js { render }
     end
   end
 
@@ -91,8 +123,16 @@ class ApplicationController < ActionController::Base
   def update
     updates = params[@object.class.to_s.underscore.singularize.to_sym]
     updates.keys.each do |attr|
-      if @object.send(attr).is_a? Hash and updates[attr].is_a? String
-        updates[attr] = Oj.load updates[attr]
+      if @object.send(attr).is_a? Hash
+        if updates[attr].is_a? String
+          updates[attr] = Oj.load updates[attr]
+        end
+        if params[:merge] || params["merge_#{attr}".to_sym]
+          # Merge provided Hash with current Hash, instead of
+          # replacing.
+          updates[attr] = @object.send(attr).with_indifferent_access.
+            deep_merge(updates[attr].with_indifferent_access)
+        end
       end
     end
     if @object.update_attributes updates
@@ -103,14 +143,23 @@ class ApplicationController < ActionController::Base
   end
 
   def create
-    @object ||= model_class.new params[model_class.to_s.singularize.to_sym]
+    @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.save!
-    redirect_to(params[:return_to] || @object)
+    show
   end
 
   def destroy
     if @object.destroy
-      redirect_to(params[:return_to] || :back)
+      respond_to do |f|
+        f.json { render json: @object }
+        f.html {
+          redirect_to(params[:return_to] || :back)
+        }
+        f.js { render }
+      end
     else
       self.render_error status: 422
     end
@@ -131,17 +180,81 @@ class ApplicationController < ActionController::Base
 
   def breadcrumb_page_name
     (@breadcrumb_page_name ||
-     (@object.friendly_link_name if @object.respond_to? :friendly_link_name))
+     (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
+     action_name)
+  end
+
+  def index_pane_list
+    %w(Recent)
+  end
+
+  def show_pane_list
+    %w(Attributes Metadata JSON API)
   end
 
   protected
-    
+
+  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)
+        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
+      }
+    end
+    false  # For convenience to return from callbacks
+  end
+
+  def using_reader_tokens(login_optional=false)
+    if params[:reader_tokens].is_a?(Array) and params[:reader_tokens].any?
+      Thread.current[:reader_tokens] = params[:reader_tokens]
+    end
+    begin
+      yield
+    rescue ArvadosApiClient::NotLoggedInException
+      if login_optional
+        raise
+      else
+        return redirect_to_login
+      end
+    ensure
+      Thread.current[:reader_tokens] = nil
+    end
+  end
+
+  def using_specific_api_token(api_token)
+    start_values = {}
+    [: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
+    begin
+      yield
+    ensure
+      start_values.each_key { |key| Thread.current[key] = start_values[key] }
+    end
+  end
+
   def find_object_by_uuid
     if params[:id] and params[:id].match /\D/
       params[:uuid] = params.delete :id
     end
-    if params[:uuid].is_a? String
-      @object = model_class.find(params[:uuid])
+    if not model_class
+      @object = nil
+    elsif params[:uuid].is_a? String
+      if params[:uuid].empty?
+        @object = nil
+      else
+        @object = model_class.find(params[:uuid])
+      end
     else
       @object = model_class.where(uuid: params[:uuid]).first
     end
@@ -150,7 +263,9 @@ class ApplicationController < ActionController::Base
   def thread_clear
     Thread.current[:arvados_api_token] = nil
     Thread.current[:user] = 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)
@@ -192,20 +307,7 @@ class ApplicationController < ActionController::Base
       end
       if try_redirect_to_login
         unless login_optional
-          respond_to do |f|
-            f.html {
-              if request.method == 'GET'
-                redirect_to $arvados_api_client.arvados_login_url(return_to: 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
-            }
-          end
+          redirect_to_login
         else
           # login is optional for this route so go on to the regular controller
           Thread.current[:arvados_api_token] = nil
@@ -218,12 +320,25 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def thread_with_optional_api_token 
-    thread_with_api_token(true) do 
+  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
+    if Thread.current[:arvados_api_token]
+      # We are already inside thread_with_mandatory_api_token.
+      yield
+    else
+      # We skipped thread_with_mandatory_api_token. Use the optional version.
+      thread_with_api_token(true) do
+        yield
+      end
+    end
+  end
+
   def verify_api_token
     begin
       Link.where(uuid: 'just-verifying-my-api-token')
@@ -267,4 +382,71 @@ class ApplicationController < ActionController::Base
   def select_theme
     return Rails.configuration.arvados_theme
   end
+
+  @@notification_tests = []
+
+  @@notification_tests.push lambda { |controller, current_user|
+    AuthorizedKey.limit(1).where(authorized_user_uuid: current_user.uuid).each do
+      return nil
+    end
+    return lambda { |view|
+      view.render partial: 'notifications/ssh_key_notification'
+    }
+  }
+
+  #@@notification_tests.push lambda { |controller, current_user|
+  #  Job.limit(1).where(created_by: current_user.uuid).each do
+  #    return nil
+  #  end
+  #  return lambda { |view|
+  #    view.render partial: 'notifications/jobs_notification'
+  #  }
+  #}
+
+  @@notification_tests.push lambda { |controller, current_user|
+    Collection.limit(1).where(created_by: current_user.uuid).each do
+      return nil
+    end
+    return lambda { |view|
+      view.render partial: 'notifications/collections_notification'
+    }
+  }
+
+  @@notification_tests.push lambda { |controller, current_user|
+    PipelineInstance.limit(1).where(created_by: current_user.uuid).each do
+      return nil
+    end
+    return lambda { |view|
+      view.render partial: 'notifications/pipelines_notification'
+    }
+  }
+
+  def check_my_folders
+    @my_top_level_folders = lambda do
+      @top_level_folders ||= Group.
+        filter([['group_class','=','folder'],
+                ['owner_uuid','=',current_user.uuid]]).
+        sort_by { |x| x.name || '' }
+    end
+  end
+
+  def check_user_notifications
+    @notification_count = 0
+    @notifications = []
+
+    if current_user
+      @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
+  end
 end