[ARVADOS] created: 420949e37a2903ab87f64f57278dfdc6a261a7f3

git at public.curoverse.com git at public.curoverse.com
Tue Apr 29 00:38:46 EDT 2014


        at  420949e37a2903ab87f64f57278dfdc6a261a7f3 (commit)


commit 420949e37a2903ab87f64f57278dfdc6a261a7f3
Merge: 2943d9c 34350a8
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Apr 29 00:38:22 2014 -0400

    Merge branch '2640-folder-api' into 1970-folder-view


commit 34350a8b802a8c48b534673a712614d36a5b97ac
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Apr 29 00:37:53 2014 -0400

    Skip UserAgreement in owned_items. They are just collections again.

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 0f144d8..d9f01f5 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -88,7 +88,7 @@ class ApplicationController < ActionController::Base
         # disappointed: when Rails reloads model classes, we get two
         # distinct classes called Link which do not equal each
         # other. But we can still rely on klass.to_s to be "Link".
-      when 'ApiClientAuthorization'
+      when 'ApiClientAuthorization', 'UserAgreement'
         # Do not want.
       else
         @objects = klass.readable_by(current_user)

commit 2943d9c3622e2c5bca081dc48fd5d8d148dac386
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Apr 29 00:37:01 2014 -0400

    Add folders page, backed by groups.

diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb
index 2e3a596..169f304 100644
--- a/apps/workbench/app/controllers/application_controller.rb
+++ b/apps/workbench/app/controllers/application_controller.rb
@@ -170,6 +170,10 @@ class ApplicationController < ActionController::Base
     controller_name.classify.constantize
   end
 
+  def model_class_for_display
+    model_class.to_s
+  end
+
   def breadcrumb_page_name
     (@breadcrumb_page_name ||
      (@object.friendly_link_name if @object.respond_to? :friendly_link_name) ||
diff --git a/apps/workbench/app/controllers/groups_controller.rb b/apps/workbench/app/controllers/groups_controller.rb
index 672fe90..358cb2c 100644
--- a/apps/workbench/app/controllers/groups_controller.rb
+++ b/apps/workbench/app/controllers/groups_controller.rb
@@ -1,6 +1,14 @@
 class GroupsController < ApplicationController
+  def model_class_for_display
+    params[:group_class] || super
+  end
+
   def index
-    @groups = Group.all
+    if params[:group_class]
+      @groups = Group.where(group_class: params[:group_class])
+    else
+      @groups = Group.all
+    end
     @group_uuids = @groups.collect &:uuid
     @links_from = Link.where link_class: 'permission', tail_uuid: @group_uuids
     @links_to = Link.where link_class: 'permission', head_uuid: @group_uuids
diff --git a/apps/workbench/app/models/arvados_base.rb b/apps/workbench/app/models/arvados_base.rb
index 81732ba..45a4d8b 100644
--- a/apps/workbench/app/models/arvados_base.rb
+++ b/apps/workbench/app/models/arvados_base.rb
@@ -244,6 +244,10 @@ class ArvadosBase < ActiveRecord::Base
     }
   end
 
+  def class_for_display
+    self.class.to_s
+  end
+
   def self.creatable?
     current_user
   end
diff --git a/apps/workbench/app/models/arvados_resource_list.rb b/apps/workbench/app/models/arvados_resource_list.rb
index a474b13..f6bcaae 100644
--- a/apps/workbench/app/models/arvados_resource_list.rb
+++ b/apps/workbench/app/models/arvados_resource_list.rb
@@ -90,6 +90,12 @@ class ArvadosResourceList
     self
   end
 
+  def collect
+    results.collect do |m|
+      yield m
+    end
+  end
+
   def first
     results.first
   end
diff --git a/apps/workbench/app/views/application/_delete_object_button.html.erb b/apps/workbench/app/views/application/_delete_object_button.html.erb
index 67a3d06..52a568f 100644
--- a/apps/workbench/app/views/application/_delete_object_button.html.erb
+++ b/apps/workbench/app/views/application/_delete_object_button.html.erb
@@ -1,5 +1,5 @@
 <% if object.editable? %>
-  <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "You are about to delete #{object.class} #{object.uuid}.\n\nAre you sure?"}) do %>
+  <%= link_to({action: 'destroy', id: object.uuid}, method: :delete, remote: true, data: {confirm: "You are about to delete #{object.class_for_display} #{object.uuid}.\n\nAre you sure?"}) do %>
     <i class="glyphicon glyphicon-trash"></i>
   <% end %>
 <% end %>
diff --git a/apps/workbench/app/views/application/_show_recent.html.erb b/apps/workbench/app/views/application/_show_recent.html.erb
index 04387ff..b02ce19 100644
--- a/apps/workbench/app/views/application/_show_recent.html.erb
+++ b/apps/workbench/app/views/application/_show_recent.html.erb
@@ -1,7 +1,7 @@
 <% if @objects.empty? %>
 <br/>
 <p style="text-align: center">
-  No <%= controller.model_class.to_s.pluralize.underscore.gsub '_', ' ' %> to display.
+  No <%= controller.model_class_for_display.pluralize.underscore.gsub '_', ' ' %> to display.
 </p>
 
 <% else %>
diff --git a/apps/workbench/app/views/application/index.html.erb b/apps/workbench/app/views/application/index.html.erb
index 3f31240..f6bd49c 100644
--- a/apps/workbench/app/views/application/index.html.erb
+++ b/apps/workbench/app/views/application/index.html.erb
@@ -12,7 +12,7 @@
           'data-target' => '#user-setup-modal-window', return_to: request.url}  %>
       <div id="user-setup-modal-window" class="modal fade" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"></div>
     <% else %>
-      <%= button_to "Add a new #{controller.model_class.to_s.underscore.gsub '_', ' '}",
+      <%= button_to "Add a new #{controller.model_class_for_display.underscore.gsub '_', ' '}",
         { action: 'create', return_to: request.url },
         { class: 'btn btn-primary pull-right' } %>
     <% end %>
diff --git a/apps/workbench/app/views/groups/_show_recent.html.erb b/apps/workbench/app/views/groups/_show_recent.html.erb
index c709e89..83b31cc 100644
--- a/apps/workbench/app/views/groups/_show_recent.html.erb
+++ b/apps/workbench/app/views/groups/_show_recent.html.erb
@@ -4,7 +4,7 @@
   <thead>
     <tr class="contain-align-left">
       <th>
-	Group
+	<%= controller.model_class_for_display.capitalize %>
       </th><th>
 	Owner
       </th><th>
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index 04e5dfe..094cbc9 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -85,7 +85,7 @@
             <li><a href="/pipeline_templates">
                 <i class="fa fa-lg fa-gears fa-fw"></i> Pipeline templates
             </a></li>
-            <li><a href="/groups">
+            <li><a href="/folders">
                 <i class="fa fa-lg fa-folder-o fa-fw"></i> Folders
             </a></li>
             <li class="dropdown">
@@ -138,7 +138,7 @@
               <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
               <li>
                 <%= link_to(
-                            controller.model_class.to_s.pluralize.underscore.gsub('_', ' '),
+                            controller.model_class_for_display.pluralize.underscore.gsub('_', ' '),
                             url_for({controller: params[:controller]})) %>
               </li>
               <% if params[:action] != 'index' %>
diff --git a/apps/workbench/config/routes.rb b/apps/workbench/config/routes.rb
index 9890ce4..4fb6578 100644
--- a/apps/workbench/config/routes.rb
+++ b/apps/workbench/config/routes.rb
@@ -42,6 +42,7 @@ ArvadosWorkbench::Application.routes.draw do
   match '/collections/graph' => 'collections#graph'
   resources :collections
   get '/collections/:uuid/*file' => 'collections#show_file', :format => false
+  resources :folders, controller: :groups, group_class: 'folder'
 
   post 'actions' => 'actions#post'
 

commit bd240259a9d95a4da53eb0ff8a3644d7acd7705d
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Apr 29 00:00:35 2014 -0400

    Add sb-admin layout.

diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss
index ff97da8..c918125 100644
--- a/apps/workbench/app/assets/stylesheets/application.css.scss
+++ b/apps/workbench/app/assets/stylesheets/application.css.scss
@@ -91,25 +91,6 @@ form.small-form-margin {
     text-decoration: none;
     text-shadow: 0 1px 0 #ffffff;
 }
