closes #3821
authorradhika <radhika@curoverse.com>
Wed, 29 Mar 2017 15:44:42 +0000 (11:44 -0400)
committerradhika <radhika@curoverse.com>
Wed, 29 Mar 2017 15:44:42 +0000 (11:44 -0400)
Merge branch '3821-collection-file-manage'

apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/collection.rb
apps/workbench/app/views/collections/_show_files.html.erb
apps/workbench/config/routes.rb
apps/workbench/test/controllers/collections_controller_test.rb
apps/workbench/test/integration/collections_test.rb
services/api/test/fixtures/collections.yml

index 28680df33f3cf4f5902d5abdc278c305011018f2..55e4e9aa4a0227ac4b68e064451e9d51ebcfd834 100644 (file)
@@ -114,42 +114,7 @@ class ActionsController < ApplicationController
   end
 
   expose_action :combine_selected_files_into_collection do
-    link_uuids, coll_ids = params["selection"].partition do |sel_s|
-      ArvadosBase::resource_class_for_uuid(sel_s) == Link
-    end
-
-    unless link_uuids.empty?
-      Link.select([:head_uuid]).where(uuid: link_uuids).each do |link|
-        if ArvadosBase::resource_class_for_uuid(link.head_uuid) == Collection
-          coll_ids << link.head_uuid
-        end
-      end
-    end
-
-    uuids = []
-    pdhs = []
-    source_paths = Hash.new { |hash, key| hash[key] = [] }
-    coll_ids.each do |coll_id|
-      if m = CollectionsHelper.match(coll_id)
-        key = m[1] + m[2]
-        pdhs << key
-        source_paths[key] << m[4]
-      elsif m = CollectionsHelper.match_uuid_with_optional_filepath(coll_id)
-        key = m[1]
-        uuids << key
-        source_paths[key] << m[4]
-      end
-    end
-
-    unless pdhs.empty?
-      Collection.where(portable_data_hash: pdhs.uniq).
-          select([:uuid, :portable_data_hash]).each do |coll|
-        unless source_paths[coll.portable_data_hash].empty?
-          uuids << coll.uuid
-          source_paths[coll.uuid] = source_paths.delete(coll.portable_data_hash)
-        end
-      end
-    end
+    uuids, source_paths = selected_collection_files params
 
     new_coll = Arv::Collection.new
     Collection.where(uuid: uuids.uniq).
index 23b8788bcb17520b4e8ca71f493b514c0fcda9f3..a63fe6e21f1311a353392a2bc2f27f656147bee5 100644 (file)
@@ -1250,6 +1250,49 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  # helper method to get the names of collection files selected
+  helper_method :selected_collection_files
+  def selected_collection_files params
+    link_uuids, coll_ids = params["selection"].partition do |sel_s|
+      ArvadosBase::resource_class_for_uuid(sel_s) == Link
+    end
+
+    unless link_uuids.empty?
+      Link.select([:head_uuid]).where(uuid: link_uuids).each do |link|
+        if ArvadosBase::resource_class_for_uuid(link.head_uuid) == Collection
+          coll_ids << link.head_uuid
+        end
+      end
+    end
+
+    uuids = []
+    pdhs = []
+    source_paths = Hash.new { |hash, key| hash[key] = [] }
+    coll_ids.each do |coll_id|
+      if m = CollectionsHelper.match(coll_id)
+        key = m[1] + m[2]
+        pdhs << key
+        source_paths[key] << m[4]
+      elsif m = CollectionsHelper.match_uuid_with_optional_filepath(coll_id)
+        key = m[1]
+        uuids << key
+        source_paths[key] << m[4]
+      end
+    end
+
+    unless pdhs.empty?
+      Collection.where(portable_data_hash: pdhs.uniq).
+          select([:uuid, :portable_data_hash]).each do |coll|
+        unless source_paths[coll.portable_data_hash].empty?
+          uuids << coll.uuid
+          source_paths[coll.uuid] = source_paths.delete(coll.portable_data_hash)
+        end
+      end
+    end
+
+    [uuids, source_paths]
+  end
+
   def wiselinks_layout
     'body'
   end
index be58b91871f1a7a51fdeb28dc854425af9d8e68a..02b204f18cbec2dc0d2e0a03bfd17c6ec042c8b6 100644 (file)
@@ -1,4 +1,5 @@
 require "arvados/keep"
+require "arvados/collection"
 require "uri"
 
 class CollectionsController < ApplicationController
@@ -293,6 +294,51 @@ class CollectionsController < ApplicationController
     sharing_popup
   end
 
