Merge branch '2223-repo-owner-admin-perms'
authorTom Clegg <tom@curoverse.com>
Fri, 2 May 2014 20:27:18 +0000 (16:27 -0400)
committerTom Clegg <tom@curoverse.com>
Fri, 2 May 2014 20:27:18 +0000 (16:27 -0400)
closes #2223

doc/api/methods.html.textile.liquid
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/jobs_controller.rb
services/api/app/controllers/arvados/v1/schema_controller.rb
services/api/lib/load_param.rb
services/api/test/integration/select_test.rb [new file with mode: 0644]
services/api/test/test_helper.rb

index 9078720c060f3355ae901d64e2c38de785f7b2cc..57d058e157b7c113f529ce5398f380965b201842 100644 (file)
@@ -14,7 +14,7 @@ h2(#index). Index, list, search
 
 <pre>
 GET https://{{ site.arvados_api_host }}/arvados/v1/groups?filters=[["owner_uuid","=","xyzzy-tpzed-a4lcehql0dv2u25"]]
+
 POST https://{{ site.arvados_api_host }}/arvados/v1/groups
 _method=GET
 filters=[["owner_uuid","=","xyzzy-tpzed-a4lcehql0dv2u25"]]
@@ -24,8 +24,12 @@ filters=[["owner_uuid","=","xyzzy-tpzed-a4lcehql0dv2u25"]]
 
 table(table table-bordered table-condensed).
 |*Parameter name*|*Value*|*Description*|
-|limit|integer|Maximum number of resources to return|
-|filters|array|Conditions for selecting resources to return|
+|limit   |integer|Maximum number of resources to return|
+|offset  |integer|Skip the first 'offset' objects|
+|filters |array  |Conditions for selecting resources to return|
+|order   |array  |List of fields to use to determine sorting order for returned objects|
+|select  |array  |Specify which fields to return|
+|distinct|boolean|true: (default) do not return duplicate objects<br> false: permitted to return duplicates|
 
 h2. Create
 
index 4b5a27d99621d4d25e4c66fcb30581fadace4186..4d23b0bcbfddf07b47638979e3877682b1333087 100644 (file)
@@ -1,3 +1,16 @@
+module ApiTemplateOverride
+  def allowed_to_render?(fieldset, field, model, options)
+    if options[:select]
+      return options[:select].include? field.to_s
+    end
+    super
+  end
+end
+
+class ActsAsApi::ApiTemplate
+  prepend ApiTemplateOverride
+end
+
 require 'load_param'
 require 'record_filters'
 
@@ -9,6 +22,7 @@ class ApplicationController < ActionController::Base
 
   ERROR_ACTIONS = [:render_error, :render_not_found]
 
+
   respond_to :json
   protect_from_forgery
 
@@ -33,7 +47,7 @@ class ApplicationController < ActionController::Base
   attr_accessor :resource_attrs
 
   def index
-    @objects.uniq!(&:id)
+    @objects.uniq!(&:id) if @select.nil? or @select.include? "id"
     if params[:eager] and params[:eager] != '0' and params[:eager] != 0 and params[:eager] != ''
       @objects.each(&:eager_load_associations)
     end
@@ -246,9 +260,11 @@ class ApplicationController < ActionController::Base
       end
     end
 
+    @objects = @objects.select(@select.map { |s| "#{table_name}.#{ActiveRecord::Base.connection.quote_column_name s.to_s}" }.join ", ") if @select
     @objects = @objects.order(@orders.join ", ") if @orders.any?
     @objects = @objects.limit(@limit)
     @objects = @objects.offset(@offset)
+    @objects = @objects.uniq(@distinct) if not @distinct.nil?
   end
 
   def resource_attrs
@@ -403,7 +419,7 @@ class ApplicationController < ActionController::Base
       :self_link => "",
       :offset => @offset,
       :limit => @limit,
-      :items => @objects.as_api_response(nil)
+      :items => @objects.as_api_response(nil, {select: @select})
     }
     if @objects.respond_to? :except
       @object_list[:items_available] = @objects.
@@ -428,7 +444,9 @@ class ApplicationController < ActionController::Base
     {
       filters: { type: 'array', required: false },
       where: { type: 'object', required: false },
-      order: { type: 'string', required: false },
+      order: { type: 'array', required: false },
+      select: { type: 'array', required: false },
+      distinct: { type: 'boolean', required: false },
       limit: { type: 'integer', required: false, default: DEFAULT_LIMIT },
       offset: { type: 'integer', required: false, default: 0 },
     }
index 8ef6acdc38cf21d9e025e5a9e31478019357698f..b0d93a4b8f190f441cef441ae68b7e383460b045 100644 (file)
@@ -173,7 +173,7 @@ class Arvados::V1::JobsController < ApplicationController
                     cancelled_at: nil,
                     success: nil
                   })
-    params[:order] ||= 'priority desc, created_at'
+    params[:order] ||= ['priority desc', 'created_at']
     find_objects_for_index
     index
   end
index a15f9a0d05234f17ce6506e014eba82986d67b82..1db5eff2595792f9714cb3c812ffbe30c5b4023a 100644 (file)
@@ -231,6 +231,16 @@ class Arvados::V1::SchemaController < ApplicationController
                   type: "string",
                   description: "Order in which to return matching #{k.to_s.underscore.pluralize}.",
                   location: "query"