-/*.navbar .nav .dropdown .dropdown-menu li a {
-    padding: 2px 20px;
-}*/
-
-ul.arvados-nav {
-    list-style: none;
-    padding-left: 0em;
-    margin-left: 0em;
-}
-
-ul.arvados-nav li ul {
-    list-style: none;
-    padding-left: 0;
-}
-
-ul.arvados-nav li ul li {
-    list-style: none;
-    padding-left: 1em;
-}
 
 .dax {
     max-width: 10%;
@@ -151,20 +132,6 @@ span.removable-tag-container {
 li.notification {
     padding: 10px;
 }
-.arvados-nav-container {
-    top: 70px; 
-    height: calc(100% - 70px); 
-    overflow: auto; 
-    z-index: 2;
-}
-
-.arvados-nav-active {
-    background: rgb(66, 139, 202);
-}
-
-.arvados-nav-active a, .arvados-nav-active a:hover {
-    color: white;
-}
 
 // See HeaderRowFixer in application.js
 table.table-fixed-header-row {
@@ -189,3 +156,6 @@ table.table-fixed-header-row tbody {
     overflow-y: auto;
 }
 
+.row-fill-height, .row-fill-height>div[class*='col-'] {
+    display: flex;
+}
diff --git a/apps/workbench/app/assets/stylesheets/sb-admin.css.scss b/apps/workbench/app/assets/stylesheets/sb-admin.css.scss
new file mode 100644
index 0000000..e9b99d6
--- /dev/null
+++ b/apps/workbench/app/assets/stylesheets/sb-admin.css.scss
@@ -0,0 +1,165 @@
+/* 
+Author: Start Bootstrap - http://startbootstrap.com
+'SB Admin' HTML Template by Start Bootstrap
+
+All Start Bootstrap themes are licensed under Apache 2.0. 
+For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com!
+*/
+
+/* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */
+
+/* Global Styles */
+
+body {
+  margin-top: 50px;
+}
+
+#wrapper {
+  padding-left: 0;
+}
+
+#page-wrapper {
+  width: 100%;
+  padding: 5px 15px;
+}
+
+/* Nav Messages */
+
+.messages-dropdown .dropdown-menu .message-preview .avatar,
+.messages-dropdown .dropdown-menu .message-preview .name,
+.messages-dropdown .dropdown-menu .message-preview .message,
+.messages-dropdown .dropdown-menu .message-preview .time {
+  display: block;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .avatar {
+  float: left;
+  margin-right: 15px;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .name {
+  font-weight: bold;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .message {
+  font-size: 12px;
+}
+
+.messages-dropdown .dropdown-menu .message-preview .time {
+  font-size: 12px;
+}
+
+
+/* Nav Announcements */
+
+.announcement-heading {
+  font-size: 50px;
+  margin: 0;
+}
+
+.announcement-text {
+  margin: 0;
+}
+
+/* Table Headers */
+
+table.tablesorter thead {
+  cursor: pointer;
+}
+
+table.tablesorter thead tr th:hover {
+  background-color: #f5f5f5;
+}
+
+/* Flot Chart Containers */
+
+.flot-chart {
+  display: block;
+  height: 400px;
+}
+
+.flot-chart-content {
+  width: 100%;
+  height: 100%;
+}
+
+/* Edit Below to Customize Widths > 768px */
+ at media (min-width:768px) {
+
+  /* Wrappers */
+
+  #wrapper {
+        padding-left: 225px;
+  }
+
+  #page-wrapper {
+        padding: 15px 25px;
+        border-left: 1px solid #e7e7e7;
+  }
+
+  /* Side Nav */
+
+  .side-nav {
+        margin-left: -225px;
+        left: 225px;
+        width: 225px;
+        position: fixed;
+        top: 50px;
+        height: calc(100% - 50px);
+        border-radius: 0;
+        border: none;
+        background-color: #f8f8f8;
+        overflow-y: auto;
+        overflow-x: hidden; /* no left nav scroll bar */
+  }
+
+  /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */
+
+  .side-nav>li.dropdown>ul.dropdown-menu {
+        position: relative;
+        min-width: 225px;
+        margin: 0;
+        padding: 0;
+        border: none;
+        border-radius: 0;
+        background-color: transparent;
+        box-shadow: none;
+        -webkit-box-shadow: none;
+  }
+
+  .side-nav>li.dropdown>ul.dropdown-menu>li>a {
+        color: #777777;
+        padding: 15px 15px 15px 25px;
+  }
+
+  .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover,
+  .side-nav>li.dropdown>ul.dropdown-menu>li>a.active,
+  .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus {
+        background-color: #f8ffff;
+  }
+
+  .side-nav>li>a {
+        width: 225px;
+  }
+
+  .navbar-default .navbar-nav.side-nav>li>a:hover,
+  .navbar-default .navbar-nav.side-nav>li>a:focus {
+        background-color: #f8ffff;
+  }
+
+  /* Nav Messages */
+
+  .messages-dropdown .dropdown-menu {
+        min-width: 300px;
+  }
+
+  .messages-dropdown .dropdown-menu li a {
+        white-space: normal;
+  }
+
+  .navbar-collapse {
+    padding-left: 15px !important;
+    padding-right: 15px !important;
+  }
+
+}
diff --git a/apps/workbench/app/views/groups/show.html.erb b/apps/workbench/app/views/groups/show.html.erb
index 41541f8..b6f970f 100644
--- a/apps/workbench/app/views/groups/show.html.erb
+++ b/apps/workbench/app/views/groups/show.html.erb
@@ -4,76 +4,80 @@
 }
 <% end %>
 
-<% content_for :above_left_nav do %>
-<div class="panel panel-info">
-  <div class="panel-heading">
-    <a class="btn btn-xs btn-info pull-right">
-      Rename
-    </a>
-    <h3 class="panel-title">
-      <%= @object.name %>
-    </h3>
-  </div>
-  <div class="panel-body">
-    <img src="/favicon.ico" class="pull-right" alt=""/>
-    <p>
-      This folder was created <%= @object.created_at %>. (This
-      description defaults to something generic.)
-    </p>
-    <a href="#" class="btn btn-xs btn-info">Edit description</a>
-  </div>
-</div>
-
-<div class="panel panel-default">
-  <div class="panel-heading">
-    <h3 class="panel-title">
-      Activity
-    </h3>
-  </div>
-  <div class="panel-body">
-    <input type="text" class="form-control" placeholder="Search"/>
-    <div style="height:0.5em;"></div>
-    <p>
-      11:12 - Some Subfolder added
-    </p>
-    <p>
-      10:06 - <%= @object.name %> - renamed from OldFolderName to <%= @object.name %>
-    </p>
-    <p>
-      10:01 - Test Dataset (4 GiB collection) added to <%= @object.name %> by <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %>
-    </p>
+<div class="row row-fill-height">
+  <div class="col-md-6">
+    <div class="panel panel-info">
+      <div class="panel-heading">
+	<a class="btn btn-xs btn-info pull-right">
+	  Rename
+	</a>
+	<h3 class="panel-title">
+	  <%= @object.name %>
+	</h3>
+      </div>
+      <div class="panel-body">
+	<img src="/favicon.ico" class="pull-right" alt=""/>
+	<p>
+	  This folder was created <%= @object.created_at %>. (This
+	  description defaults to something generic.)
+	</p>
+	<a href="#" class="btn btn-xs btn-info">Edit description</a>
+      </div>
+    </div>
   </div>
-</div>
-
-<div class="panel panel-default">
-  <div class="panel-heading">
-    <h3 class="panel-title">
-      Sharing and permissions
-    </h3>
+  <div class="col-md-3">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+	<h3 class="panel-title">
+	  Activity
+	</h3>
+      </div>
+      <div class="panel-body">
+	<input type="text" class="form-control" placeholder="Search"/>
+	<div style="height:0.5em;"></div>
+	<p>
+	  11:12 - Some Subfolder added
+	</p>
+	<p>
+	  10:06 - <%= @object.name %> - renamed from OldFolderName to <%= @object.name %>
+	</p>
+	<p>
+	  10:01 - Test Dataset (4 GiB collection) added to <%= @object.name %> by <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %>
+	</p>
+      </div>
+    </div>
   </div>
-  <div class="panel-body">
-    <input type="text" class="form-control" placeholder="Search"/>
-    <div style="height:0.5em;"></div>
-    <table class="table table-condensed">
-      <tbody>
-        <tr>
-          <td><%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %></td>
-          <td>Owner</td>
-        </tr>
-        <tr>
-          <td>Someone Else</td>
-          <td>read only</td>
-        </tr>
-        <tr>
-          <td>Someone Else</td>
-          <td>read+write</td>
-        </tr>
-      </tbody>
-      <thead><tr><th>User</th><th>Role</th></tr></thead>
-    </table>
+  <div class="col-md-3">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+	<h3 class="panel-title">
+	  Sharing and permissions
+	</h3>
+      </div>
+      <div class="panel-body">
+	<input type="text" class="form-control" placeholder="Search"/>
+	<div style="height:0.5em;"></div>
+	<table class="table table-condensed">
+	  <tbody>
+	    <tr>
+	      <td><%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %></td>
+	      <td>Owner</td>
+	    </tr>
+	    <tr>
+	      <td>Someone Else</td>
+	      <td>read only</td>
+	    </tr>
+	    <tr>
+	      <td>Someone Else</td>
+	      <td>read+write</td>
+	    </tr>
+	  </tbody>
+	  <thead><tr><th>User</th><th>Role</th></tr></thead>
+	</table>
+      </div>
+    </div>
   </div>
 </div>
-<% end %>
 
 <div class="row">
   <div class="card arvados-object">
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index ece2f2e..04e5dfe 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -32,34 +32,25 @@
     padding-top: 70px; /* 70px to make the container go all the way to the bottom of the navbar */
     }
 
-    body > div.container-fluid > div.col-sm-9.col-sm-offset-3 {
-    overflow: auto;
-    }
-
     @media (max-width: 979px) { body { padding-top: 0; } }
 
     .navbar .nav li.nav-separator > span.glyphicon.glyphicon-arrow-right {
     padding-top: 1.25em;
     }
 
-    @media (min-width: 768px) {
-    .left-nav {
-    position: fixed;
-    }
-    }
     @media (max-width: 767px) {
     .breadcrumbs {
     display: none;
     }
     }
   </style>
+  <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
 </head>
 <body>
-
-  <div class="navbar navbar-default navbar-fixed-top">
-    <div class="container-fluid">
+  <div id="wrapper">
+    <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
       <div class="navbar-header">
-        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#workbench-navbar.navbar-collapse">
+        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
           <span class="sr-only">Toggle navigation</span>
           <span class="icon-bar"></span>
           <span class="icon-bar"></span>
@@ -68,168 +59,185 @@
         <a class="navbar-brand" href="/"><%= Rails.configuration.site_name rescue Rails.application.class.parent_name %></a>
       </div>
 
-      <div class="collapse navbar-collapse" id="workbench-navbar">
-      <ul class="nav navbar-nav navbar-left breadcrumbs">
-        <% if current_user %>
-        <% if content_for?(:breadcrumbs) %>
-          <%= yield(:breadcrumbs) %>
-        <% else %>
-          <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
-          <li>
-            <%= link_to(
-                        controller.model_class.to_s.pluralize.underscore.gsub('_', ' '),
-                        url_for({controller: params[:controller]})) %>
-          </li>
-          <% if params[:action] != 'index' %>
-            <li class="nav-separator">
-              <span class="glyphicon glyphicon-arrow-right"></span>
+      <div class="collapse navbar-collapse">
+          <ul class="nav navbar-nav side-nav">
+            <% if current_user.andand.is_active %>
+
+            <li class="<%= 'arvados-nav-active' if params[:action] == 'home' %>">
+              <a href="/"><i class="fa fa-lg fa-dashboard fa-fw"></i> Dashboard</a>
             </li>
-            <li>
-              <%= link_to_if_arvados_object @object %>
+
+            <li class="dropdown">
+              <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-lg fa-hand-o-up fa-fw"></i> Help <b class="caret"></b></a>
+              <ul class="dropdown-menu">
+                <li><%= link_to raw('<i class="fa fa-lg fa-book fa-fw"></i> Tutorials and User guide'), "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
+                <li><%= link_to raw('<i class="fa fa-lg fa-book fa-fw"></i> API Reference'), "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
+                <li><%= link_to raw('<i class="fa fa-lg fa-book fa-fw"></i> SDK Reference'), "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+              </ul>
             </li>
-            <li style="padding: 14px 0 14px">
-              <%= form_tag do |f| %>
-                <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
-              <% end %>
+
+            <li><a href="/collections">
+                <i class="fa fa-lg fa-briefcase fa-fw"></i> Collections (data files)
+            </a></li>
+            <li><a href="/pipeline_instances">
+                <i class="fa fa-lg fa-tasks fa-fw"></i> Pipeline instances
+            </a></li>
+            <li><a href="/pipeline_templates">
+                <i class="fa fa-lg fa-gears fa-fw"></i> Pipeline templates
+            </a></li>
+            <li><a href="/groups">
+                <i class="fa fa-lg fa-folder-o fa-fw"></i> Folders
+            </a></li>
+            <li class="dropdown">
+              <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-lg fa-ellipsis-h fa-fw"></i> More <b class="caret"></b></a>
+              <ul class="dropdown-menu">
+                <li><a href="/humans">
+                    <i class="fa fa-lg fa-male fa-fw"></i> Humans
+                </a></li>
+                <li><a href="/specimens">
+                    <i class="fa fa-lg fa-flask fa-fw"></i> Specimens
+                </a></li>
+                <li><a href="/traits">
+                    <i class="fa fa-lg fa-clipboard fa-fw"></i> Traits
+                </a></li>
+                <li><a href="/links">
+                    <i class="fa fa-lg fa-arrows-h fa-fw"></i> Links
+                </a></li>
+                <li><a href="/repositories">
+                    <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
+                </a></li>
+                <li><a href="/virtual_machines">
+                    <i class="fa fa-lg fa-ellipsis-h fa-fw"></i> Virtual machines
+                </a></li>
+                <% if current_user.andand.is_admin %>
+                  <li><a href="/users">
+                      <i class="fa fa-lg fa-user fa-fw"></i> Users
+                  </a></li>
+                <% end %>
+                <li><a href="/groups">
+                    <i class="fa fa-lg fa-users fa-fw"></i> Groups
+                </a></li>
+                <li><a href="/nodes">
+                    <i class="fa fa-lg fa-cogs fa-fw"></i> Compute nodes
+                </a></li>
+                <li><a href="/keep_disks">
+                    <i class="fa fa-lg fa-hdd-o fa-fw"></i> Keep disks
+                </a></li>
+              </ul>
             </li>
-          <% end %>
-        <% end %>
-        <% end %>
-      </ul>
-
-      <ul class="nav navbar-nav navbar-right">
-
-        <li>
-          <a><i class="rotating loading glyphicon glyphicon-refresh"></i></a>
-        </li>
-
-        <% if current_user %>
-        <!-- XXX placeholder for this when search is implemented
-        <li>
-          <form class="navbar-form" role="search">
-            <div class="input-group" style="width: 220px">
-              <input type="text" class="form-control" placeholder="search">
-              <span class="input-group-addon"><span class="glyphicon glyphicon-search"></span></span>
-            </div>
-          </form>
-        </li>
-        -->
-
-        <li class="dropdown notification-menu">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
-            <span class="glyphicon glyphicon-paperclip"></span>
-            <span class="badge" id="persistent-selection-count"></span>
-            <span class="caret"></span>
-          </a>
-            <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
-              <%= form_tag '/actions' do %>
-              <div id="selection-form-content"></div>
-              <% end %>
-          </ul>
-        </li>
-
-        <% if current_user.is_active %>
-        <li class="dropdown notification-menu">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
-            <span class="glyphicon glyphicon-envelope"></span>
-            <span class="badge badge-alert notification-count"><%= @notification_count %></span>
-            <span class="caret"></span>
-          </a>
-          <ul class="dropdown-menu" role="menu">
-            <% if (@notifications || []).length > 0 %>
-              <% @notifications.each_with_index do |n, i| %>
-                <% if i > 0 %><li class="divider"></li><% end %>
-                <li class="notification"><%= n.call(self) %></li>
-              <% end %>
-            <% else %>
-              <li class="notification empty">No notifications.</li>
             <% end %>
           </ul>
