From 1a14bae5cefb4d607efaa65b96142562d618b4de Mon Sep 17 00:00:00 2001 From: Karel Picman Date: Wed, 19 Apr 2017 11:56:29 +0200 Subject: [PATCH] Document Location - Folder Structure #543 --- app/models/dmsf_file.rb | 28 +--- app/models/dmsf_file_revision.rb | 36 +++--- app/models/dmsf_folder.rb | 4 + app/models/dmsf_folder_permission.rb | 10 ++ app/models/dmsf_link.rb | 16 +-- .../20170418104901_migrate_documents.rb | 120 ++++++++++++++++++ test/fixtures/dmsf_file_revisions.yml | 17 ++- .../files/{p_ecookbook => 2017/04}/test.gif | Bin .../files/{p_ecookbook => 2017/04}/test.pdf | Bin .../files/{p_ecookbook => 2017/04}/test.txt | 0 test/fixtures/files/p_onlinestore/test.txt | 1 - test/unit/dmsf_folder_permission_test.rb | 20 ++- test/unit/dmsf_folder_test.rb | 37 +++--- test/unit/project_patch_test.rb | 10 +- 14 files changed, 223 insertions(+), 76 deletions(-) create mode 100644 db/migrate/20170418104901_migrate_documents.rb rename test/fixtures/files/{p_ecookbook => 2017/04}/test.gif (100%) rename test/fixtures/files/{p_ecookbook => 2017/04}/test.pdf (100%) rename test/fixtures/files/{p_ecookbook => 2017/04}/test.txt (100%) delete mode 100644 test/fixtures/files/p_onlinestore/test.txt diff --git a/app/models/dmsf_file.rb b/app/models/dmsf_file.rb index 2069b36c..4788c9bb 100644 --- a/app/models/dmsf_file.rb +++ b/app/models/dmsf_file.rb @@ -254,34 +254,20 @@ class DmsfFile < ActiveRecord::Base errors[:base] << l(:error_file_is_locked) return false end - - project = container.is_a?(Project) ? container : container.project - # If the target project differs from the source project we must physically move the disk files - if self.project != project - self.dmsf_file_revisions.all.each do |rev| - if File.exist? rev.disk_file(self.project) - FileUtils.mv rev.disk_file(self.project), rev.disk_file(project) - end - end - end - # Must invalidate source parent folder cache before moving RedmineDmsf::Webdav::Cache.invalidate_item(propfind_cache_key) - self.container_type = self.container_type self.container_id = container.id self.dmsf_folder = folder new_revision = self.last_revision.clone new_revision.dmsf_file = self + project = container.is_a?(Project) ? container : container.project new_revision.comment = l(:comment_moved_from, :source => "#{self.project.identifier}:#{self.dmsf_path_str}") new_revision.custom_values = [] - self.last_revision.custom_values.each do |cv| new_revision.custom_values << CustomValue.new({:custom_field => cv.custom_field, :value => cv.value}) end - self.set_last_revision(new_revision) - self.save && new_revision.save end @@ -290,13 +276,6 @@ class DmsfFile < ActiveRecord::Base end def copy_to_filename(container, folder=nil, filename) - project = container.is_a?(Project) ? container : container.project - # If the target project differs from the source project we must physically move the disk files - if (self.project != project) && self.last_revision - if File.exist? self.last_revision.disk_file(self.project) - FileUtils.cp self.last_revision.disk_file(self.project), self.last_revision.disk_file(project) - end - end file = DmsfFile.new file.dmsf_folder = folder file.container_type = self.container_type @@ -306,6 +285,11 @@ class DmsfFile < ActiveRecord::Base if file.save && self.last_revision new_revision = self.last_revision.clone new_revision.dmsf_file = file + new_revision.disk_filename = new_revision.new_storage_filename + if File.exist? self.last_revision.disk_file + FileUtils.cp self.last_revision.disk_file, new_revision.disk_file + end + project = container.is_a?(Project) ? container : container.project new_revision.comment = l(:comment_copied_from, :source => "#{project.identifier}: #{self.dmsf_path_str}") new_revision.custom_values = [] self.last_revision.custom_values.each do |cv| diff --git a/app/models/dmsf_file_revision.rb b/app/models/dmsf_file_revision.rb index 21a8b5ec..87378718 100644 --- a/app/models/dmsf_file_revision.rb +++ b/app/models/dmsf_file_revision.rb @@ -132,22 +132,28 @@ class DmsfFileRevision < ActiveRecord::Base "#{self.major_version}.#{self.minor_version}" end - def storage_base_path(project = nil) - project = self.dmsf_file.project unless project - path = DmsfFile.storage_path.dup - if self.dmsf_file && project - project_base = project.identifier.gsub(/[^\w\.\-]/,'_') - path << "/p_#{project_base}" - end + def storage_base_path + time = self.created_at || DateTime.now + path = time.strftime('%Y/%m') + "#{DmsfFile.storage_path}/#{path}" end - def disk_file(project = nil) - project = self.dmsf_file.project unless project - path = storage_base_path(project) + def disk_file + path = self.storage_base_path FileUtils.mkdir_p(path) unless File.exist?(path) "#{path}/#{self.disk_filename}" end + def new_storage_filename + raise DmsfAccessError, 'File id is not set' unless self.dmsf_file.id + filename = DmsfHelper.sanitize_filename(self.name) + timestamp = DateTime.now.strftime("%y%m%d%H%M%S") + while File.exist?(File.join(storage_base_path, "#{timestamp}_#{self.dmsf_file.id}_#{filename}")) + timestamp.succ! + end + "#{timestamp}_#{self.dmsf_file.id}_#{filename}" + end + def detect_content_type content_type = self.mime_type content_type = Redmine::MimeType.of(self.disk_filename) if content_type.blank? @@ -228,16 +234,6 @@ class DmsfFileRevision < ActiveRecord::Base end end - def new_storage_filename - raise DmsfAccessError, 'File id is not set' unless self.dmsf_file.id - filename = DmsfHelper.sanitize_filename(self.name) - timestamp = DateTime.now.strftime("%y%m%d%H%M%S") - while File.exist?(File.join(storage_base_path, "#{timestamp}_#{self.dmsf_file.id}_#{filename}")) - timestamp.succ! - end - "#{timestamp}_#{self.dmsf_file.id}_#{filename}" - end - def copy_file_content(open_file) File.open(self.disk_file, 'wb') do |f| while (buffer = open_file.read(8192)) diff --git a/app/models/dmsf_folder.rb b/app/models/dmsf_folder.rb index 49e75586..713cfd26 100644 --- a/app/models/dmsf_folder.rb +++ b/app/models/dmsf_folder.rb @@ -297,6 +297,10 @@ class DmsfFolder < ActiveRecord::Base l.copy_to project, new_folder end + self.dmsf_folder_permissions.each do |p| + p.copy_to new_folder + end + return new_folder end diff --git a/app/models/dmsf_folder_permission.rb b/app/models/dmsf_folder_permission.rb index fb2df021..0c3d86ab 100644 --- a/app/models/dmsf_folder_permission.rb +++ b/app/models/dmsf_folder_permission.rb @@ -25,4 +25,14 @@ class DmsfFolderPermission < ActiveRecord::Base scope :users, -> { where(:object_type => User.model_name.to_s) } scope :roles, -> { where(:object_type => Role.model_name.to_s) } + + def copy_to(folder) + permission = DmsfFolderPermission.new + permission.dmsf_folder_id = folder.id + permission.object_id = self.object_id + permission.object_type = self.object_type + permission.save + permission + end + end diff --git a/app/models/dmsf_link.rb b/app/models/dmsf_link.rb index ee0c63a0..26b837a7 100644 --- a/app/models/dmsf_link.rb +++ b/app/models/dmsf_link.rb @@ -110,14 +110,14 @@ class DmsfLink < ActiveRecord::Base end def copy_to(project, folder) - link = DmsfLink.new( - :target_project_id => self.target_project_id, - :target_id => self.target_id, - :target_type => self.target_type, - :name => self.name, - :external_url => self.external_url, - :project_id => project.id, - :dmsf_folder_id => folder ? folder.id : nil) + link = DmsfLink.new + link.target_project_id = self.target_project_id + link.target_id = self.target_id + link.target_type = self.target_type + link.name = self.name + link.external_url = self.external_url + link.project_id = project.id + link.dmsf_folder_id = folder ? folder.id : nil link.save link end diff --git a/db/migrate/20170418104901_migrate_documents.rb b/db/migrate/20170418104901_migrate_documents.rb new file mode 100644 index 00000000..1165f7aa --- /dev/null +++ b/db/migrate/20170418104901_migrate_documents.rb @@ -0,0 +1,120 @@ +# encoding: utf-8 +# +# Redmine plugin for Document Management System "Features" +# +# Copyright (C) 2011-17 Karel Pičman +# +# 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 2 +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +class MigrateDocuments < ActiveRecord::Migration + + def up + # Migrate all documents from dmsf/p_{project identifier} to dmsf/{year}/{month} + DmsfFileRevision.find_each do |dmsf_file_revision| + if dmsf_file_revision.dmsf_file + if dmsf_file_revision.dmsf_file.project + origin = self.disk_file(dmsf_file_revision) + if origin + if File.exist?(origin) + target = dmsf_file_revision.disk_file + if target + unless File.exist?(target) + begin + FileUtils.mv origin, target, :verbose => true + folder = self.storage_base_path(dmsf_file_revision) + Dir.rmdir(folder) if (folder && (Dir.entries(folder).size == 2)) + rescue Exception => e + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: #{e.message}" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: Target '#{target}' exists" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: target = nil" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: Origin '#{origin}' doesn't exist" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: disk_file = nil" + end + else + Rails.logger.error "DmsfFile ID #{dmsf_file_revision.dmsf_file.id}: project = nil" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: dmsf_file = nil" + end + end + end + + def down + # Migrate all documents from dmsf/p_{project identifier} to dmsf/{year}/{month} + DmsfFileRevision.find_each do |dmsf_file_revision| + if dmsf_file_revision.dmsf_file + if dmsf_file_revision.dmsf_file.project + origin = dmsf_file_revision.disk_file + if origin + if File.exist?(origin) + target = self.disk_file(dmsf_file_revision) + if target + unless File.exist?(target) + begin + FileUtils.mv origin, target, :verbose => true + folder = dmsf_file_revision.storage_base_path + Dir.rmdir(folder) if (folder && (Dir.entries(folder).size == 2)) + rescue Exception => e + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: #{e.message}" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: Target '#{target}' exists" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: target = nil" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: Origin '#{origin}' doesn't exist" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: disk_file = nil" + end + else + Rails.logger.error "DmsfFile ID #{dmsf_file_revision.dmsf_file.id}: project = nil" + end + else + Rails.logger.error "DmsfFileRevisions ID #{dmsf_file_revision.id}: dmsf_file = nil" + end + end + end + + def storage_base_path(dmsf_file_revision) + if dmsf_file_revision.dmsf_file + if dmsf_file_revision.dmsf_file.project + project_base = dmsf_file_revision.dmsf_file.project.identifier.gsub(/[^\w\.\-]/,'_') + return "#{DmsfFile.storage_path}/p_#{project_base}" + end + end + nil + end + + def disk_file(dmsf_file_revision) + path = storage_base_path(dmsf_file_revision) + if path + FileUtils.mkdir_p(path) unless File.exist?(path) + return "#{path}/#{dmsf_file_revision.disk_filename}" + end + nil + end + +end diff --git a/test/fixtures/dmsf_file_revisions.yml b/test/fixtures/dmsf_file_revisions.yml index c0f0dc1f..6c2f3fcd 100644 --- a/test/fixtures/dmsf_file_revisions.yml +++ b/test/fixtures/dmsf_file_revisions.yml @@ -17,7 +17,8 @@ dmsf_file_revisions_001: deleted_by_user_id: NULL user_id: 1 dmsf_workflow_assigned_by: 1 - dmsf_workflow_started_by: 1 + dmsf_workflow_started_by: 1 + created_at: 2017-04-18 14:52:27 +02:00 #revision for file on non-enabled project dmsf_file_revisions_002: @@ -38,7 +39,8 @@ dmsf_file_revisions_002: deleted_by_user_id: NULL user_id: 1 dmsf_workflow_assigned_by: 1 - dmsf_workflow_started_by: 1 + dmsf_workflow_started_by: 1 + created_at: 2017-04-18 14:52:27 +02:00 #revision for deleted file on dmsf-enabled project dmsf_file_revisions_003: @@ -59,7 +61,8 @@ dmsf_file_revisions_003: deleted_by_user_id: 1 user_id: 1 dmsf_workflow_assigned_by: 1 - dmsf_workflow_started_by: 1 + dmsf_workflow_started_by: 1 + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_004: id: 4 @@ -79,7 +82,8 @@ dmsf_file_revisions_004: deleted_by_user_id: NULL user_id: 1 dmsf_workflow_assigned_by: NULL - dmsf_workflow_started_by: NULL + dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_005: id: 5 @@ -100,6 +104,7 @@ dmsf_file_revisions_005: user_id: 1 dmsf_workflow_assigned_by: NULL dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_006: id: 6 @@ -120,6 +125,7 @@ dmsf_file_revisions_006: user_id: 1 dmsf_workflow_assigned_by: NULL dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_007: id: 7 @@ -140,6 +146,7 @@ dmsf_file_revisions_007: user_id: 1 dmsf_workflow_assigned_by: NULL dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_008: id: 8 @@ -160,6 +167,7 @@ dmsf_file_revisions_008: user_id: 1 dmsf_workflow_assigned_by: NULL dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 dmsf_file_revisions_009: id: 9 @@ -180,5 +188,6 @@ dmsf_file_revisions_009: user_id: 1 dmsf_workflow_assigned_by: NULL dmsf_workflow_started_by: NULL + created_at: 2017-04-18 14:52:27 +02:00 diff --git a/test/fixtures/files/p_ecookbook/test.gif b/test/fixtures/files/2017/04/test.gif similarity index 100% rename from test/fixtures/files/p_ecookbook/test.gif rename to test/fixtures/files/2017/04/test.gif diff --git a/test/fixtures/files/p_ecookbook/test.pdf b/test/fixtures/files/2017/04/test.pdf similarity index 100% rename from test/fixtures/files/p_ecookbook/test.pdf rename to test/fixtures/files/2017/04/test.pdf diff --git a/test/fixtures/files/p_ecookbook/test.txt b/test/fixtures/files/2017/04/test.txt similarity index 100% rename from test/fixtures/files/p_ecookbook/test.txt rename to test/fixtures/files/2017/04/test.txt diff --git a/test/fixtures/files/p_onlinestore/test.txt b/test/fixtures/files/p_onlinestore/test.txt deleted file mode 100644 index 274c0052..00000000 --- a/test/fixtures/files/p_onlinestore/test.txt +++ /dev/null @@ -1 +0,0 @@ -1234 \ No newline at end of file diff --git a/test/unit/dmsf_folder_permission_test.rb b/test/unit/dmsf_folder_permission_test.rb index 692e9a8b..2bb8bc7e 100644 --- a/test/unit/dmsf_folder_permission_test.rb +++ b/test/unit/dmsf_folder_permission_test.rb @@ -21,7 +21,17 @@ require File.expand_path('../../test_helper', __FILE__) class DmsfFolderPermissionTest < RedmineDmsf::Test::UnitTest - fixtures :dmsf_folder_permissions + fixtures :dmsf_folder_permissions, :dmsf_folders + + def setup + @folder1 = DmsfFolder.find 1 + @permission1 = DmsfFolderPermission.find 1 + end + + def test_truth + assert_kind_of DmsfFolder, @folder1 + assert_kind_of DmsfFolderPermission, @permission1 + end def test_scope assert_equal 2, DmsfFolderPermission.count @@ -35,4 +45,12 @@ class DmsfFolderPermissionTest < RedmineDmsf::Test::UnitTest assert_equal 1, DmsfFolderPermission.roles.count end + def test_copy_to + permission = @permission1.copy_to(@folder1) + assert permission + assert_equal @folder1.id, permission.dmsf_folder_id + assert_equal @permission1.object_id, permission.object_id + assert_equal @permission1.object_type, permission.object_type + end + end diff --git a/test/unit/dmsf_folder_test.rb b/test/unit/dmsf_folder_test.rb index 925c691a..b1e1b2e5 100644 --- a/test/unit/dmsf_folder_test.rb +++ b/test/unit/dmsf_folder_test.rb @@ -34,6 +34,10 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest @folder7 = DmsfFolder.find_by_id 7 @manager = User.find_by_id 2 @developer = User.find_by_id 3 + manager_role = Role.find 1 + manager_role.add_permission! :view_dmsf_folders + developer_role = Role.find 2 + developer_role.add_permission! :view_dmsf_folders end def test_truth @@ -49,13 +53,14 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest def test_visiblity # The role has got permissions User.current = @manager - assert_equal 6, DmsfFolder.visible.where(:project_id => 1).count + assert_equal 5, DmsfFolder.where(:project_id => 1).count + assert_equal 5, DmsfFolder.visible.where(:project_id => 1).count # The user has got permissions User.current = @developer - assert_equal 6, DmsfFolder.visible.where(:project_id => 1).count + assert_equal 5, DmsfFolder.visible.where(:project_id => 1).count # Hasn't got permissions for @folder7 @folder7.dmsf_folder_permissions.where(:object_type => 'User').delete_all - assert_equal 5, DmsfFolder.visible.where(:project_id => 1).count + assert_equal 4, DmsfFolder.visible.where(:project_id => 1).count end def test_permissions @@ -65,22 +70,22 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest @folder7.reload assert !DmsfFolder.permissions(@folder7) end - - def test_delete + + def test_delete assert @folder6.delete(false), @folder6.errors.full_messages.to_sentence assert @folder6.deleted?, "Folder #{@folder6} hasn't been deleted" end - - def test_restore + + def test_restore assert @folder6.delete(false), @folder6.errors.full_messages.to_sentence assert @folder6.deleted?, "Folder #{@folder6} hasn't been deleted" assert @folder6.restore, @folder6.errors.full_messages.to_sentence assert !@folder6.deleted?, "Folder #{@folder6} hasn't been restored" end - - def test_destroy + + def test_destroy @folder6.delete true - assert_nil DmsfFolder.find_by_id(@folder6.id) + assert_nil DmsfFolder.find_by_id(@folder6.id) end def test_is_column_on_default @@ -94,7 +99,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest assert !DmsfFolder.is_column_on?(column), "The column #{column} is on?" end end - + def test_get_column_position_default # 0 - checkbox assert_nil DmsfFolder.get_column_position('checkbox'), "The column 'checkbox' is on?" @@ -133,7 +138,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest def test_save_and_destroy_with_cache RedmineDmsf::Webdav::Cache.init_testcache - + # save cache_key = @folder4.propfind_cache_key RedmineDmsf::Webdav::Cache.write(cache_key, "") @@ -143,7 +148,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest assert !RedmineDmsf::Webdav::Cache.exist?(cache_key) assert RedmineDmsf::Webdav::Cache.exist?("#{cache_key}.invalid") RedmineDmsf::Webdav::Cache.delete("#{cache_key}.invalid") - + # destroy RedmineDmsf::Webdav::Cache.write(cache_key, "") assert RedmineDmsf::Webdav::Cache.exist?(cache_key) @@ -152,7 +157,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest assert !RedmineDmsf::Webdav::Cache.exist?(cache_key) assert RedmineDmsf::Webdav::Cache.exist?("#{cache_key}.invalid") RedmineDmsf::Webdav::Cache.cache.clear - + # save! cache_key = @folder5.propfind_cache_key RedmineDmsf::Webdav::Cache.write(cache_key, "") @@ -162,7 +167,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest assert !RedmineDmsf::Webdav::Cache.exist?(cache_key) assert RedmineDmsf::Webdav::Cache.exist?("#{cache_key}.invalid") RedmineDmsf::Webdav::Cache.delete("#{cache_key}.invalid") - + # destroy! RedmineDmsf::Webdav::Cache.write(cache_key, "") assert RedmineDmsf::Webdav::Cache.exist?(cache_key) @@ -170,7 +175,7 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest @folder5.destroy! assert !RedmineDmsf::Webdav::Cache.exist?(cache_key) assert RedmineDmsf::Webdav::Cache.exist?("#{cache_key}.invalid") - + RedmineDmsf::Webdav::Cache.init_nullcache end diff --git a/test/unit/project_patch_test.rb b/test/unit/project_patch_test.rb index e7b3ba75..3a7ac473 100644 --- a/test/unit/project_patch_test.rb +++ b/test/unit/project_patch_test.rb @@ -27,6 +27,8 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest @project1 = Project.find_by_id 1 @project2 = Project.find_by_id 2 @project3 = Project.find_by_id 3 + admin = User.find 1 + User.current = admin end def test_truth @@ -66,7 +68,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest def test_dmsf_count hash = @project1.dmsf_count assert_equal 7, hash[:files] - assert_equal 7, hash[:folders] + assert_equal 6, hash[:folders] end def test_copy_approval_workflows @@ -78,18 +80,18 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest def test_copy_dmsf assert_equal 3, @project1.dmsf_files.visible.count - assert_equal 4, @project1.dmsf_folders.visible.count + assert_equal 3, @project1.dmsf_folders.visible.count assert_equal 1, @project1.file_links.visible.count assert_equal 1, @project1.folder_links.visible.count assert_equal 1, @project1.url_links.visible.count assert_equal 0, @project3.dmsf_files.visible.count - assert_equal 0, @project3.dmsf_folders.visible.count + assert_equal 0, @project3.dmsf_folders.count assert_equal 0, @project3.file_links.visible.count assert_equal 0, @project3.folder_links.visible.count assert_equal 0, @project3.url_links.visible.count @project3.copy_dmsf(@project1) assert_equal 3, @project3.dmsf_files.visible.count - assert_equal 3, @project3.dmsf_folders.visible.count # Folder if 7 is not visible due to folder permissions + assert_equal 3, @project3.dmsf_folders.count assert_equal 1, @project3.file_links.visible.count assert_equal 1, @project3.folder_links.visible.count assert_equal 1, @project3.url_links.visible.count