+  def remove_selected_files
+    uuids, source_paths = selected_collection_files params
+
+    arv_coll = Arv::Collection.new(@object.manifest_text)
+    source_paths[uuids[0]].each do |p|
+      arv_coll.rm "."+p
+    end
+
+    if @object.update_attributes manifest_text: arv_coll.manifest_text
+      show
+    else
+      self.render_error status: 422
+    end
+  end
+
+  def update
+    updated_attr = params[:collection].each.select {|a| a[0].andand.start_with? 'rename-file-path:'}
+
+    if updated_attr.size > 0
+      # Is it file rename?
+      file_path = updated_attr[0][0].split('rename-file-path:')[-1]
+
+      new_file_path = updated_attr[0][1]
+      if new_file_path.start_with?('./')
+        # looks good
+      elsif new_file_path.start_with?('/')
+        new_file_path = '.' + new_file_path
+      else
+        new_file_path = './' + new_file_path
+      end
+
+      arv_coll = Arv::Collection.new(@object.manifest_text)
+      arv_coll.rename "./"+file_path, new_file_path
+
+      if @object.update_attributes manifest_text: arv_coll.manifest_text
+        show
+      else
+        self.render_error status: 422
+      end
+    else
+      # Not a file rename; use default
+      super
+    end
+  end
+
   protected
 
   def find_usable_token(token_list)
index 21879a57a7d90aa77ba0f90e6a325f9ae9d5f00c..056f12f6c8e70faa77f45d5f5d356439d528a1ec 100644 (file)
@@ -274,10 +274,10 @@ module ApplicationHelper
       "data-placement" => "bottom",
       "data-type" => input_type,
       "data-title" => "Edit #{attr.to_s.gsub '_', ' '}",
-      "data-name" => attr,
+      "data-name" => htmloptions['selection_name'] || attr,
       "data-object-uuid" => object.uuid,
       "data-toggle" => "manual",
-      "data-value" => attrvalue,
+      "data-value" => htmloptions['data-value'] || attrvalue,
       "id" => span_id,
       :class => "editable #{is_textile?( object, attr ) ? 'editable-textile' : ''}"
     }.merge(htmloptions).merge(ajax_options)
index 13f5357faadba842e57bbc24b1bdc883509ed7f6..ea81ad8c0a7588edc00062585d99ba9fa116035f 100644 (file)
@@ -71,7 +71,7 @@ class Collection < ArvadosBase
   end
 
   def editable_attributes
-    %w(name description manifest_text)
+    %w(name description manifest_text filename)
   end
 
   def provenance
index a21a514c47de1c51ec184dbe1531d5785c2b68d9..d39c81b2b16d2b9b8331226695f8df97f0ac0cec 100644 (file)
                     'data-selection-action' => 'combine-collections',
                     'data-toggle' => 'dropdown'
               %></li>
+            <% if object.editable? %>
+            <li><%= link_to "Remove selected files", '#',
+                    method: :post,
+                    'data-href' => url_for(controller: 'collections', action: :remove_selected_files),
+                    'data-selection-param-name' => 'selection[]',
+                    'data-selection-action' => 'remove-selected-files',
+                    'data-toggle' => 'dropdown'
+              %></li>
+            <% end %>
           </ul>
         </div>
         <div class="btn-group btn-group-sm">
                 } %>
             <span>&nbsp;</span>
             <% end %>
+
+            <% if object.editable? %>
+                <%= link_to({controller: 'collections', action: 'remove_selected_files', id: object.uuid, selection: [object.portable_data_hash+'/'+file_path]}, method: :post, remote: true, data: {confirm: "Remove #{file_path}?", toggle: 'tooltip', placement: 'top'}, class: 'btn btn-sm btn-default btn-nodecorate', title: "Remove #{file_path}") do %>
+                  <i class="fa fa-fw fa-trash-o"></i>
+                <% end %>
+            <% end %>
         <% if CollectionsHelper::is_image(filename) %>