-        </li>
-        <% end %>
-
-        <li class="dropdown">
-          <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="user-menu">
-            <span class="glyphicon glyphicon-user"></span><span class="caret"></span>
-          </a>
-          <ul class="dropdown-menu" role="menu">
-            <li role="presentation" class="dropdown-header"><%= current_user.email %></li>
-            <% if current_user.is_active %>
-            <li role="presentation" class="divider"></li>
-            <li role="presentation"><a href="/authorized_keys" role="menuitem">Manage ssh keys</a></li>
-            <li role="presentation"><a href="/api_client_authorizations" role="menuitem">Manage API tokens</a></li>
-            <li role="presentation" class="divider"></li>
+
+
+
+        <ul class="nav navbar-nav navbar-left breadcrumbs">
+          <% if current_user %>
+            <% if content_for?(:breadcrumbs) %>
+              <%= yield(:breadcrumbs) %>
+            <% else %>
+              <li class="nav-separator"><span class="glyphicon glyphicon-arrow-right"></span></li>
+              <li>
+                <%= link_to(
+                            controller.model_class.to_s.pluralize.underscore.gsub('_', ' '),
+                            url_for({controller: params[:controller]})) %>
+              </li>
+              <% if params[:action] != 'index' %>
+                <li class="nav-separator">
+                  <span class="glyphicon glyphicon-arrow-right"></span>
+                </li>
+                <li>
+                  <%= link_to_if_arvados_object @object %>
+                </li>
+                <li style="padding: 14px 0 14px">
+                  <%= form_tag do |f| %>
+                    <%= render :partial => "selection_checkbox", :locals => {:object => @object} %>
+                  <% end %>
+                </li>
+              <% end %>
             <% end %>
-            <li role="presentation"><a href="<%= logout_path %>" role="menuitem">Log out</a></li>
-          </ul>
-        </li>
-	<% else -%>
-          <li><a href="<%= $arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
-	<% end -%>
-      </ul>
-      </div><!-- /.navbar-collapse -->
-    </div><!-- /.container-fluid -->
-  </div>
+          <% end %>
+        </ul>
 
-  <div class="container-fluid">
-      <div class="col-sm-9 col-sm-offset-3">
-        <div id="content" class="body-content">
-          <%= yield %>
-        </div>
-      </div>
-      <div class="col-sm-3 left-nav">
-        <%= yield :above_left_nav %>
-        <div class="arvados-nav-container">
-        <% if current_user.andand.is_active %>
-        <div class="well">
-        <ul class="arvados-nav">
-          <li class="<%= 'arvados-nav-active' if params[:action] == 'home' %>">
-            <a href="/">Dashboard</a>
+        <ul class="nav navbar-nav navbar-right">
+
+          <li>
+            <a><i class="rotating loading glyphicon glyphicon-refresh"></i></a>
+          </li>
+
+          <% if current_user %>
+          <!-- XXX placeholder for this when search is implemented
+          <li>
+            <form class="navbar-form" role="search">
+              <div class="input-group" style="width: 220px">
+                <input type="text" class="form-control" placeholder="search">
+                <span class="input-group-addon"><span class="glyphicon glyphicon-search"></span></span>
+              </div>
+            </form>
+          </li>
+          -->
+
+          <li class="dropdown notification-menu">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="collections-menu">
+              <span class="glyphicon glyphicon-paperclip"></span>
+              <span class="badge" id="persistent-selection-count"></span>
+              <span class="caret"></span>
+            </a>
+              <ul class="dropdown-menu" role="menu" id="persistent-selection-list">
+                <%= form_tag '/actions' do %>
+                <div id="selection-form-content"></div>
+                <% end %>
+            </ul>
           </li>
 
-          <% [['Data', [['collections', 'Collections (data files)'],
-                        ['humans'],
-                        ['traits'],
-                        ['specimens'],
-                        ['links']]],
-              ['Activity', [['pipeline_instances', 'Recent pipeline instances'],
-                            ['jobs', 'Recent jobs']]],
-              ['Compute', [['pipeline_templates'],
-                           ['repositories', 'Code repositories'],
-                           ['virtual_machines']]],
-              ['System', [['users'],
-                         ['groups'],
-                         ['nodes', 'Compute nodes'],
-                         ['keep_disks']]]].each do |j| %>
-            <li><%= j[0] %>
-              <ul>
-              <% j[1].each do |k| %>
-                <% unless k[0] == 'users' and !current_user.andand.is_admin %>
-                  <li class="<%= 'arvados-nav-active' if (params[:controller] == k[0] && params[:action] != 'home') %>">
-                    <a href="/<%= k[0] %>">
-                      <%= if k[1] then k[1] else k[0].capitalize.gsub('_', ' ') end %>
-                    </a>
-                  </li>
+          <% if current_user.is_active %>
+          <li class="dropdown notification-menu">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
+              <span class="glyphicon glyphicon-envelope"></span>
+              <span class="badge badge-alert notification-count"><%= @notification_count %></span>
+              <span class="caret"></span>
+            </a>
+            <ul class="dropdown-menu" role="menu">
+              <% if (@notifications || []).length > 0 %>
+                <% @notifications.each_with_index do |n, i| %>
+                  <% if i > 0 %><li class="divider"></li><% end %>
+                  <li class="notification"><%= n.call(self) %></li>
                 <% end %>
+              <% else %>
+                <li class="notification empty">No notifications.</li>
               <% end %>
-              </ul>
-            </li>
+            </ul>
+          </li>
           <% end %>
 
-          <li>Help
-            <ul>
-              <li><%= link_to 'Tutorials and User guide', "#{Rails.configuration.arvados_docsite}/user", target: "_blank" %></li>
-              <li><%= link_to 'API Reference', "#{Rails.configuration.arvados_docsite}/api", target: "_blank" %></li>
-              <li><%= link_to 'SDK Reference', "#{Rails.configuration.arvados_docsite}/sdk", target: "_blank" %></li>
+          <li class="dropdown">
+            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="user-menu">
+              <span class="glyphicon glyphicon-user"></span><span class="caret"></span>
+            </a>
+            <ul class="dropdown-menu" role="menu">
+              <li role="presentation" class="dropdown-header"><%= current_user.email %></li>
+              <% if current_user.is_active %>
+              <li role="presentation" class="divider"></li>
+              <li role="presentation"><a href="/authorized_keys" role="menuitem"><i class="fa fa-key fa-fw"></i> Manage ssh keys</a></li>
+              <li role="presentation"><a href="/api_client_authorizations" role="menuitem"><i class="fa fa-ticket fa-fw"></i> Manage API tokens</a></li>
+              <li role="presentation" class="divider"></li>
+              <% end %>
+              <li role="presentation"><a href="<%= logout_path %>" role="menuitem"><i class="fa fa-sign-out fa-fw"></i> Log out</a></li>
             </ul>
           </li>
+          <% else %>
+            <li><a href="<%= $arvados_api_client.arvados_login_url(return_to: root_url) %>">Log in</a></li>
+          <% end %>
         </ul>
-        </div>
-        <% end %>
-      </div>
-        </div>
+      </div><!-- /.navbar-collapse -->
+    </nav>
+
+    <div id="page-wrapper">
+      <%= yield %>
+    </div>
   </div>
 
+</div>
+
   <%= yield :footer_html %>
   <%= piwik_tracking_tag %>
   <%= javascript_tag do %>

commit e35fb48f1485a92a64a30efe8b1c43a179b70260
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 28 19:30:35 2014 -0400

    Show folder contents with editable names.

diff --git a/apps/workbench/app/assets/javascripts/editable.js b/apps/workbench/app/assets/javascripts/editable.js
index e6799bf..24da286 100644
--- a/apps/workbench/app/assets/javascripts/editable.js
+++ b/apps/workbench/app/assets/javascripts/editable.js
@@ -1,4 +1,4 @@
-$.fn.editable.defaults.ajaxOptions = {type: 'put', dataType: 'json'};
+$.fn.editable.defaults.ajaxOptions = {type: 'post', dataType: 'json'};
 $.fn.editable.defaults.send = 'always';
 
 // Default for editing is popup.  I experimented with inline which is a little
@@ -13,8 +13,13 @@ $.fn.editable.defaults.params = function (params) {
     var a = {};
     var key = params.pk.key;
     a.id = params.pk.id;
-    a[key] = {};
+    a[key] = params.pk.defaults || {};
     a[key][params.name] = params.value;
+    if (params.pk._method) {
+        a['_method'] = params.pk._method;
+    } else {
+        a['_method'] = 'put';
+    }
     return a;
 };
 
@@ -24,6 +29,13 @@ $.fn.editable.defaults.validate = function (value) {
     }
 }
 
+$(document).
+    on('ready ajax:complete', function() {
+        $('#editable-submit').click(function() {
+            console.log($(this));
+        });
+    });
+
 $.fn.editabletypes.text.defaults.tpl = '<input type="text" name="editable-text">'
 
 $.fn.editableform.buttons = '\
diff --git a/apps/workbench/app/assets/stylesheets/application.css.scss b/apps/workbench/app/assets/stylesheets/application.css.scss
index 455e4c0..ff97da8 100644
--- a/apps/workbench/app/assets/stylesheets/application.css.scss
+++ b/apps/workbench/app/assets/stylesheets/application.css.scss
@@ -44,6 +44,10 @@ table.table-justforlayout {
     font-size: .8em;
     color: #888;
 }
+.arvados-uuid {
+    font-size: .8em;
+    font-family: monospace;
+}
 table .data-size, .table .data-size {
     text-align: right;
 }
diff --git a/apps/workbench/app/assets/stylesheets/cards.css.scss b/apps/workbench/app/assets/stylesheets/cards.css.scss
new file mode 100644
index 0000000..c9560ad
--- /dev/null
+++ b/apps/workbench/app/assets/stylesheets/cards.css.scss
@@ -0,0 +1,85 @@
+.card {
+    padding-top: 20px;
+    margin: 10px 0 20px 0;
+    background-color: #ffffff;
+    border: 1px solid #d8d8d8;
+    border-top-width: 0;
+    border-bottom-width: 2px;
+    -webkit-border-radius: 3px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    -webkit-box-shadow: none;
+    -moz-box-shadow: none;
+    box-shadow: none;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.card.arvados-object {
+    position: relative;
+    display: inline-block;
+    width: 170px;
+    height: 175px;
+    padding-top: 0;
+    margin-left: 20px;
+    overflow: hidden;
+    vertical-align: top;
+}
+.card.arvados-object .card-top.green {
+    background-color: #53a93f;
+}
+.card.arvados-object .card-top.blue {
+    background-color: #427fed;
+}
+.card.arvados-object .card-top {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: inline-block;
+    width: 170px;
+    height: 25px;
+    background-color: #ffffff;
+}
+.card.arvados-object .card-info {
+    position: absolute;
+    top: 25px;
+    display: inline-block;
+    width: 100%;
+    height: 101px;
+    overflow: hidden;
+    background: #ffffff;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.card.arvados-object .card-info .title {
+    display: block;
+    margin: 8px 14px 0 14px;
+    overflow: hidden;
+    font-size: 16px;
+    font-weight: bold;
+    line-height: 18px;
+    color: #404040;
+}
+.card.arvados-object .card-info .desc {
+    display: block;
+    margin: 8px 14px 0 14px;
+    overflow: hidden;
+    font-size: 12px;
+    line-height: 16px;
+    color: #737373;
+    text-overflow: ellipsis;
+}
+.card.arvados-object .card-bottom {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    display: inline-block;
+    width: 100%;
+    padding: 10px 20px;
+    line-height: 29px;
+    text-align: center;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
diff --git a/apps/workbench/app/controllers/application_controller.rb b/apps/workbench/app/controllers/application_controller.rb
index 41d5566..2e3a596 100644
--- a/apps/workbench/app/controllers/application_controller.rb
+++ b/apps/workbench/app/controllers/application_controller.rb
@@ -129,7 +129,9 @@ class ApplicationController < ActionController::Base
   end
 
   def create
-    @object ||= model_class.new params[model_class.to_s.underscore.singularize]
+    new_resource_attrs = params[model_class.to_s.underscore.singularize].
+      reject { |k,v| k.to_s == 'uuid' }
+    @object ||= model_class.new new_resource_attrs
     @object.save!
 
     respond_to do |f|
diff --git a/apps/workbench/app/controllers/groups_controller.rb b/apps/workbench/app/controllers/groups_controller.rb
index b360b19..672fe90 100644
--- a/apps/workbench/app/controllers/groups_controller.rb
+++ b/apps/workbench/app/controllers/groups_controller.rb
@@ -5,4 +5,9 @@ class GroupsController < ApplicationController
     @links_from = Link.where link_class: 'permission', tail_uuid: @group_uuids
     @links_to = Link.where link_class: 'permission', head_uuid: @group_uuids
   end
+
+  def show
+    @objects = @object.owned_items include_linked: true
+    super
+  end
 end
diff --git a/apps/workbench/app/helpers/application_helper.rb b/apps/workbench/app/helpers/application_helper.rb
index b172313..d6b258b 100644
--- a/apps/workbench/app/helpers/application_helper.rb
+++ b/apps/workbench/app/helpers/application_helper.rb
@@ -141,16 +141,29 @@ module ApplicationHelper
 
     attrvalue = attrvalue.to_json if attrvalue.is_a? Hash or attrvalue.is_a? Array
 
+    ajax_options = {
+      "data-pk" => {
+        id: object.uuid,
+        key: object.class.to_s.underscore
+      }
+    }
+    if object.uuid
+      ajax_options['data-url'] = url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore)
+    else
+      ajax_options['data-url'] = url_for(action: "create", controller: object.class.to_s.pluralize.underscore)
+      ajax_options['data-pk'][:defaults] = object.attributes
+      ajax_options['data-pk'][:_method] = 'post'
+    end
+    ajax_options['data-pk'] = ajax_options['data-pk'].to_json
+
     link_to attrvalue.to_s, '#', {
       "data-emptytext" => "none",
       "data-placement" => "bottom",
       "data-type" => input_type,
-      "data-url" => url_for(action: "update", id: object.uuid, controller: object.class.to_s.pluralize.underscore),
       "data-title" => "Update #{attr.gsub '_', ' '}",
       "data-name" => attr,
-      "data-pk" => "{id: \"#{object.uuid}\", key: \"#{object.class.to_s.underscore}\"}",
       :class => "editable"
-    }.merge(htmloptions)
+    }.merge(htmloptions).merge(ajax_options)
   end
 
   def render_pipeline_component_attribute(object, attr, subattr, value_info, htmloptions={})