+                },
+                select: {
+                  type: "array",
+                  description: "Select which fields to return",
+                  location: "query"
+                },
+                distinct: {
+                  type: "boolean",
+                  description: "Return each distinct object",
+                  location: "query"
                 }
               },
               response: {
index e01063d1530a89a8e5918510a01a0523a5fc9d8f..7acf014ec6aaed42ecf91ac9501a8a852b7ba8bc 100644 (file)
@@ -69,7 +69,22 @@ module LoadParam
 
     @orders = []
     if params[:order]
-      params[:order].split(',').each do |order|
+      od = []
+      (case params[:order]
+       when String
+         if params[:order].starts_with? '['
+           od = Oj.load(params[:order])
+           raise unless od.is_a? Array
+           od
+         else
+           params[:order].split(',')
+         end
+       when Array
+         params[:order]
+       else
+         []
+       end).each do |order|
+        order = order.to_s
         attr, direction = order.strip.split " "
         direction ||= 'asc'
         if attr.match /^[a-z][_a-z0-9]+$/ and
@@ -79,9 +94,34 @@ module LoadParam
         end
       end
     end
+
     if @orders.empty?
-      @orders << default_orders
+      @orders = default_orders
     end
+
+    case params[:select]
+    when Array
+      @select = params[:select]
+    when String
+      begin
+        @select = Oj.load params[:select]
+        raise unless @select.is_a? Array
+      rescue
+        raise ArgumentError.new("Could not parse \"select\" param as an array")
+      end
+    end
+
+    if @select
+      # Any ordering columns must be selected when doing select,
+      # otherwise it is an SQL error, so filter out invaliding orderings.
+      @orders.select! { |o|
+        # match select column against order array entry
+        @select.select { |s| /^#{table_name}.#{s}( (asc|desc))?$/.match o }.any?
+      }
+    end
+
+    @distinct = true if (params[:distinct] == true || params[:distinct] == "true")
+    @distinct = false if (params[:distinct] == false || params[:distinct] == "false")
   end
 
 
diff --git a/services/api/test/integration/select_test.rb b/services/api/test/integration/select_test.rb
new file mode 100644 (file)
index 0000000..b5f09df
--- /dev/null
@@ -0,0 +1,82 @@
+require 'test_helper'
+
+class SelectTest < ActionDispatch::IntegrationTest
+  test "should select just two columns" do
+    get "/arvados/v1/links", {:format => :json, :select => ['uuid', 'link_class']}, auth(:active)
+    assert_response :success
+    assert_equal json_response['items'].count, json_response['items'].select { |i|
+      i.count == 2 and i['uuid'] != nil and i['link_class'] != nil
+    }.count
+  end
+
+  test "fewer distinct than total count" do
+    get "/arvados/v1/links", {:format => :json, :select => ['link_class'], :distinct => false}, auth(:active)
+    assert_response :success
+    links = json_response['items']
+
+    get "/arvados/v1/links", {:format => :json, :select => ['link_class'], :distinct => true}, auth(:active)
+    assert_response :success
+    distinct = json_response['items']
+
+    assert distinct.count < links.count, "distinct count should be less than link count"
+    assert_equal links.uniq.count, distinct.count
+  end
+
+  test "select with order" do
+    get "/arvados/v1/links", {:format => :json, :select => ['uuid'], :order => ["uuid asc"]}, auth(:active)
+    assert_response :success
+
+    assert json_response['items'].length > 0
+
+    p = ""
+    json_response['items'].each do |i|
+      assert i['uuid'] > p
+      p = i['uuid']
+    end
+  end
+
+  test "select two columns with order" do
+    get "/arvados/v1/links", {:format => :json, :select => ['link_class', 'uuid'], :order => ['link_class asc', "uuid desc"]}, auth(:active)
+    assert_response :success
+
+    assert json_response['items'].length > 0
+
+    prev_link_class = ""
+    prev_uuid = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+
+    json_response['items'].each do |i|
+      if prev_link_class != i['link_class']
+        prev_uuid = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+      end
+
+      assert i['link_class'] >= prev_link_class
+      assert i['uuid'] < prev_uuid
+
+      prev_link_class = i['link_class']
+      prev_uuid = i['uuid']
+    end
+  end
+
+  test "select two columns with old-style order syntax" do
+    get "/arvados/v1/links", {:format => :json, :select => ['link_class', 'uuid'], :order => 'link_class asc, uuid desc'}, auth(:active)
+    assert_response :success
+
+    assert json_response['items'].length > 0
+
+    prev_link_class = ""
+    prev_uuid = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+
+    json_response['items'].each do |i|
+      if prev_link_class != i['link_class']
+        prev_uuid = "zzzzz-zzzzz-zzzzzzzzzzzzzzz"
+      end
+
+      assert i['link_class'] >= prev_link_class
+      assert i['uuid'] < prev_uuid
+
+      prev_link_class = i['link_class']
+      prev_uuid = i['uuid']
+    end
+  end
+
+end
index 869d87f7db19f24d63743b04d410404ba4379082..1bd1b51263a1483df79fede893d52e28636e302b 100644 (file)
@@ -4,7 +4,7 @@ require 'rails/test_help'
 
 module ArvadosTestSupport
   def json_response
-    @json_response ||= ActiveSupport::JSON.decode @response.body
+    ActiveSupport::JSON.decode @response.body
   end
 
   def api_token(api_client_auth_name)