diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb
index 3aaf0ad38..9c4499831 100644
--- a/BrainPortal/app/controllers/userfiles_controller.rb
+++ b/BrainPortal/app/controllers/userfiles_controller.rb
@@ -39,7 +39,7 @@ class UserfilesController < ApplicationController
around_action :permission_check, :only => [
:download, :update_multiple, :delete_files,
- :create_collection, :change_provider, :quality_control,
+ :create_collection, :create_virtual_collection, :change_provider, :quality_control,
:export_file_list
]
@@ -1040,6 +1040,84 @@ def create_collection #:nodoc:
end
+ #Create a collection from the selected files.
+ def create_virtual_collection #:nodoc:
+ filelist = params[:file_ids].uniq || []
+ data_provider_id = params[:data_provider_id_for_collection]
+ collection_name = params[:collection_name]
+ file_group = current_assignable_group.id
+
+ if data_provider_id.blank?
+ flash[:error] = "No data provider selected.\n"
+ redirect_to :action => :index
+ return
+ end
+
+ # Handle collection name
+ if collection_name.blank?
+ suffix = Time.now.to_i
+ while Userfile.where(:user_id => current_user.id, :name => "VirtualCollection-#{suffix}").first.present?
+ suffix += 1
+ end
+ collection_name = "VirtualCollection-#{suffix}"
+ end
+
+ if ! Userfile.is_legal_filename?(collection_name)
+ flash[:error] = "Error: collection name '#{collection_name}' is not acceptable (illegal characters?)."
+ redirect_to :action => :index, :format => request.format.to_sym
+ return
+ end
+
+ # Check if the collection name chosen by the user already exists for this user on the data_provider
+ if current_user.userfiles.exists?(:name => collection_name, :data_provider_id => data_provider_id)
+ flash[:error] = "Error: collection with name '#{collection_name}' already exists."
+ redirect_to :action => :index, :format => request.format.to_sym
+ return
+ end
+
+ userfiles = Userfile.find_accessible_by_user(filelist, current_user, :access_requested => :read)
+
+ # todo double check how 0 is possible, bad files should cause exception
+ if userfiles.count == 0
+ flash[:error] = "Error: Inaccessible files selected."
+ redirect_to :action => :index, :format => request.format.to_sym
+ return
+ end
+
+ collection = VirtualFileCollection.new(
+ :user_id => current_user.id,
+ :group_id => file_group,
+ :data_provider_id => data_provider_id,
+ :name => collection_name
+ )
+
+ collection.save!
+ collection.cache_prepare
+ coldir = collection.cache_full_path
+ Dir.mkdir(coldir)
+
+ collection.set_virtual_file_collection(userfiles)
+
+ # Save the content and DB model
+
+ collection.sync_to_provider
+ collection.save
+ collection.set_size
+
+ # Find the files
+ userfiles = Userfile
+ .find_all_accessible_by_user(current_user, :access_requested => :read)
+ .where(:id => filelist).all.to_a
+
+ if userfiles.empty?
+ flash[:error] = "You need to select some files first."
+ redirect_to(:action => :index)
+ return
+ end
+ redirect_to(:controller => :userfiles, :action => :show, :id => collection.id)
+
+ end
+
# Copy or move files to a new provider.
def change_provider #:nodoc:
diff --git a/BrainPortal/app/helpers/userfiles_helper.rb b/BrainPortal/app/helpers/userfiles_helper.rb
index ad0eb5089..3432b3a47 100644
--- a/BrainPortal/app/helpers/userfiles_helper.rb
+++ b/BrainPortal/app/helpers/userfiles_helper.rb
@@ -110,11 +110,11 @@ def data_link(file_name, userfile, replace_div_id="sub_viewer_filecollection_cbr
end
elsif display_name =~ /\.html$/i # TODO: this will never happen if we ever create a HtmlFile model with at least one viewer
link_to "#{display_name}",
- stream_userfile_path(@userfile, :file_path => file_name, :disposition => 'inline'),
+ stream_userfile_path(userfile, :file_path => file_name, :disposition => 'inline'),
:target => '_BLANK'
else
link_to h(display_name),
- url_for(:action => :content, :content_loader => :collection_file, :arguments => file_name)
+ url_for(:action => :content, :id => userfile.id, :content_loader => :collection_file, :arguments => file_name)
end
end
diff --git a/BrainPortal/app/views/userfiles/_dialogs.html.erb b/BrainPortal/app/views/userfiles/_dialogs.html.erb
index 4e670c292..08a395e4c 100644
--- a/BrainPortal/app/views/userfiles/_dialogs.html.erb
+++ b/BrainPortal/app/views/userfiles/_dialogs.html.erb
@@ -349,6 +349,36 @@
+
+
+
+ New virtual collection
+
+
+
+
+
.
+#
+-%>
+
+<% limit = 500 %>
+<% base_dir = base_directory rescue params[:base_directory] %>
+<% base_dir = base_dir.presence || "." %>
+
+<% file_list ||= ( @userfile.list_linked_files(base_dir, [:regular, :directory, :link]) rescue [] ) %>
+
+<% if file_list.blank? %>
+
+
">
+ |
+ (Empty) |
+ |
+
+
+<% else %>
+
+ <% for file in file_list[0,limit] %>
+ <% fname = file.name %>
+ <% fname = fname.delete_prefix(@userfile.name + '/') unless @userfile.id == file.userfile.id %>
+ <% if file.symbolic_type == :directory %>
+ <%= on_click_ajax_replace( { :element => "tr",
+ :url => url_for(
+ :id => file.userfile.id,
+ :action => :display,
+ :viewer => "directory_contents",
+ :viewer_userfile_class => "FileCollection",
+ :base_directory => ".",
+ :apply_div => "false"
+ ),
+ :position => "after",
+ :before => "
Loading... | "
+ },
+ { :class => "#{cycle("list-odd", "list-even")}",
+ :id => file.name.gsub(/\W+/, "_")
+ }
+ ) do %>
+ <%= render :file => @viewer.partial_path(:plain_file_list_row), :locals => {:file => file} %>
+ <% end %>
+ <% else %>
+
">
+ <%= render :file => @viewer.partial_path(:plain_file_list_row), :locals => {:file => file} %>
+
+ <% end %>
+ <% end %>
+
+ <% if file_list.size > limit %>
+
">
+ |
+ <%= (" " * 6 * file_list.first.depth).html_safe %> ... <%= image_tag "/images/lotsa_files_icon.png" %> <%= pluralize(file_list.size-limit, "more entry") %> |
+ |
+
+ <% end %>
+
+<% end %>
+
+
+
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb
new file mode 100644
index 000000000..2c7292876
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.html.erb
@@ -0,0 +1,36 @@
+
+<%-
+#
+# CBRAIN Project
+#
+# Copyright (C) 2008-2012
+# The Royal Institution for the Advancement of Learning
+# McGill University
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
.
+#
+-%>
+
+<%
+ # This partial can be invoked directly as a viewer from the display action,
+ # or rendered as part of another piece of view code. As such it will
+ # accept a "base_directory" either as a params[] or as a local variable
+ base_dir = base_directory rescue params[:base_directory]
+%>
+
+<%= render :file => VirtualFileCollection.view_path(:file_collection_form),
+ :locals => { :base_directory => base_dir } %>
+
+
+
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb
new file mode 100644
index 000000000..4cd7bc06f
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection.json.erb
@@ -0,0 +1,2 @@
+<%= @userfile.list_linked_files.to_json %>
+
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb
new file mode 100644
index 000000000..b9ad42ce8
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_form.html.erb
@@ -0,0 +1,57 @@
+
+<%-
+#
+# CBRAIN Project
+#
+# Copyright (C) 2008-2012
+# The Royal Institution for the Advancement of Learning
+# McGill University
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
.
+#
+-%>
+
+<%
+ # This partial requires one local variable:
+ #
+ # base_directory : the subdirectory inside the userfile where we start to render the directory content
+%>
+
+<% if @userfile.num_files && @userfile.num_files > 0 %>
+
+
+ <%= form_for @userfile, :as => :userfile,
+ :url => { :controller => :userfiles,
+ :action => :extract_from_collection
+ },
+ :html => { :method => :post,
+ :id => "userfile_edit_#{@userfile.id}_#{base_directory}"
+ } do |f| %>
+
+ <%= ajax_element(display_userfile_path(@userfile,
+ :viewer => :file_collection_top_table,
+ :viewer_userfile_class => :VirtualFileCollection,
+ :base_directory => base_directory,
+ ), :class => "loading_message") do %>
+
+ Loading...
+
+ <% end %>
+
+
+
+ <% end %>
+
+<% end %>
+
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb
new file mode 100644
index 000000000..a3114b88d
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_file_collection_top_table.html.erb
@@ -0,0 +1,46 @@
+
+<%-
+#
+# CBRAIN Project
+#
+# Copyright (C) 2008-2012
+# The Royal Institution for the Advancement of Learning
+# McGill University
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
.
+#
+-%>
+
+<%
+ # This partial requires one params variable:
+ # params[:base_directory] : a relative path to the subdirectory inside the userfile; we will list from that point.
+%>
+
+<%
+ base_dir = params[:base_directory].presence || ""
+%>
+
+
+
+
+ |
+ File |
+ DL |
+ Size |
+
+ <%= render :file => VirtualFileCollection.view_path(:directory_contents),
+ :locals => { :base_directory => base_dir }
+ %>
+
+
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb
new file mode 100644
index 000000000..37049256e
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/views/_plain_file_list_row.html.erb
@@ -0,0 +1,87 @@
+
+<%-
+#
+# CBRAIN Project
+#
+# Copyright (C) 2008-2012
+# The Royal Institution for the Advancement of Learning
+# McGill University
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
.
+#
+-%>
+
+<%
+ # This partial receives one local variable:
+ # file : a file structure as returned by the FileCollection listing method
+ # Note that is contains a full relative path, starting with the @userfile's name itself.
+%>
+
+
+ <% if @userfile.is_locally_synced? && file.symbolic_type == :regular %>
+ <%= check_box_tag("file_names[]", file.name, false, :class => "collection_checkbox", :id => nil) %>
+ <% end %>
+ |
+
+
+
+
+
+ <% if file.symbolic_type == :directory %>
+ <%= image_tag "/images/folder_icon_solid.png" %>
+ <% else %>
+ <%= image_tag "/images/file_icon.png" %>
+ <% end %>
+
+
+ <% bname = Pathname.new(file.name).basename.to_s %>
+
+ <% fname = file.name %>
+ <% fname = fname.delete_prefix(@userfile.name + '/') if @userfile != file.userfile %>
+
+ <% if file.size > 0 %>
+ <%= data_link fname, file.userfile %>
+ <% else %>
+ <%= bname %>
+ <% end %>
+
+ <% if file.symbolic_type == :directory %>
+
+ Expand
+ Hide
+ <% end %>
+
+ |
+
+
+ <% if file.symbolic_type == :regular && file.size > 0 && file.size < UserfilesController::MAX_DOWNLOAD_MEGABYTES.megabytes %>
+
+ <% if @userfile.id != file.userfile.id %>
+ <% download_url = url_for(:action => :content, :content_loader => :collection_file, :id => file.userfile.id, :arguments => fname) %>
+ <% else %>
+ <% download_url = url_for(:action => :download, :id => file.userfile.id) %>
+ <% end %>
+
+ <%= link_to download_url do %>
+
+ <% end %>
+
+ <% end %>
+ |
+
+
+ <% if file.symbolic_type != :directory %>
+ <%= colored_pretty_size(file.size) %>
+ <% end %>
+ |
diff --git a/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb
new file mode 100644
index 000000000..c2e73cdce
--- /dev/null
+++ b/BrainPortal/cbrain_plugins/cbrain-plugins-base/userfiles/virtual_file_collection/virtual_file_collection.rb
@@ -0,0 +1,246 @@
+
+#
+# CBRAIN Project
+#
+# Copyright (C) 2008-2024
+# The Royal Institution for the Advancement of Learning
+# McGill University
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see
.
+#
+
+# This file collection is collection of other Userfiles or Collections
+# It is implemented using soft links
+# two level collections are forbidden to prevent recursion or other issues
+class VirtualFileCollection < FileCollection
+
+ Revision_info=CbrainFileRevision[__FILE__] #:nodoc:
+
+ CSV_BASENAME = "_virtual_file_collection.cbcsv"
+ # todo. add .bidsignore file, otherwise bids validation. Or we can allow CBRAIN filenames starting with dot
+
+ CBRAIN_ARCHIVE_CONTENT_BASENAME = nil
+
+
+ reset_viewers # we opted to ignore superclass viewers rather than adjust them
+ has_viewer :name => 'Virtual File Collection', :partial => :file_collection , :if => :is_locally_synced?
+
+ def self.pretty_type #:nodoc:
+ "Virtual File Collection"
+ end
+
+
+ def set_size!
+ self.size, self.num_files = Rails.cache.fetch("VirtualFileCollection_#{self.id || "#{current_user.id}_#{self.data_provider_id}_#{self.name}"}#size", expires_in: 3.minutes) do
+ userfiles = self.get_userfiles
+ [userfiles.sum(&:size), userfiles.sum(&:num_files)]
+ end
+ self.assign_attributes(size: self.size , num_files: self.num_files) if self.id
+ true
+ end
+
+ # Sync the VirtualFileCollection, with the files too
+ def sync_to_cache(deep=true) #:nodoc:
+ syncstat = self.local_sync_status(:refresh)
+ return true if syncstat && syncstat.status == 'InSync'
+ super()
+ if deep && ! self.archived?
+ self.sync_files
+ self.update_cache_symlinks
+ end
+ @cbfl = files= nil # flush internal cache
+ true
+ end
+
+ # Invokes the local sync_to_cache with deep=false; this means the
+ # constitute FileCollection are not synchronized and symlinks not created.
+ # This method is used by FileCollection when archiving or unarchiving.
+ def sync_to_cache_for_archiving
+ result = sync_to_cache(false)
+ self.erase_cache_symlinks rescue nil
+ result
+ end
+
+ # When syncing to the provider, we locally erase
+ # the symlinks, because they make no sense outside
+ # of the local Rails app.
+ # FIXME: this method has a slight race condition,
+ # after syncing to the provider we recreate the
+ # symlinks, but if another program tries to access
+ # them during that time they might not yet be there.
+ def sync_to_provider #:nodoc:
+ self.cache_writehandle do # when the block ends, it will trigger the provider upload
+ self.erase_cache_symlinks unless self.archived?
+ end
+ self.make_cache_symlinks unless self.archived?
+ true
+ end
+
+ # Sets the set of FileCollections that constitute VirtualFileCollection.
+ # The CSV file inside the study will be created/updated,
+ # as well as all the symbolic links. The content
+ # is NOT synced to the provider side.
+ def set_virtual_file_collection(userfiles)
+ cb_error "Multi layer collections are not supported." if userfiles.any? { |f| f.is_a?(VirtualFileCollection) || f.is_a?(CivetVirtualStudy) }
+
+ # Prepare CSV content
+ content = CbrainFileList.create_csv_file_from_userfiles(userfiles)
+
+ # This optimize so we don't reload the content for making the symlinks
+ @cbfl = CbrainFileList.new
+ @cbfl.load_from_content(content)
+ @files = nil
+
+ # Write CSV content to the interal CSV file
+ self.cache_prepare
+ Dir.mkdir(self.cache_full_path) unless Dir.exist?(self.cache_full_path)
+ File.write(csv_cache_full_path.to_s, content)
+ self.update_cache_symlinks
+ self.cache_is_newer
+ end
+
+ # List linked files or directories, as if present directly
+ def list_linked_files(dir=:all, allowed_types = :regular)
+ if allowed_types.is_a? Array
+ types = allowed_types.dup
+ else
+ types = [allowed_types]
+ end
+ types.map!(&:to_sym)
+ types << :file if types.delete(:regular)
+
+ # for combination of :top and :directory file type data are maid up,
+ # to avoid running file stats command which should not affect file browsing
+ # alternatively, new option(s) can be added to list_files/cache_collection_index,
+ # or new dir_info method
+ if (dir == :top || dir == '.')
+ cloned_files = self.list_files(:top, :link).cb_deep_clone # no altering the cache of list_files methods
+ userfiles_by_name = self.get_userfiles.index_by(&:name)
+ return cloned_files.filter_map do |file|
+ fname = file.name.split('/')[1] # gets basename
+ userfile = userfiles_by_name[fname]
+ if types.include?(:directory) && userfile.is_a?(FileCollection)
+ file.symbolic_type = :directory
+ file.userfile = userfile
+ # binding.pry
+ file
+ elsif types.include?(:file) && userfile.is_a?(SingleFile)
+ file = userfile.list_files.first.clone
+ file&.name = self.name + '/' + fname
+ file&.symbolic_type = :regular
+ file.userfile = userfile
+ # binding.pry
+ file
+ end
+ end
+ end
+
+ userfiles = self.get_userfiles
+
+ if dir.is_a? String
+ name, dir = dir.split '/'
+ dir |= '.'
+ userfiles = userfiles.select { |x| x.name == name }
+ end
+
+ userfiles.map do |userfile|
+ userfile.list_files(dir, allowed_types).each do |f|
+ f.name = self.name + '/' + f.name
+ end
+ f.userfile = userfile
+ end.flatten
+ end
+
+ # todo - remove unless needed for creation?
+ # def validate_componets
+ # ufiles = self.get_userfiles
+ # error "Nested Virtual Collection" if ufile.is_a?(VirtualFileCollection) || ufile.is_a?(CivetVirtualStudy) || ufile.type.lower.include?('virtual')
+ # end
+
+ # Returns the files IDs
+ def get_ids
+ self.get_userfiles.map(&:id)
+ end
+
+ # Returns the list of files in the internal CbrainFileList
+ # The list is cached internally and access control is applied
+ # based on the owner of the VirtualFileCollection.
+ def get_userfiles #:nodoc:
+
+ if @cbfl.blank?
+ @cbfl = CbrainFileList.new
+ file_content = File.read(csv_cache_full_path.to_s)
+ @cbfl.load_from_content(file_content)
+ end
+
+ @files ||= @cbfl.userfiles_accessible_by_user!(self.user).compact
+ file_names = @files.map(&:name)
+ dup_names = file_names.select { |name| file_names.count(name) >1 }.uniq
+ cb_error "Virtual file collection contains duplicate filenames #{dup_names.join(',')}" if dup_names.present?
+ @files.each do |f|
+ cb_error "Nested virtual file collections are not supported, remove file with id #{f.id}" if (
+ f.is_a?(VirtualFileCollection) ||
+ f.is_a?(CivetVirtualStudy) ||
+ f.type.downcase.include?('virtual')
+ )
+ end
+ end
+
+
+
+ #====================================================================
+ # Support methods, not part of this model's API.
+ #====================================================================
+
+ protected
+
+ # Synchronize each file
+ def sync_files #:nodoc:
+ self.get_userfiles.each { |uf| uf.sync_to_cache }
+ end
+
+ # Clean up ALL symbolic links
+ def erase_cache_symlinks #:nodoc:
+ Dir.chdir(self.cache_full_path) do
+ Dir.glob('*').each do |entry|
+ # FIXME how to only erase symlinks that points to a CBRAIN cache or local DP?
+ # Parsing the value of the symlink is tricky...
+ File.unlink(entry) if File.symlink?(entry)
+ end
+ end
+ end
+
+ # This cleans up any old symbolic links, then recreates them.
+ # Note that this does not sync the files themselves.
+ def update_cache_symlinks #:nodoc:
+ self.erase_cache_symlinks
+ self.make_cache_symlinks
+ end
+
+ # Create symbolic links in cache for each element of the virtual collection
+ # Note that this does not sync the files themselves.
+ def make_cache_symlinks #:nodoc:
+ self.get_userfiles.each do |uf|
+ link_value = uf.cache_full_path
+ link_path = self.cache_full_path + link_value.basename
+ File.unlink(link_path) if File.symlink?(link_path) && File.readlink(link_path) != link_value
+ File.symlink(link_value, link_path) unless File.exist?(link_path)
+ end
+ end
+
+ def csv_cache_full_path #:nodoc:
+ self.cache_full_path + CSV_BASENAME
+ end
+
+end
diff --git a/BrainPortal/config/routes.rb b/BrainPortal/config/routes.rb
index d8aa1c02e..92bc2b666 100644
--- a/BrainPortal/config/routes.rb
+++ b/BrainPortal/config/routes.rb
@@ -149,6 +149,7 @@
post 'create_parent_child'
delete 'delete_files'
post 'create_collection'
+ post 'create_virtual_collection'
put 'update_multiple'
post 'change_provider'
post 'compress'
diff --git a/BrainPortal/public/javascripts/userfiles.js b/BrainPortal/public/javascripts/userfiles.js
index e22b4046f..62b7a32a9 100644
--- a/BrainPortal/public/javascripts/userfiles.js
+++ b/BrainPortal/public/javascripts/userfiles.js
@@ -124,7 +124,8 @@ $(function() {
rename: '/userfiles/:id',
update: $('#prop-dialog > form').attr('action'),
tags: '/tags/:id',
- create_collection: $('#collection-dialog > form').attr('action')
+ create_collection: $('#collection-dialog > form').attr('action'),
+ create_virtual_collection: $('#virtual-collection-dialog > form').attr('action')
};
/* Userfiles actions/operations */
@@ -386,6 +387,20 @@ $(function() {
return defer(function () { uform.submit(); }).promise();
},
+ /*
+ * Create a new VirtualFileCollection containing the currently selected files.
+ * The new collection's name and target data provider are to be specified
+ * in the HTML form argument +form.
+ */
+ create_virtual_collection: function (form) {
+ var uform = userfiles.children('form');
+
+ setup_form(uform, urls.create_virtual_collection, 'POST', form);
+ clear_selection(true);
+
+ return defer(function () { uform.submit(); }).promise();
+ },
+
/*
* Tag-related operations; generic CRUD with parameters +id+ (tag ID) and
* +data+ (tag attributes as a JS object)
@@ -1097,6 +1112,40 @@ $(function() {
.toggleClass('ui-state-disabled', !valid);
});
+ /* New virtual collection dialog */
+ $('#virtual-collection-dialog')
+ .dialog('option', 'buttons', {
+ 'Cancel': function (event) {
+ $(this).trigger('close.uf');
+ },
+ 'Create': function (event) {
+ var dialog = $(this);
+
+ dialog.trigger('close.uf');
+ userfiles.create_virtual_collection(dialog.children('form')[0]);
+ }
+ })
+ .unbind('open.uf.col-open')
+ .bind( 'open.uf.col-open', function () {
+ $(this).dialog('option', 'title',
+ 'New virtual collection - ' + formatted_selection()
+ );
+ })
+ .undelegate('#co-name', 'input.uf.co-name-check')
+ .delegate( '#co-name', 'input.uf.co-name-check', function () {
+ var valid = /^\w[\w~!@#%^&*()-+=:[\]{}|<>,.?]*$/.test($(this).val());
+
+ $('#co-invalid-name').css({
+ visibility: valid ? 'hidden' : 'visible'
+ });
+
+ $('#virtual-collection-dialog')
+ .parent()
+ .find(':button:contains("Create")')
+ .prop('disabled', !valid)
+ .toggleClass('ui-state-disabled', !valid);
+ });
+
/* Delete files confirmation dialog */
$('#delete-confirm')
.unbind('open.uf.del-cfrm-open')