diff --git a/apps/workbench/app/models/arvados_base.rb b/apps/workbench/app/models/arvados_base.rb
index 1cf0d1f..81732ba 100644
--- a/apps/workbench/app/models/arvados_base.rb
+++ b/apps/workbench/app/models/arvados_base.rb
@@ -299,6 +299,10 @@ class ArvadosBase < ActiveRecord::Base
     (name if self.respond_to? :name) || uuid
   end
 
+  def content_summary
+    self.class.to_s
+  end
+
   def selection_label
     friendly_link_name
   end
diff --git a/apps/workbench/app/models/arvados_resource_list.rb b/apps/workbench/app/models/arvados_resource_list.rb
index ba3f0a0..a474b13 100644
--- a/apps/workbench/app/models/arvados_resource_list.rb
+++ b/apps/workbench/app/models/arvados_resource_list.rb
@@ -159,7 +159,7 @@ class ArvadosResourceList
   end
 
   def name_for item_or_uuid
-    links_for(item_or_uuid, 'name').first.name
+    links_for(item_or_uuid, 'name').first.andand.name
   end
 
 end
diff --git a/apps/workbench/app/models/collection.rb b/apps/workbench/app/models/collection.rb
index 5460e9a..a63bf90 100644
--- a/apps/workbench/app/models/collection.rb
+++ b/apps/workbench/app/models/collection.rb
@@ -1,4 +1,5 @@
 class Collection < ArvadosBase
+  include ApplicationHelper
 
   MD5_EMPTY = 'd41d8cd98f00b204e9800998ecf8427e'
 
@@ -7,6 +8,10 @@ class Collection < ArvadosBase
     !!locator.to_s.match("^#{MD5_EMPTY}(\\+.*)?\$")
   end
 
+  def content_summary
+    human_readable_bytes_html(total_bytes) + " " + super
+  end
+
   def total_bytes
     if files
       tot = 0
diff --git a/apps/workbench/app/views/groups/show.html.erb b/apps/workbench/app/views/groups/show.html.erb
index fdb460e..41541f8 100644
--- a/apps/workbench/app/views/groups/show.html.erb
+++ b/apps/workbench/app/views/groups/show.html.erb
@@ -2,91 +2,6 @@
 .arvados-nav-container {
     display:none;
 }
-.card {
-    padding-top: 20px;
-    margin: 10px 0 20px 0;
-    background-color: #ffffff;
-    border: 1px solid #d8d8d8;
-    border-top-width: 0;
-    border-bottom-width: 2px;
-    -webkit-border-radius: 3px;
-    -moz-border-radius: 3px;
-    border-radius: 3px;
-    -webkit-box-shadow: none;
-    -moz-box-shadow: none;
-    box-shadow: none;
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box;
-}
-.card.arvados-object {
-    position: relative;
-    display: inline-block;
-    width: 170px;
-    height: 175px;
-    padding-top: 0;
-    margin-left: 20px;
-    overflow: hidden;
-    vertical-align: top;
-}
-.card.arvados-object .card-top.green {
-    background-color: #53a93f;
-}
-.card.arvados-object .card-top.blue {
-    background-color: #427fed;
-}
-.card.arvados-object .card-top {
-    position: absolute;
-    top: 0;
-    left: 0;
-    display: inline-block;
-    width: 170px;
-    height: 25px;
-    background-color: #ffffff;
-}
-.card.arvados-object .card-info {
-    position: absolute;
-    top: 25px;
-    display: inline-block;
-    width: 100%;
-    height: 101px;
-    overflow: hidden;
-    background: #ffffff;
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box;
-}
-.card.arvados-object .card-info .title {
-    display: block;
-    margin: 8px 14px 0 14px;
-    overflow: hidden;
-    font-size: 16px;
-    font-weight: bold;
-    line-height: 18px;
-    color: #404040;
-}
-.card.arvados-object .card-info .desc {
-    display: block;
-    margin: 8px 14px 0 14px;
-    overflow: hidden;
-    font-size: 12px;
-    line-height: 16px;
-    color: #737373;
-    text-overflow: ellipsis;
-}
-.card.arvados-object .card-bottom {
-    position: absolute;
-    bottom: 0;
-    left: 0;
-    display: inline-block;
-    width: 100%;
-    padding: 10px 20px;
-    line-height: 29px;
-    text-align: center;
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    box-sizing: border-box;
-}
 <% end %>
 
 <% content_for :above_left_nav do %>
@@ -241,74 +156,34 @@
       </div>
       <div class="panel-body">
         <p>
-        </p><table class="table">
+        </p>
+        <table class="table">
           <tbody>
+            <colgroup>
+              <col width="30%" />
+              <col width="20%" />
+              <col width="20%" />
+              <col width="30%" />
+            </colgroup>
+            <% @objects.each do |object| %>
             <tr>
               <td>
-                Some Subfolder
-              </td>
-              <td>
-                12 items
-              </td>
-              <td>
-                2014-04-01
-              </td>
-            </tr>
-            <tr>
-              <td>
-                Test Dataset
+                <% name_link = @objects.links_for(object, 'name').first || Link.new(link_class: "name", owner_uuid: @object.uuid, tail_uuid: @object.uuid, head_uuid: object.uuid, name: "") %>
+                <%= render_editable_attribute name_link, 'name', nil, { 'data-emptytext' => "Unnamed #{object.class}" } %>
               </td>
               <td>
-                4 GiB
+                <%= object.content_summary %>
               </td>
-              <td>
-                2014-04-01
+              <td title="<%= object.modified_at %>">
+                <span>
+                  <%= raw distance_of_time_in_words(object.modified_at, Time.now).sub('about ','~').sub(' ',' ') + ' ago' %>
+                </span>
               </td>
-            </tr>
-            <tr>
-              <td>
-                Test Dataset 2
-              </td>
-              <td>
-                4 GiB
-              </td>
-              <td>
-                2014-04-01
-              </td>
-            </tr>
-            <tr>
-              <td>
-                GATK Exome Pipeline
-              </td>
-              <td>
-                7 components
-              </td>
-              <td>
-                2014-03-21
-              </td>
-            </tr>
-            <tr>
-              <td>
-                Reference result
-              </td>
-              <td>
-                250 MiB
-              </td>
-              <td>
-                2014-03-22
-              </td>
-            </tr>
-            <tr>
-              <td>
-                Some other thing
-              </td>
-              <td>
-                1.2 TiB
-              </td>
-              <td>
-                2014-01-01
+              <td class="arvados-uuid">
+                <%= link_to_if_arvados_object(object, {no_tags: true}) %>
               </td>
             </tr>
+            <% end %>
           </tbody>
           <thead>
             <tr>
@@ -320,6 +195,9 @@
               <th>
                 Modified
               </th>
+              <th>
+                UUID
+              </th>
             </tr>
           </thead>
         </table>

commit 7e8f99556391cc81c014b517a9fa6efed8fe8113
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Apr 28 13:01:18 2014 -0400

    api: Support filters in API client auths index.
    
    Per comments on Refs #1904.  filters is generally the preferred way to
    do searching now.  I maintained existing limits on what can be
    searched with this method.

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 9674bb7..73d1405 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -208,6 +208,10 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  def default_orders
+    ["#{table_name}.modified_at desc"]
+  end
+
   def load_limit_offset_order_params
     if params[:limit]
       unless params[:limit].to_s.match(/^\d+$/)
@@ -240,7 +244,7 @@ class ApplicationController < ActionController::Base
       end
     end
     if @orders.empty?
-      @orders << "#{table_name}.modified_at desc"
+      @orders = default_orders
     end
   end
 
diff --git a/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb b/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
index ff322a7..dc95b2f 100644
--- a/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
+++ b/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
@@ -34,21 +34,34 @@ class Arvados::V1::ApiClientAuthorizationsController < ApplicationController
 
   protected
 
+  def default_orders
+    ["#{table_name}.created_at desc"]
+  end
+
   def find_objects_for_index
     # Here we are deliberately less helpful about searching for client
-    # authorizations. Rather than use the generic index/where/order
-    # features, we look up tokens belonging to the current user and
-    # filter by exact match on api_token (which we expect in the form
-    # of a where[uuid] parameter to make things easier for API client
-    # libraries).
+    # authorizations.  We look up tokens belonging to the current user
+    # and filter by exact matches on api_token and scopes.
+    wanted_scopes = []
+    if @filters
+      wanted_scopes.concat(@filters.map { |attr, operator, operand|
+        ((attr == 'scopes') and (operator == '=')) ? operand : nil
+      })
+      @filters.select! { |attr, operator, operand|
+        (attr == 'uuid') and (operator == '=')
+      }
+    end
+    if @where
+      wanted_scopes << @where['scopes']
+      @where.select! { |attr, val| attr == 'uuid' }
+    end
     @objects = model_class.
       includes(:user, :api_client).
-      where('user_id=? and (? or api_token=?)', current_user.id, !@where['uuid'], @where['uuid']).
-      order('created_at desc')
-    unless @where['scopes'].nil?
-      @objects = @objects.select { |auth|
-        (auth.scopes & @where['scopes']) == (auth.scopes | @where['scopes'])
-      }
+      where('user_id=?', current_user.id)
+    super
+    wanted_scopes.compact.each do |scope_list|
+      sorted_scopes = scope_list.sort
+      @objects = @objects.select { |auth| auth.scopes.sort == sorted_scopes }
     end
   end
 
diff --git a/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb b/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
index 0072792..8877719 100644
--- a/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
@@ -37,22 +37,33 @@ class Arvados::V1::ApiClientAuthorizationsControllerTest < ActionController::Tes
     assert_response 403
   end
 