-            <i class="fa fa-fw fa-bar-chart-o"></i> <%= filename %></div>
+            <i class="fa fa-fw fa-bar-chart-o"></i>
+              <% if object.editable? %>
+                <%= render_editable_attribute object, 'filename', filename, {'data-value' => file_path, 'data-toggle' => 'manual', 'selection_path' => 'rename-file-path:'+file_path}, {tiptitle: 'Edit path of this file (name or directory or both). If you use the same path as another file, it may be removed.'} %>
+              <% else %>
+                <%= filename %>
+              <% end %>
+            </div>
           <div class="collection_files_inline">
             <%= link_to(image_tag("#{url_for object}/#{file_path}"),
                         link_params.merge(disposition: 'inline'),
           </div>
          </div>
         <% else %>
-            <i class="fa fa-fw fa-file" href="<%=object.uuid%>/<%=file_path%>" ></i> <%= filename %></div>
+              <% if object.editable? %>
+                <i class="fa fa-fw fa-file"></i><%= render_editable_attribute object, 'filename', filename, {'data-value' => file_path, 'data-toggle' => 'manual', 'selection_name' => 'rename-file-path:'+file_path}, {tiptitle: 'Edit path of this file (name or directory or both). If you use the same path as another file, it may be removed.'}  %>
+              <% else %>
+                <i class="fa fa-fw fa-file" href="<%=object.uuid%>/<%=file_path%>" ></i> <%= filename %>
+              <% end %>
+            </div>
          </div>
         <% end %>
         </li>
index 0d5b8fca83a3a789776baa48b9ea0556767df210..0eef73f8ae3d0116ba96ccf8f93f6663fc4204e8 100644 (file)
@@ -87,6 +87,7 @@ ArvadosWorkbench::Application.routes.draw do
     post 'share', :on => :member
     post 'unshare', :on => :member
     get 'choose', on: :collection
+    post 'remove_selected_files', on: :member
   end
   get('/collections/download/:uuid/:reader_token/*file' => 'collections#show_file',
       format: false)
index 1bf967ccfd8c1e8e2da60a10db81b7e241d70fa9..b4d0f4ae09bec870985f5895a8522faf35766c0e 100644 (file)
@@ -506,7 +506,7 @@ class CollectionsControllerTest < ActionController::TestCase
     collection = api_fixture('collections')['foo_file']
     get :show, {id: collection['uuid']}, session_for(:active)
     assert_includes @response.body, collection['name']
-    assert_match /href="#{collection['uuid']}\/foo" ><\/i> foo</, @response.body
+    assert_match /not authorized to manage collection sharing links/, @response.body
   end
 
   test "No Upload tab on non-writable collection" do
@@ -632,4 +632,104 @@ class CollectionsControllerTest < ActionController::TestCase
       assert_equal "https://download.example/c=#{id.sub '+', '-'}/_/w%20a%20z?api_token=#{tok}", @response.redirect_url
     end
   end
+
+  test "remove selected files from collection" do
+    use_token :active
+
+    # create a new collection to test; using existing collections will cause other tests to fail,
+    # and resetting fixtures after each test makes it take almost 4 times to run this test file.
+    manifest_text = ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n./dir1 d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n"
+
+    collection = Collection.create(manifest_text: manifest_text)
+    assert_includes(collection['manifest_text'], "0:0:file1")
+
+    # now remove all files named 'file1' from the collection
+    post :remove_selected_files, {
+      id: collection['uuid'],
+      selection: ["#{collection['uuid']}/file1",
+                  "#{collection['uuid']}/dir1/file1"],
+      format: :json
+    }, session_for(:active)
+    assert_response :success
+
+    # verify no 'file1' in the updated collection
+    collection = Collection.select([:uuid, :manifest_text]).where(uuid: collection['uuid']).first
+    assert_not_includes(collection['manifest_text'], "0:0:file1")
+    assert_includes(collection['manifest_text'], "0:0:file2") # but other files still exist
+  end
+
+  test "remove all files from a subdir of a collection" do
+    use_token :active
+
+    # create a new collection to test
+    manifest_text = ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n./dir1 d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n"
+
+    collection = Collection.create(manifest_text: manifest_text)
+    assert_includes(collection['manifest_text'], "0:0:file1")
+
+    # now remove all files from "dir1" subdir of the collection
+    post :remove_selected_files, {
+      id: collection['uuid'],
+      selection: ["#{collection['uuid']}/dir1/file1",
+                  "#{collection['uuid']}/dir1/file2"],
+      format: :json
+    }, session_for(:active)
+    assert_response :success
+
+    # verify that "./dir1" no longer exists in this collection's manifest text
+    collection = Collection.select([:uuid, :manifest_text]).where(uuid: collection['uuid']).first
+    assert_match /. d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:file1 0:0:file2\n$/, collection['manifest_text']
+    assert_not_includes(collection['manifest_text'], 'dir1')
+  end
+
+  test "rename file in a collection" do
+    use_token :active
+
+    # create a new collection to test
+    manifest_text = ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n./dir1 d41d8cd98f00b204e9800998ecf8427e+0 0:0:dir1file1 0:0:dir1file2\n"
+
+    collection = Collection.create(manifest_text: manifest_text)
+    assert_includes(collection['manifest_text'], "0:0:file1")
+
+    # rename 'file1' as 'file1renamed' and verify
+    post :update, {
+      id: collection['uuid'],
+      collection: {
+        'rename-file-path:file1' => 'file1renamed'
+      },
+      format: :json
+    }, session_for(:active)
+    assert_response :success
+
+    collection = Collection.select([:uuid, :manifest_text]).where(uuid: collection['uuid']).first
+    assert_match /. d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:file1renamed 0:0:file2\n.\/dir1 d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:dir1file1 0:0:dir1file2\n$/, collection['manifest_text']
+
+    # now rename 'file2' such that it is moved into 'dir1'
+    @test_counter = 0
+    post :update, {
+      id: collection['uuid'],
+      collection: {
+        'rename-file-path:file2' => 'dir1/file2'
+      },
+      format: :json
+    }, session_for(:active)
+    assert_response :success
+
+    collection = Collection.select([:uuid, :manifest_text]).where(uuid: collection['uuid']).first
+    assert_match /. d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:file1renamed\n.\/dir1 d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:dir1file1 0:0:dir1file2 0:0:file2\n$/, collection['manifest_text']
+
+    # now rename 'dir1/dir1file1' such that it is moved into a new subdir
+    @test_counter = 0
+    post :update, {
+      id: collection['uuid'],
+      collection: {
+        'rename-file-path:dir1/dir1file1' => 'dir2/dir3/dir1file1moved'
+      },
+      format: :json
+    }, session_for(:active)
+    assert_response :success
+
+    collection = Collection.select([:uuid, :manifest_text]).where(uuid: collection['uuid']).first
+    assert_match /. d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:file1renamed\n.\/dir1 d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:dir1file2 0:0:file2\n.\/dir2\/dir3 d41d8cd98f00b204e9800998ecf8427e\+0\+A(.*) 0:0:dir1file1moved\n$/, collection['manifest_text']
+  end
 end
index 8190b35f00747627c198af7bdd296d2e61d23377..eb9c2e831a8866d7a25f3f89c9c1eb04ef00a003 100644 (file)
@@ -298,4 +298,63 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     # Make sure we're not still on the old collection page.
     refute_match(%r{/collections/#{col['uuid']}}, page.current_url)
   end
+
+  test "remove a file from collection using checkbox and dropdown option" do
+    visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
+    assert(page.has_text?('file1'), 'file not found - file1')
+
+    # remove first file
+    input_files = page.all('input[type=checkbox]')
+    input_files[0].click
+
+    click_button 'Selection...'
+    within('.selection-action-container') do
+      click_link 'Remove selected files'
+    end
+
+    assert(page.has_no_text?('file1'), 'file found - file')
+    assert(page.has_text?('file2'), 'file not found - file2')
+  end
+
+  test "remove a file in collection using trash icon" do
+    need_selenium 'to confirm remove'
+
+    visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
+    assert(page.has_text?('file1'), 'file not found - file1')
+
+    first('.fa-trash-o').click
+    page.driver.browser.switch_to.alert.accept
+
+    assert(page.has_no_text?('file1'), 'file found - file')
+    assert(page.has_text?('file2'), 'file not found - file2')
+  end
+
+  test "rename a file in collection" do
+    visit page_with_token('active', '/collections/zzzzz-4zz18-a21ux3541sxa8sf')
+
+    within('.collection_files') do
+      first('.fa-pencil').click
+      find('.editable-input input').set('file1renamed')
+      find('.editable-submit').click
+    end
+
+    assert(page.has_text?('file1renamed'), 'file not found - file1renamed')
+  end
+
+  test "remove/rename file options not presented if user cannot update a collection" do
+    # visit a publicly accessible collection as 'spectator'
+    visit page_with_token('spectator', '/collections/zzzzz-4zz18-uukreo9rbgwsujr')
+
+    click_button 'Selection'
+    within('.selection-action-container') do
+      assert_selector 'li', text: 'Create new collection with selected files'
+      assert_no_selector 'li', text: 'Remove selected files'
+    end
+
+    within('.collection_files') do
+      assert(page.has_text?('GNU_General_Public_License'), 'file not found - GNU_General_Public_License')
+      assert_nil first('.fa-pencil')
+      assert_nil first('.fa-trash-o')
+    end
+  end
 end
index 2f4d5b08a836dbd0cf3fc5c39bcdf9586521ca50..f48fbf1b8542d0f35fd37888c778fd264e820e7a 100644 (file)
@@ -645,6 +645,18 @@ collection_not_readable_by_active:
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: collection_not_readable_by_active
 
+collection_to_remove_and_rename_files:
+  uuid: zzzzz-4zz18-a21ux3541sxa8sf
+  portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-02-03T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-02-03T17:22:54Z
+  updated_at: 2014-02-03T17:22:54Z
+  manifest_text: ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:file1 0:0:file2\n"
+  name: collection to remove and rename files
+
 
 # Test Helper trims the rest of the file