-  test "admin search filters where scopes exactly match" do
-    def check_tokens_by_scopes(scopes, *expected_tokens)
-      expected_tokens.map! { |name| api_client_authorizations(name).api_token }
-      get :index, where: {scopes: scopes}
-      assert_response :success
-      got_tokens = JSON.parse(@response.body)['items']
-        .map { |auth| auth['api_token'] }
-      assert_equal(expected_tokens.sort, got_tokens.sort,
-                   "wrong results for scopes = #{scopes}")
+  def assert_found_tokens(auth, search_params, *expected_tokens)
+    authorize_with auth
+    expected_tokens.map! { |name| api_client_authorizations(name).api_token }
+    get :index, search_params
+    assert_response :success
+    got_tokens = JSON.parse(@response.body)['items']
+      .map { |auth| auth['api_token'] }
+    assert_equal(expected_tokens.sort, got_tokens.sort,
+                 "wrong results for #{search_params.inspect}")
+  end
+
+  # Three-tuples with auth to use, scopes to find, and expected tokens.
+  # Make two tests for each tuple, one searching with where and the other
+  # with filter.
+  [[:admin_trustedclient, [], :admin_noscope],
+   [:active_trustedclient, ["GET /arvados/v1/users"], :active_userlist],
+   [:active_trustedclient,
+    ["POST /arvados/v1/api_client_authorizations",
+     "GET /arvados/v1/api_client_authorizations"],
+    :active_apitokens],
+  ].each do |auth, scopes, *expected|
+    test "#{auth.to_s} can find auths where scopes=#{scopes.inspect}" do
+      assert_found_tokens(auth, {where: {scopes: scopes}}, *expected)
+    end
+
+    test "#{auth.to_s} can find auths filtered with scopes=#{scopes.inspect}" do
+      assert_found_tokens(auth, {filters: [['scopes', '=', scopes]]}, *expected)
     end
-    authorize_with :admin_trustedclient
-    check_tokens_by_scopes([], :admin_noscope)
-    authorize_with :active_trustedclient
-    check_tokens_by_scopes(["GET /arvados/v1/users"], :active_userlist)
-    check_tokens_by_scopes(["POST /arvados/v1/api_client_authorizations",
-                            "GET /arvados/v1/api_client_authorizations"],
-                           :active_apitokens)
   end
 end

commit 2f3e496712802324e5d184f9ae59866df1772ef0
Author: Brett Smith <brett at curoverse.com>
Date:   Wed Apr 23 16:15:37 2014 -0400

    api: Support scope searching in API token index.

diff --git a/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb b/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
index 8fd915d..ff322a7 100644
--- a/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
+++ b/services/api/app/controllers/arvados/v1/api_client_authorizations_controller.rb
@@ -45,6 +45,11 @@ class Arvados::V1::ApiClientAuthorizationsController < ApplicationController
       includes(:user, :api_client).
       where('user_id=? and (? or api_token=?)', current_user.id, !@where['uuid'], @where['uuid']).
       order('created_at desc')
+    unless @where['scopes'].nil?
+      @objects = @objects.select { |auth|
+        (auth.scopes & @where['scopes']) == (auth.scopes | @where['scopes'])
+      }
+    end
   end
 
   def find_object_by_uuid
diff --git a/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb b/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
index cbb0096..0072792 100644
--- a/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/api_client_authorizations_controller_test.rb
@@ -1,7 +1,6 @@
 require 'test_helper'
 
 class Arvados::V1::ApiClientAuthorizationsControllerTest < ActionController::TestCase
-
   test "should get index" do
     authorize_with :active_trustedclient
     get :index
@@ -38,4 +37,22 @@ class Arvados::V1::ApiClientAuthorizationsControllerTest < ActionController::Tes
     assert_response 403
   end
 
+  test "admin search filters where scopes exactly match" do
+    def check_tokens_by_scopes(scopes, *expected_tokens)
+      expected_tokens.map! { |name| api_client_authorizations(name).api_token }
+      get :index, where: {scopes: scopes}
+      assert_response :success
+      got_tokens = JSON.parse(@response.body)['items']
+        .map { |auth| auth['api_token'] }
+      assert_equal(expected_tokens.sort, got_tokens.sort,
+                   "wrong results for scopes = #{scopes}")
+    end
+    authorize_with :admin_trustedclient
+    check_tokens_by_scopes([], :admin_noscope)
+    authorize_with :active_trustedclient
+    check_tokens_by_scopes(["GET /arvados/v1/users"], :active_userlist)
+    check_tokens_by_scopes(["POST /arvados/v1/api_client_authorizations",
+                            "GET /arvados/v1/api_client_authorizations"],
+                           :active_apitokens)
+  end
 end

commit bf15373590e21dafd696fa0c10906eb653610d1d
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Apr 28 14:01:53 2014 -0400

    api: Migrate VM auth scopes to new system.
    
    VirtualMachinesController was the only one doing anything special with
    API token scopes before we provided the more general-purpose system.
    This commit removes its specialized code, and provides a database
    migration to convert those specialized scopes to the general-purpose
    schema.

diff --git a/services/api/app/controllers/arvados/v1/virtual_machines_controller.rb b/services/api/app/controllers/arvados/v1/virtual_machines_controller.rb
index 10b4bd8..e176348 100644
--- a/services/api/app/controllers/arvados/v1/virtual_machines_controller.rb
+++ b/services/api/app/controllers/arvados/v1/virtual_machines_controller.rb
@@ -1,12 +1,8 @@
 class Arvados::V1::VirtualMachinesController < ApplicationController
   skip_before_filter :find_object_by_uuid, :only => :get_all_logins
   skip_before_filter :render_404_if_no_object, :only => :get_all_logins
-  skip_before_filter(:require_auth_scope_all,
-                     :only => [:logins, :get_all_logins])
   before_filter(:admin_required,
                 :only => [:logins, :get_all_logins])
-  before_filter(:require_auth_scope_for_get_all_logins,
-                :only => [:logins, :get_all_logins])
 
   def logins
     get_all_logins
@@ -44,16 +40,4 @@ class Arvados::V1::VirtualMachinesController < ApplicationController
     end
     render json: { kind: "arvados#HashList", items: @response }
   end
-
-  protected
-
-  def require_auth_scope_for_get_all_logins
-    if @object
-      # Client wants all logins for a single VM.
-      require_auth_scope(['all', arvados_v1_virtual_machine_url(@object.uuid)])
-    else
-      # ...for a non-existent VM, or all VMs.
-      require_auth_scope(['all'])
-    end
-  end
 end
diff --git a/services/api/db/migrate/20140423133559_new_scope_format.rb b/services/api/db/migrate/20140423133559_new_scope_format.rb
new file mode 100644
index 0000000..5b69e95
--- /dev/null
+++ b/services/api/db/migrate/20140423133559_new_scope_format.rb
@@ -0,0 +1,48 @@
+# At the time we introduced scopes everywhere, VirtualMachinesController
+# recognized scopes that gave the URL for a VM to grant access to that VM's
+# login list.  This migration converts those VM-specific scopes to the new
+# general format, and back.
+
+class NewScopeFormat < ActiveRecord::Migration
+  include CurrentApiClient
+
+  VM_PATH_REGEX =
+    %r{(/arvados/v1/virtual_machines/[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15})}
+  OLD_SCOPE_REGEX = %r{^https?://[^/]+#{VM_PATH_REGEX.source}$}
+  NEW_SCOPE_REGEX = %r{^GET #{VM_PATH_REGEX.source}/logins$}
+
+  def fix_scopes_matching(regex)
+    act_as_system_user
+    ApiClientAuthorization.find_each do |auth|
+      auth.scopes = auth.scopes.map do |scope|
+        if match = regex.match(scope)
+          yield match
+        else
+          scope
+        end
+      end
+      auth.save!
+    end
+  end
+
+  def up
+    fix_scopes_matching(OLD_SCOPE_REGEX) do |match|
+      "GET #{match[1]}/logins"
+    end
+  end
+
+  def down
+    case Rails.env
+    when 'test'
+      hostname = 'www.example.com'
+    else
+      require 'socket'
+      hostname = Socket.gethostname
+    end
+    fix_scopes_matching(NEW_SCOPE_REGEX) do |match|
+      Rails.application.routes.url_for(controller: 'virtual_machines',
+                                       uuid: match[1].split('/').last,
+                                       host: hostname, protocol: 'https')
+    end
+  end
+end
diff --git a/services/api/db/schema.rb b/services/api/db/schema.rb
index 988cb87..034ee35 100644
--- a/services/api/db/schema.rb
+++ b/services/api/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20140422011506) do
+ActiveRecord::Schema.define(:version => 20140423133559) do
 
   create_table "api_client_authorizations", :force => true do |t|
     t.string   "api_token",                                           :null => false
diff --git a/services/api/test/fixtures/api_client_authorizations.yml b/services/api/test/fixtures/api_client_authorizations.yml
index 77e5048..f772c4f 100644
--- a/services/api/test/fixtures/api_client_authorizations.yml
+++ b/services/api/test/fixtures/api_client_authorizations.yml
@@ -42,7 +42,7 @@ admin_vm:
   api_token: adminvirtualmachineabcdefghijklmnopqrstuvwxyz12345
   expires_at: 2038-01-01 00:00:00
   # scope refers to the testvm fixture.
-  scopes: ["https://www.example.com/arvados/v1/virtual_machines/zzzzz-2x53u-382brsig8rp3064"]
+  scopes: ["GET /arvados/v1/virtual_machines/zzzzz-2x53u-382brsig8rp3064/logins"]
 
 admin_noscope:
   api_client: untrusted

commit 8086f73aca674d7533e88bdd3850042553487d2b
Author: Brett Smith <brett at curoverse.com>
Date:   Tue Apr 22 17:45:46 2014 -0400

    api: Introduce path-based API token scopes.
    
    Refs #1904, #2662 for background discussion.

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 0f144d8..9674bb7 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -7,7 +7,7 @@ class ApplicationController < ActionController::Base
   around_filter :thread_with_auth_info, :except => [:render_error, :render_not_found]
 
   before_filter :remote_ip
-  before_filter :require_auth_scope_all, :except => :render_not_found
+  before_filter :require_auth_scope, :except => :render_not_found
   before_filter :catch_redirect_hint
 
   before_filter :find_object_by_uuid, :except => [:index, :create,
@@ -406,12 +406,9 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def require_auth_scope_all
-    require_login and require_auth_scope(['all'])
-  end
-
-  def require_auth_scope(ok_scopes)
-    unless current_api_client_auth_has_scope(ok_scopes)
+  def require_auth_scope
+    return false unless require_login
+    unless current_api_client_auth_has_scope("#{request.method} #{request.path}")
       render :json => { errors: ['Forbidden'] }.to_json, status: 403
     end
   end
diff --git a/services/api/app/controllers/arvados/v1/keep_disks_controller.rb b/services/api/app/controllers/arvados/v1/keep_disks_controller.rb
index 3d91916..47018d4 100644
--- a/services/api/app/controllers/arvados/v1/keep_disks_controller.rb
+++ b/services/api/app/controllers/arvados/v1/keep_disks_controller.rb
@@ -1,5 +1,5 @@
 class Arvados::V1::KeepDisksController < ApplicationController
-  skip_before_filter :require_auth_scope_all, :only => :ping
+  skip_before_filter :require_auth_scope, :only => :ping
 
   def self._ping_requires_parameters
     {
diff --git a/services/api/app/controllers/arvados/v1/nodes_controller.rb b/services/api/app/controllers/arvados/v1/nodes_controller.rb
index eda8b07..d7a477d 100644
--- a/services/api/app/controllers/arvados/v1/nodes_controller.rb
+++ b/services/api/app/controllers/arvados/v1/nodes_controller.rb
@@ -1,5 +1,5 @@
 class Arvados::V1::NodesController < ApplicationController
-  skip_before_filter :require_auth_scope_all, :only => :ping
+  skip_before_filter :require_auth_scope, :only => :ping
   skip_before_filter :find_object_by_uuid, :only => :ping
   skip_before_filter :render_404_if_no_object, :only => :ping
 
diff --git a/services/api/app/controllers/arvados/v1/schema_controller.rb b/services/api/app/controllers/arvados/v1/schema_controller.rb
index 1cc8496..625519e 100644
--- a/services/api/app/controllers/arvados/v1/schema_controller.rb
+++ b/services/api/app/controllers/arvados/v1/schema_controller.rb
@@ -2,7 +2,7 @@ class Arvados::V1::SchemaController < ApplicationController
   skip_before_filter :find_objects_for_index
   skip_before_filter :find_object_by_uuid
   skip_before_filter :render_404_if_no_object
-  skip_before_filter :require_auth_scope_all
+  skip_before_filter :require_auth_scope
 
   def index
     expires_in 24.hours, public: true
@@ -69,7 +69,7 @@ class Arvados::V1::SchemaController < ApplicationController
         schemas: {},
         resources: {}
       }
-      
+
       ActiveRecord::Base.descendants.reject(&:abstract_class?).each do |k|
         begin
           ctl_class = "Arvados::V1::#{k.to_s.pluralize}Controller".constantize
@@ -175,7 +175,7 @@ class Arvados::V1::SchemaController < ApplicationController
               description:
                  %|List #{k.to_s.pluralize}.
 
-                   The <code>list</code> method returns a 
+                   The <code>list</code> method returns a
                    <a href="/api/resources.html">resource list</a> of
                    matching #{k.to_s.pluralize}. For example:
 
diff --git a/services/api/app/controllers/static_controller.rb b/services/api/app/controllers/static_controller.rb
index fda0880..c71b850 100644
--- a/services/api/app/controllers/static_controller.rb
+++ b/services/api/app/controllers/static_controller.rb
@@ -3,7 +3,7 @@ class StaticController < ApplicationController
 
   skip_before_filter :find_object_by_uuid
   skip_before_filter :render_404_if_no_object
-  skip_before_filter :require_auth_scope_all, :only => [ :home, :login_failure ]
+  skip_before_filter :require_auth_scope, :only => [ :home, :login_failure ]
 
   def home
     if Rails.configuration.respond_to? :workbench_address
diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb
index a7391bd..3d4b05a 100644
--- a/services/api/app/controllers/user_sessions_controller.rb
+++ b/services/api/app/controllers/user_sessions_controller.rb
@@ -1,5 +1,5 @@
 class UserSessionsController < ApplicationController
-  before_filter :require_auth_scope_all, :only => [ :destroy ]
+  before_filter :require_auth_scope, :only => [ :destroy ]
 
   skip_before_filter :find_object_by_uuid
   skip_before_filter :render_404_if_no_object
diff --git a/services/api/lib/current_api_client.rb b/services/api/lib/current_api_client.rb
index bbba4dc..0803d54 100644
--- a/services/api/lib/current_api_client.rb
+++ b/services/api/lib/current_api_client.rb
@@ -29,14 +29,17 @@ module CurrentApiClient
     Thread.current[:api_client_ip_address]
   end
 
-  # Does the current API client authorization include any of ok_scopes?
-  def current_api_client_auth_has_scope(ok_scopes)
-    auth_scopes = current_api_client_authorization.andand.scopes || []
-    unless auth_scopes.index('all') or (auth_scopes & ok_scopes).any?
-      logger.warn "Insufficient auth scope: need #{ok_scopes}, #{current_api_client_authorization.inspect} has #{auth_scopes}"
-      return false
-    end
-    true
+  # Is the current API client authorization scoped for the request?
+  def current_api_client_auth_has_scope(req_s)
+    (current_api_client_authorization.andand.scopes || []).select { |scope|
+      if scope == 'all'
+        true
+      elsif scope.end_with? '/'
+        req_s.start_with? scope
+      else
+        req_s == scope
+      end
+    }.any?
   end
 
   def system_user_uuid
diff --git a/services/api/test/fixtures/api_client_authorizations.yml b/services/api/test/fixtures/api_client_authorizations.yml
index 5a715e3..77e5048 100644
--- a/services/api/test/fixtures/api_client_authorizations.yml
+++ b/services/api/test/fixtures/api_client_authorizations.yml
@@ -51,6 +51,28 @@ admin_noscope:
   expires_at: 2038-01-01 00:00:00
   scopes: []
 
+active_userlist:
+  api_client: untrusted
+  user: active
+  api_token: activeuserlistabcdefghijklmnopqrstuvwxyz1234568900
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /arvados/v1/users"]
+
+active_specimens:
+  api_client: untrusted
+  user: active
+  api_token: activespecimensabcdefghijklmnopqrstuvwxyz123456890
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /arvados/v1/specimens/"]
+
+active_apitokens:
+  api_client: trusted_workbench
+  user: active
+  api_token: activeapitokensabcdefghijklmnopqrstuvwxyz123456789
+  expires_at: 2038-01-01 00:00:00
+  scopes: ["GET /arvados/v1/api_client_authorizations",
+           "POST /arvados/v1/api_client_authorizations"]
+
 spectator:
   api_client: untrusted
   user: spectator
diff --git a/services/api/test/integration/api_client_authorizations_scopes_test.rb b/services/api/test/integration/api_client_authorizations_scopes_test.rb
index 7269d38..ba91670 100644
--- a/services/api/test/integration/api_client_authorizations_scopes_test.rb
+++ b/services/api/test/integration/api_client_authorizations_scopes_test.rb
@@ -20,7 +20,6 @@ class Arvados::V1::ApiTokensScopeTest < ActionController::IntegrationTest
   end
 
   def request_with_auth(method, path, params={})
-    https!
     send(method, path, @token.merge(params))
   end
 
@@ -32,6 +31,51 @@ class Arvados::V1::ApiTokensScopeTest < ActionController::IntegrationTest
     request_with_auth(:post_via_redirect, *args)
   end
 
+  test "user list token can only list users" do
+    auth_with :active_userlist
+    get_with_auth v1_url('users')
+    assert_response :success
+    get_with_auth v1_url('users', '')  # Add trailing slash.
+    assert_response :success
+    get_with_auth v1_url('users', 'current')
+    assert_response 403
+    get_with_auth v1_url('virtual_machines')
+    assert_response 403
+  end
+
+  test "specimens token can see exactly owned specimens" do
+    auth_with :active_specimens
+    get_with_auth v1_url('specimens')
+    assert_response 403
+    get_with_auth v1_url('specimens', specimens(:owned_by_active_user).uuid)
+    assert_response :success
+    get_with_auth v1_url('specimens', specimens(:owned_by_spectator).uuid)
+    assert_includes(403..404, @response.status)
+  end
+
+  test "token with multiple scopes can use them all" do
+    def get_token_count
+      get_with_auth v1_url('api_client_authorizations')
+      assert_response :success
+      token_count = JSON.parse(@response.body)['items_available']
+      assert_not_nil(token_count, "could not find token count")
+      token_count
+    end
+    auth_with :active_apitokens
+    # Test the GET scope.
+    token_count = get_token_count
+    # Test the POST scope.
+    post_with_auth(v1_url('api_client_authorizations'),
+                   api_client_authorization: {user_id: users(:active).id})
+    assert_response :success
+    assert_equal(token_count + 1, get_token_count,
+                 "token count suggests POST was not accepted")
+    # Test other requests are denied.
+    get_with_auth v1_url('api_client_authorizations',
+                         api_client_authorizations(:active_apitokens).uuid)
+    assert_response 403
+  end
+
   test "token without scope has no access" do
     # Logs are good for this test, because logs have relatively
     # few access controls enforced at the model level.

commit 8b77f66275fd87f70dd79075a71d8062311541bc
Author: Brett Smith <brett at curoverse.com>
Date:   Tue Apr 22 14:44:34 2014 -0400

    api: Test VM login scopes.
    
    The virtual machine controller is the only one doing anything
    interesting with API token scopes right now.  I'm writing this test
    for that functionality to make sure it stays effective through
    refactoring.

diff --git a/services/api/test/fixtures/api_client_authorizations.yml b/services/api/test/fixtures/api_client_authorizations.yml
index 5cada90..5a715e3 100644
--- a/services/api/test/fixtures/api_client_authorizations.yml
+++ b/services/api/test/fixtures/api_client_authorizations.yml
@@ -36,6 +36,21 @@ active_trustedclient:
   api_token: 27bnddk6x2nmq00a1e3gq43n9tsl5v87a3faqar2ijj8tud5en
   expires_at: 2038-01-01 00:00:00
 
+admin_vm:
+  api_client: untrusted
+  user: admin
+  api_token: adminvirtualmachineabcdefghijklmnopqrstuvwxyz12345
+  expires_at: 2038-01-01 00:00:00
+  # scope refers to the testvm fixture.
+  scopes: ["https://www.example.com/arvados/v1/virtual_machines/zzzzz-2x53u-382brsig8rp3064"]
+
+admin_noscope:
+  api_client: untrusted
+  user: admin
+  api_token: adminnoscopeabcdefghijklmnopqrstuvwxyz123456789012
+  expires_at: 2038-01-01 00:00:00
+  scopes: []
+
 spectator:
   api_client: untrusted
   user: spectator
diff --git a/services/api/test/integration/api_client_authorizations_scopes_test.rb b/services/api/test/integration/api_client_authorizations_scopes_test.rb
new file mode 100644
index 0000000..7269d38
--- /dev/null
+++ b/services/api/test/integration/api_client_authorizations_scopes_test.rb
@@ -0,0 +1,59 @@
+# The v1 API uses token scopes to control access to the REST API at the path
+# level.  This is enforced in the base ApplicationController, making it a
+# functional test that we can run against many different controllers.
+
+require 'test_helper'
+
+class Arvados::V1::ApiTokensScopeTest < ActionController::IntegrationTest
+  fixtures :all
+
+  def setup
+    @token = {}
+  end
+
+  def auth_with(name)
+    @token = {api_token: api_client_authorizations(name).api_token}
+  end
+
+  def v1_url(*parts)
+    (['arvados', 'v1'] + parts).join('/')
+  end
+
+  def request_with_auth(method, path, params={})
+    https!
+    send(method, path, @token.merge(params))
+  end
+
+  def get_with_auth(*args)
+    request_with_auth(:get_via_redirect, *args)
+  end
+
+  def post_with_auth(*args)
+    request_with_auth(:post_via_redirect, *args)
+  end
+
+  test "token without scope has no access" do
+    # Logs are good for this test, because logs have relatively
+    # few access controls enforced at the model level.
+    auth_with :admin_noscope
+    get_with_auth v1_url('logs')
+    assert_response 403
+    get_with_auth v1_url('logs', logs(:log1).uuid)
+    assert_response 403
+    post_with_auth(v1_url('logs'), log: {})
+    assert_response 403
+  end
+
+  test "VM login scopes work" do
+    # A system administration script makes an API token with limited scope
+    # for virtual machines to let it see logins.
+    def vm_logins_url(name)
+      v1_url('virtual_machines', virtual_machines(name).uuid, 'logins')
+    end
+    auth_with :admin_vm
+    get_with_auth vm_logins_url(:testvm)
+    assert_response :success
+    get_with_auth vm_logins_url(:testvm2)
+    assert(@response.status >= 400, "getting testvm2 logins should have failed")
+  end
+end

commit 57dc9e64bb38f186e2b235a98d7437a5f986bc83
Author: Brett Smith <brett at curoverse.com>
Date:   Mon Apr 28 11:03:22 2014 -0400

    api: Shorten name of authorized_keys index.
    
    I had trouble running the new TimestampsNotNull migration, because I
    ran into the index name limit described in the migration comments.
    Running this migration first worked around the problem for me, and I
    hope it saves others from tripping over it too.

diff --git a/services/api/db/migrate/20140421151939_rename_auth_keys_user_index.rb b/services/api/db/migrate/20140421151939_rename_auth_keys_user_index.rb
new file mode 100644
index 0000000..2b057f0
--- /dev/null
+++ b/services/api/db/migrate/20140421151939_rename_auth_keys_user_index.rb
@@ -0,0 +1,11 @@
+class RenameAuthKeysUserIndex < ActiveRecord::Migration
+  # Rails' default name for this index is so long, Rails can't modify
+  # the index later, because the autogenerated temporary name exceeds
+  # PostgreSQL's 64-character limit.  This migration gives the index
+  # an explicit name to work around that issue.
+  def change
+    rename_index("authorized_keys",
+                 "index_authorized_keys_on_authorized_user_uuid_and_expires_at",
+                 "index_authkeys_on_user_and_expires_at")
+  end
+end
diff --git a/services/api/db/schema.rb b/services/api/db/schema.rb
index af751fa..988cb87 100644
--- a/services/api/db/schema.rb
+++ b/services/api/db/schema.rb
@@ -64,7 +64,7 @@ ActiveRecord::Schema.define(:version => 20140422011506) do
     t.datetime "updated_at",              :null => false
   end
 
-  add_index "authorized_keys", ["authorized_user_uuid", "expires_at"], :name => "index_authorized_keys_on_authorized_user_uuid_and_expires_at"
+  add_index "authorized_keys", ["authorized_user_uuid", "expires_at"], :name => "index_authkeys_on_user_and_expires_at"
   add_index "authorized_keys", ["uuid"], :name => "index_authorized_keys_on_uuid", :unique => true
 
   create_table "collections", :force => true do |t|

commit d7ccebe29c68df51633f6a18eba6aa6a982c3739
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 14 11:44:23 2014 -0400

    Render group with folder view

diff --git a/apps/workbench/app/views/groups/show.html.erb b/apps/workbench/app/views/groups/show.html.erb
new file mode 100644
index 0000000..fdb460e
--- /dev/null
+++ b/apps/workbench/app/views/groups/show.html.erb
@@ -0,0 +1,330 @@
+<% content_for :css do %>
+.arvados-nav-container {
+    display:none;
+}
+.card {
+    padding-top: 20px;
+    margin: 10px 0 20px 0;
+    background-color: #ffffff;
+    border: 1px solid #d8d8d8;
+    border-top-width: 0;
+    border-bottom-width: 2px;
+    -webkit-border-radius: 3px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    -webkit-box-shadow: none;
+    -moz-box-shadow: none;
+    box-shadow: none;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.card.arvados-object {
+    position: relative;
+    display: inline-block;
+    width: 170px;
+    height: 175px;
+    padding-top: 0;
+    margin-left: 20px;
+    overflow: hidden;
+    vertical-align: top;
+}
+.card.arvados-object .card-top.green {
+    background-color: #53a93f;
+}
+.card.arvados-object .card-top.blue {
+    background-color: #427fed;
+}
+.card.arvados-object .card-top {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: inline-block;
+    width: 170px;
+    height: 25px;
+    background-color: #ffffff;
+}
+.card.arvados-object .card-info {
+    position: absolute;
+    top: 25px;
+    display: inline-block;
+    width: 100%;
+    height: 101px;
+    overflow: hidden;
+    background: #ffffff;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+.card.arvados-object .card-info .title {
+    display: block;
+    margin: 8px 14px 0 14px;
+    overflow: hidden;
+    font-size: 16px;
+    font-weight: bold;
+    line-height: 18px;
+    color: #404040;
+}
+.card.arvados-object .card-info .desc {
+    display: block;
+    margin: 8px 14px 0 14px;
+    overflow: hidden;
+    font-size: 12px;
+    line-height: 16px;
+    color: #737373;
+    text-overflow: ellipsis;
+}
+.card.arvados-object .card-bottom {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    display: inline-block;
+    width: 100%;
+    padding: 10px 20px;
+    line-height: 29px;
+    text-align: center;
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+<% end %>
+
+<% content_for :above_left_nav do %>
+<div class="panel panel-info">
+  <div class="panel-heading">
+    <a class="btn btn-xs btn-info pull-right">
+      Rename
+    </a>
+    <h3 class="panel-title">
+      <%= @object.name %>
+    </h3>
+  </div>
+  <div class="panel-body">
+    <img src="/favicon.ico" class="pull-right" alt=""/>
+    <p>
+      This folder was created <%= @object.created_at %>. (This
+      description defaults to something generic.)
+    </p>
+    <a href="#" class="btn btn-xs btn-info">Edit description</a>
+  </div>
+</div>
+
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <h3 class="panel-title">
+      Activity
+    </h3>
+  </div>
+  <div class="panel-body">
+    <input type="text" class="form-control" placeholder="Search"/>
+    <div style="height:0.5em;"></div>
+    <p>
+      11:12 - Some Subfolder added
+    </p>
+    <p>
+      10:06 - <%= @object.name %> - renamed from OldFolderName to <%= @object.name %>
+    </p>
+    <p>
+      10:01 - Test Dataset (4 GiB collection) added to <%= @object.name %> by <%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %>
+    </p>
+  </div>
+</div>
+
+<div class="panel panel-default">
+  <div class="panel-heading">
+    <h3 class="panel-title">
+      Sharing and permissions
+    </h3>
+  </div>
+  <div class="panel-body">
+    <input type="text" class="form-control" placeholder="Search"/>
+    <div style="height:0.5em;"></div>
+    <table class="table table-condensed">
+      <tbody>
+        <tr>
+          <td><%= link_to_if_arvados_object @object.owner_uuid, friendly_name: true %></td>
+          <td>Owner</td>
+        </tr>
+        <tr>
+          <td>Someone Else</td>
+          <td>read only</td>
+        </tr>
+        <tr>
+          <td>Someone Else</td>
+          <td>read+write</td>
+        </tr>
+      </tbody>
+      <thead><tr><th>User</th><th>Role</th></tr></thead>
+    </table>
+  </div>
+</div>
+<% end %>
+
+<div class="row">
+  <div class="card arvados-object">
+    <div class="card-top green">
+      <a href="#">
+        <img src="/favicon.ico" alt=""/>
+      </a>
+    </div>
+    <div class="card-info">
+      <a class="title" href="#">Test dataset</a>
+      <div class="desc">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div>
+    </div>
+    <div class="card-bottom">
+      <button class="btn btn-default btn-block">Show details</button>
+    </div>
+  </div>
+
+  <div class="card arvados-object">
+    <div class="card-top green">
+      <a href="#">
+        <img src="/favicon.ico" alt=""/>
+      </a>
+    </div>
+    <div class="card-info">
+      <a class="title" href="#">GATK Exome Pipeline</a>
+      <div class="desc">exome p.e. fastq -> vcf</div>
+    </div>
+    <div class="card-bottom">
+      <button class="btn btn-default btn-block">Show details</button>
+    </div>
+  </div>
+
+  <div class="card arvados-object">
+    <div class="card-top blue">
+      <a href="#">
+        <img src="/favicon.ico" alt=""/>
+      </a>
+    </div>
+    <div class="card-info">
+      <a class="title" href="#">Reference result</a>
+      <div class="desc">Known good -- checked concordance against other datasets</div>
+    </div>
+    <div class="card-bottom">
+      <button class="btn btn-default btn-block">Show details</button>
+    </div>
+  </div>
+
+  <div class="card arvados-object">
+    <div class="card-top blue">
+      <a href="#">
+        <img src="/favicon.ico" alt=""/>
+      </a>
+    </div>
+    <div class="card-info">
+      <a class="title" href="#">Some other thing</a>
+      <div class="desc">(Objects that are "starred" appear in this area)</div>
+    </div>
+    <div class="card-bottom">
+      <button class="btn btn-default btn-block">Show details</button>
+    </div>
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-12">
+    <div class="panel panel-info">
+      <div class="panel-heading">
+        <div class="row">
+          <div class="col-md-6">
+            <h3 class="panel-title" style="vertical-align:middle;">
+              Contents
+            </h3>
+          </div>
+          <div class="col-md-6">
+            <div class="input-group input-group-sm pull-right">
+              <input type="text" class="form-control" placeholder="Search folder contents"/>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="panel-body">
+        <p>
+        </p><table class="table">
+          <tbody>
+            <tr>
+              <td>
+                Some Subfolder
+              </td>
+              <td>
+                12 items
+              </td>
+              <td>
+                2014-04-01
+              </td>
+            </tr>
+            <tr>
+              <td>
+                Test Dataset
+              </td>
+              <td>
+                4 GiB
+              </td>
+              <td>
+                2014-04-01
+              </td>
+            </tr>
+            <tr>
+              <td>
+                Test Dataset 2
+              </td>
+              <td>
+                4 GiB
+              </td>
+              <td>
+                2014-04-01
+              </td>
+            </tr>
+            <tr>
+              <td>
+                GATK Exome Pipeline
+              </td>
+              <td>
+                7 components
+              </td>
+              <td>
+                2014-03-21
+              </td>
+            </tr>
+            <tr>
+              <td>
+                Reference result
+              </td>
+              <td>
+                250 MiB
+              </td>
+              <td>
+                2014-03-22
+              </td>
+            </tr>
+            <tr>
+              <td>
+                Some other thing
+              </td>
+              <td>
+                1.2 TiB
+              </td>
+              <td>
+                2014-01-01
+              </td>
+            </tr>
+          </tbody>
+          <thead>
+            <tr>
+              <th>
+                Name
+              </th>
+              <th>
+              </th>
+              <th>
+                Modified
+              </th>
+            </tr>
+          </thead>
+        </table>
+        <p></p>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/apps/workbench/app/views/layouts/application.html.erb b/apps/workbench/app/views/layouts/application.html.erb
index 9da171e..ece2f2e 100644
--- a/apps/workbench/app/views/layouts/application.html.erb
+++ b/apps/workbench/app/views/layouts/application.html.erb
@@ -178,6 +178,7 @@
         </div>
       </div>
       <div class="col-sm-3 left-nav">
+        <%= yield :above_left_nav %>
         <div class="arvados-nav-container">
         <% if current_user.andand.is_active %>
         <div class="well">

commit d649a716392760cd394e18a628dc23aaec5fa3b3
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 28 20:10:17 2014 -0400

    Fix patch_paging_vars args.

diff --git a/apps/workbench/app/controllers/api_client_authorizations_controller.rb b/apps/workbench/app/controllers/api_client_authorizations_controller.rb
index 24b4ae3..8385b6b 100644
--- a/apps/workbench/app/controllers/api_client_authorizations_controller.rb
+++ b/apps/workbench/app/controllers/api_client_authorizations_controller.rb
@@ -7,7 +7,7 @@ class ApiClientAuthorizationsController < ApplicationController
     filtered = m.to_ary.reject do |x|
       x.api_client_id == 0 or (x.expires_at and x.expires_at < Time.now) rescue false
     end
-    ArvadosApiClient::patch_paging_vars(filtered, items_available, offset, limit)
+    ArvadosApiClient::patch_paging_vars(filtered, items_available, offset, limit, nil)
     @objects = ArvadosResourceList.new(ApiClientAuthorization)
     @objects.results= filtered
     super
diff --git a/apps/workbench/app/models/arvados_api_client.rb b/apps/workbench/app/models/arvados_api_client.rb
index bc07b88..d6e385f 100644
--- a/apps/workbench/app/models/arvados_api_client.rb
+++ b/apps/workbench/app/models/arvados_api_client.rb
@@ -90,7 +90,7 @@ class ArvadosApiClient
     resp
   end
 
-  def self.patch_paging_vars(ary, items_available, offset, limit, links)
+  def self.patch_paging_vars(ary, items_available, offset, limit, links=nil)
     if items_available
       (class << ary; self; end).class_eval { attr_accessor :items_available }
       ary.items_available = items_available

commit 50b746d9246c19c1ad2cf506bb18a0eb8ddd0755
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 28 19:31:48 2014 -0400

    Convert joins to subqueries to fix duplicates in owned_items

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 8e15a07..0f144d8 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -95,15 +95,9 @@ class ApplicationController < ActionController::Base
         cond_sql = "#{klass.table_name}.owner_uuid = ?"
         cond_params = [@object.uuid]
         if params[:include_linked]
-          @objects = @objects.
-            joins("LEFT JOIN links namelinks"\
-                  " ON namelinks.link_class=#{klass.sanitize 'name'}"\
-                  "    AND namelinks.owner_uuid=#{klass.sanitize @object.uuid}"\
-                  "    AND namelinks.tail_uuid=#{klass.sanitize @object.uuid}"\
-                  "    AND namelinks.head_uuid=#{klass.table_name}.uuid")
-          cond_sql += " OR namelinks.uuid IS NOT NULL"
+          cond_sql += " OR #{klass.table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class=#{klass.sanitize 'name'} AND links.owner_uuid=#{klass.sanitize @object.uuid} AND links.tail_uuid=#{klass.sanitize @object.uuid})"
         end
-        @objects = @objects.where(cond_sql, *cond_params).order(:uuid)
+        @objects = @objects.where(cond_sql, *cond_params).order("#{klass.table_name}.uuid")
         @limit = limit_all - all_objects.count
         apply_where_limit_order_params
         items_available = @objects.
diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb
index 25d7317..bfb8ea6 100644
--- a/services/api/app/models/arvados_model.rb
+++ b/services/api/app/models/arvados_model.rb
@@ -82,11 +82,10 @@ class ArvadosModel < ActiveRecord::Base
     if self == Link and user
       or_references_me = "OR (#{table_name}.link_class in (#{sanitize 'permission'}, #{sanitize 'resources'}) AND #{sanitize user.uuid} IN (#{table_name}.head_uuid, #{table_name}.tail_uuid))"
     end
-    joins("LEFT JOIN links permissions ON permissions.head_uuid in (#{table_name}.owner_uuid, #{table_name}.uuid) AND permissions.tail_uuid in (#{sanitized_uuid_list}) AND permissions.link_class='permission'").
-      where("?=? OR #{table_name}.owner_uuid in (?) OR #{table_name}.uuid=? OR permissions.head_uuid IS NOT NULL #{or_references_me}",
-            true, user.is_admin,
-            uuid_list,
-            user.uuid)
+    where("?=? OR #{table_name}.owner_uuid in (?) OR #{table_name}.uuid=? OR #{table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class='permission' AND tail_uuid IN (#{sanitized_uuid_list})) #{or_references_me}",
+          true, user.is_admin,
+          uuid_list,
+          user.uuid)
   end
 
   def logged_attributes

commit 520d80f58ab4358dfce0233fe6880794c819760c
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 28 15:53:31 2014 -0400

    Expose names for owned_items as list_response.name_for() in Workbench

diff --git a/apps/workbench/app/models/arvados_api_client.rb b/apps/workbench/app/models/arvados_api_client.rb
index cf14106..bc07b88 100644
--- a/apps/workbench/app/models/arvados_api_client.rb
+++ b/apps/workbench/app/models/arvados_api_client.rb
@@ -90,7 +90,7 @@ class ArvadosApiClient
     resp
   end
 
-  def self.patch_paging_vars(ary, items_available, offset, limit)
+  def self.patch_paging_vars(ary, items_available, offset, limit, links)
     if items_available
       (class << ary; self; end).class_eval { attr_accessor :items_available }
       ary.items_available = items_available
@@ -102,14 +102,22 @@ class ArvadosApiClient
     if limit
       (class << ary; self; end).class_eval { attr_accessor :limit }
       ary.limit = limit
-    end    
+    end
+    if links
+      (class << ary; self; end).class_eval { attr_accessor :links }
+      ary.links = links
+    end
     ary
   end
 
   def unpack_api_response(j, kind=nil)
     if j.is_a? Hash and j[:items].is_a? Array and j[:kind].match(/(_list|List)$/)
       ary = j[:items].collect { |x| unpack_api_response x, x[:kind] }
-      self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit])
+      links = ArvadosResourceList.new Link
+      links.results = (j[:links] || []).collect do |x|
+        unpack_api_response x, x[:kind]
+      end
+      self.class.patch_paging_vars(ary, j[:items_available], j[:offset], j[:limit], links)
     elsif j.is_a? Hash and (kind || j[:kind])
       oclass = self.kind_class(kind || j[:kind])
       if oclass
diff --git a/apps/workbench/app/models/arvados_resource_list.rb b/apps/workbench/app/models/arvados_resource_list.rb
index 16a59b1..ba3f0a0 100644
--- a/apps/workbench/app/models/arvados_resource_list.rb
+++ b/apps/workbench/app/models/arvados_resource_list.rb
@@ -1,7 +1,7 @@
 class ArvadosResourceList
   include Enumerable
 
-  def initialize(resource_class)
+  def initialize resource_class=nil
     @resource_class = resource_class
   end
 
@@ -134,4 +134,32 @@ class ArvadosResourceList
     results.offset if results.respond_to? :offset
   end
 
+  def result_links
+    results.links if results.respond_to? :links
+  end
+
+  def links_for item_or_uuid, link_class=nil
+    unless @links_for_uuid
+      @links_for_uuid = {}
+      results.links.each do |link|
+        if link.respond_to? :head_uuid
+          @links_for_uuid[link.head_uuid] ||= []
+          @links_for_uuid[link.head_uuid] << link
+        end
+      end
+    end
+    if item_or_uuid.respond_to? :uuid
+      uuid = item_or_uuid.uuid
+    else
+      uuid = item_or_uuid
+    end
+    (@links_for_uuid[uuid] || []).select do |link|
+      link.link_class == link_class
+    end
+  end
+
+  def name_for item_or_uuid
+    links_for(item_or_uuid, 'name').first.name
+  end
+
 end
diff --git a/apps/workbench/app/models/group.rb b/apps/workbench/app/models/group.rb
index f53a6f4..0004c88 100644
--- a/apps/workbench/app/models/group.rb
+++ b/apps/workbench/app/models/group.rb
@@ -1,6 +1,10 @@
 class Group < ArvadosBase
-  def self.owned_items
-    res = $arvados_api_client.api self, "/#{self.uuid}/owned_items", {}
-    $arvados_api_client.unpack_api_response(res)
+  def owned_items params={}
+    res = $arvados_api_client.api self.class, "/#{self.uuid}/owned_items", {
+      _method: 'GET'
+    }.merge(params)
+    ret = ArvadosResourceList.new
+    ret.results = $arvados_api_client.unpack_api_response(res)
+    ret
   end
 end
diff --git a/apps/workbench/app/models/user.rb b/apps/workbench/app/models/user.rb
index c03e317..58c1d3f 100644
--- a/apps/workbench/app/models/user.rb
+++ b/apps/workbench/app/models/user.rb
@@ -17,9 +17,13 @@ class User < ArvadosBase
                              end
   end
 
-  def owned_items
-    res = $arvados_api_client.api self.class, "/#{self.uuid}/owned_items"
-    $arvados_api_client.unpack_api_response(res)
+  def owned_items params={}
+    res = $arvados_api_client.api self.class, "/#{self.uuid}/owned_items", {
+      _method: 'GET'
+    }.merge(params)
+    ret = ArvadosResourceList.new
+    ret.results = $arvados_api_client.unpack_api_response(res)
+    ret
   end
 
   def full_name
diff --git a/apps/workbench/test/unit/group_test.rb b/apps/workbench/test/unit/group_test.rb
index 0821e1f..1e7d087 100644
--- a/apps/workbench/test/unit/group_test.rb
+++ b/apps/workbench/test/unit/group_test.rb
@@ -1,7 +1,28 @@
 require 'test_helper'
 
-class ProjectTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
+class GroupTest < ActiveSupport::TestCase
+  test "get owned_items with names" do
+    use_token :active
+    oi = Group.
+      find(api_fixture('groups')['asubfolder']['uuid']).
+      owned_items(include_linked: true)
+    assert_operator(0, :<, oi.count,
+                    "Expected to find some items belonging to :active user")
+    assert_operator(0, :<, oi.items_available,
+                    "Expected owned_items response to have items_available > 0")
+    assert_operator(0, :<, oi.result_links.count,
+                    "Expected to receive name links with owned_items response")
+    oi_uuids = oi.collect { |i| i['uuid'] }
+
+    expect_uuid = api_fixture('specimens')['in_asubfolder']['uuid']
+    assert_includes(oi_uuids, expect_uuid,
+                    "Expected '#{expect_uuid}' in asubfolder's owned_items")
+
+    expect_uuid = api_fixture('specimens')['in_afolder_linked_from_asubfolder']['uuid']
+    expect_name = api_fixture('links')['specimen_is_in_two_folders']['name']
+    assert_includes(oi_uuids, expect_uuid,
+                    "Expected '#{expect_uuid}' in asubfolder's owned_items")
+    assert_equal(expect_name, oi.name_for(expect_uuid),
+                 "Expected name_for '#{expect_uuid}' to be '#{expect_name}'")
+  end
 end
diff --git a/services/api/test/fixtures/groups.yml b/services/api/test/fixtures/groups.yml
index ce04ece..947d762 100644
--- a/services/api/test/fixtures/groups.yml
+++ b/services/api/test/fixtures/groups.yml
@@ -65,5 +65,5 @@ asubfolder:
   modified_at: 2014-04-21 15:37:48 -0400
   updated_at: 2014-04-21 15:37:48 -0400
   name: A Subfolder
-  description: Test folder belonging to active user's first test folder
+  description: "Test folder belonging to active user's first test folder"
   group_class: folder

commit f8cc86219281026b2867c543524f8e7fa23da291
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Apr 28 11:15:50 2014 -0400

    Use name links instead of permission links to include objects in groups.

diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb
index 713f2cf..8e15a07 100644
--- a/services/api/app/controllers/application_controller.rb
+++ b/services/api/app/controllers/application_controller.rb
@@ -96,12 +96,12 @@ class ApplicationController < ActionController::Base
         cond_params = [@object.uuid]
         if params[:include_linked]
           @objects = @objects.
-            joins("LEFT JOIN links mng_links"\
-                  " ON mng_links.link_class=#{klass.sanitize 'permission'}"\
-                  "    AND mng_links.name=#{klass.sanitize 'can_manage'}"\
-                  "    AND mng_links.tail_uuid=#{klass.sanitize @object.uuid}"\
-                  "    AND mng_links.head_uuid=#{klass.table_name}.uuid")
-          cond_sql += " OR mng_links.uuid IS NOT NULL"
+            joins("LEFT JOIN links namelinks"\
+                  " ON namelinks.link_class=#{klass.sanitize 'name'}"\
+                  "    AND namelinks.owner_uuid=#{klass.sanitize @object.uuid}"\
+                  "    AND namelinks.tail_uuid=#{klass.sanitize @object.uuid}"\
+                  "    AND namelinks.head_uuid=#{klass.table_name}.uuid")
+          cond_sql += " OR namelinks.uuid IS NOT NULL"
         end
         @objects = @objects.where(cond_sql, *cond_params).order(:uuid)
         @limit = limit_all - all_objects.count
@@ -116,10 +116,17 @@ class ApplicationController < ActionController::Base
       end
     end
     @objects = all_objects || []
+    @links = Link.where('link_class=? and owner_uuid=?'\
+                        ' and owner_uuid=tail_uuid'\
+                        ' and head_uuid in (?)',
+                        'name',
+                        @object.uuid,
+                        @objects.collect(&:uuid))
     @object_list = {
       :kind  => "arvados#objectList",
       :etag => "",
       :self_link => "",
+      :links => @links.as_api_response(nil),
       :offset => offset_all,
       :limit => limit_all,
       :items_available => all_available,
diff --git a/services/api/test/fixtures/links.yml b/services/api/test/fixtures/links.yml
index 5b89015..60d822e 100644
--- a/services/api/test/fixtures/links.yml
+++ b/services/api/test/fixtures/links.yml
@@ -292,7 +292,7 @@ test_timestamps:
 
 specimen_is_in_two_folders:
   uuid: zzzzz-o0j2j-ryhm1bn83ni03sn
-  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-04-21 15:37:48 -0400
   modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -300,8 +300,8 @@ specimen_is_in_two_folders:
   updated_at: 2014-04-21 15:37:48 -0400
   tail_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   head_uuid: zzzzz-2x53u-5gid26432uujf79
-  link_class: permission
-  name: can_manage
+  link_class: name
+  name: "I'm in a subfolder, too"
   properties: {}
 
 foo_collection_tag:
diff --git a/services/api/test/functional/arvados/v1/groups_controller_test.rb b/services/api/test/functional/arvados/v1/groups_controller_test.rb
index 74110ae..4b041b6 100644
--- a/services/api/test/functional/arvados/v1/groups_controller_test.rb
+++ b/services/api/test/functional/arvados/v1/groups_controller_test.rb
@@ -125,5 +125,19 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     assert_response :success
     uuids = json_response['items'].collect { |i| i['uuid'] }
     assert_includes uuids, expected_uuid, "Did not get #{expected_uuid}"
+
+    expected_name = links(:specimen_is_in_two_folders).name
+    found_specimen_name = false
+    assert(json_response['links'].any?,
+           "Expected a non-empty array of links in response")
+    json_response['links'].each do |link|
+      if link['head_uuid'] == expected_uuid
+        if link['name'] == expected_name
+          found_specimen_name = true
+        end
+      end
+    end
+    assert(found_specimen_name,
+           "Expected to find name '#{expected_name}' in response")
   end
 end

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list