From 3a5048565329dbbbf7a545d49769bf0de7d728cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Pi=C4=8Dman?= Date: Fri, 5 Dec 2025 09:22:09 +0100 Subject: [PATCH] #9 Active Storage - Duplicated columns removed --- app/controllers/dmsf_files_controller.rb | 15 +- .../dmsf_public_urls_controller.rb | 2 +- app/helpers/dmsf_upload_helper.rb | 5 +- app/models/dmsf_file.rb | 116 ++++++------ app/models/dmsf_file_revision.rb | 59 +----- app/models/dmsf_folder.rb | 6 +- app/models/dmsf_link.rb | 4 +- app/validators/dmsf_file_name_validator.rb | 32 ++++ app/validators/dmsf_folder_name_validator.rb | 44 +++++ app/validators/dmsf_link_name_validator.rb | 44 +++++ app/views/dmsf_context_menus/_file.html.erb | 2 +- .../dmsf_context_menus/_revisions.html.erb | 3 +- app/views/dmsf_files/_link.html.erb | 2 +- app/views/dmsf_files/show.api.rsb | 2 +- app/views/dmsf_files/show.html.erb | 2 +- app/views/layouts/_document.html.erb | 3 +- ...20251015130601_active_storage_migration.rb | 52 +++++- .../20251017101001_restore_updated_at.rb | 27 --- ...131001_remove_duplicities_from_revision.rb | 34 ---- .../dmsf_file_revision_format.rb | 2 +- lib/redmine_dmsf/macros.rb | 4 +- lib/redmine_dmsf/patches/pdf_patch.rb | 2 +- lib/redmine_dmsf/patches/project_patch.rb | 4 +- lib/redmine_dmsf/webdav/dmsf_resource.rb | 37 ++-- lib/redmine_dmsf/webdav/project_resource.rb | 4 +- test/fixtures/active_storage_attachments.yml | 16 +- test/fixtures/active_storage_blobs.yml | 11 ++ test/fixtures/dmsf_file_revisions.yml | 35 ++-- test/fixtures/dmsf_files.yml | 20 +- .../files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14 | 6 + test/functional/dmsf_files_controller_test.rb | 6 +- .../rest_api/dmsf_file_api_test.rb | 6 +- .../webdav/dmsf_webdav_get_test.rb | 4 +- .../webdav/dmsf_webdav_move_test.rb | 2 +- .../webdav/dmsf_webdav_put_test.rb | 8 +- test/unit/dmsf_file_revision_test.rb | 174 +++++++++++------- test/unit/dmsf_file_test.rb | 37 ++-- test/unit/dmsf_folder_test.rb | 55 ++++++ test/unit/dmsf_link_test.rb | 70 +++++-- test/unit/dmsf_query_test.rb | 12 +- .../unit/lib/redmine_dmsf/dmsf_macros_test.rb | 8 +- test/unit/project_patch_test.rb | 6 +- test/unit_test.rb | 2 + 43 files changed, 587 insertions(+), 398 deletions(-) create mode 100644 app/validators/dmsf_folder_name_validator.rb create mode 100644 app/validators/dmsf_link_name_validator.rb delete mode 100644 db/migrate/20251017101001_restore_updated_at.rb delete mode 100644 db/migrate/20251128131001_remove_duplicities_from_revision.rb create mode 100644 test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14 diff --git a/app/controllers/dmsf_files_controller.rb b/app/controllers/dmsf_files_controller.rb index 81e0cbeb..b498fdfa 100644 --- a/app/controllers/dmsf_files_controller.rb +++ b/app/controllers/dmsf_files_controller.rb @@ -67,7 +67,7 @@ class DmsfFilesController < ApplicationController Rails.logger.error "Could not send email notifications: #{e.message}" end # Allow a preview of the file by an external plugin - results = call_hook(:dmsf_files_controller_before_view, { file: @revision.disk_file }) + results = call_hook(:dmsf_files_controller_before_view, { file: @revision.file.download }) return if results.first == true member = Member.find_by(user_id: User.current.id, project_id: @file.project.id) @@ -89,7 +89,7 @@ class DmsfFilesController < ApplicationController params[:disposition] = 'attachment' if params[:filename].present? send_data @revision.file.download, filename: filename, - type: @revision.detect_content_type, + type: @revision.content_type, disposition: params[:disposition].presence || @revision.dmsf_file.disposition end rescue DmsfAccessError => e @@ -139,21 +139,18 @@ class DmsfFilesController < ApplicationController upload = DmsfUpload.create_from_uploaded_attachment(@project, @folder, file_upload) if upload revision.size = upload.size - revision.disk_filename = revision.new_storage_filename revision.file.attach( io: File.open(upload.tempfile_path), - filename: revision.disk_filename, - content_type: revision.mime_type, + filename: file_upload.filename, + content_type: Redmine::MimeType.of(file_upload.filename), identify: false ) end else revision.size = last_revision.size - revision.disk_filename = last_revision.disk_filename end # Custom fields revision.copy_custom_field_values(params[:dmsf_file_revision][:custom_field_values], last_revision) - @file.name = revision.name ok = true if revision.save revision.assign_workflow params[:dmsf_workflow_id] @@ -330,8 +327,8 @@ class DmsfFilesController < ApplicationController if tbnail if stale?(etag: tbnail) send_file tbnail, - filename: filename_for_content_disposition(@file.last_revision.disk_file), - type: @file.last_revision.detect_content_type, + filename: filename_for_content_disposition(@file.name), + type: @file.last_revision.content_type, disposition: 'inline' end else diff --git a/app/controllers/dmsf_public_urls_controller.rb b/app/controllers/dmsf_public_urls_controller.rb index fee9066b..fa4e9804 100644 --- a/app/controllers/dmsf_public_urls_controller.rb +++ b/app/controllers/dmsf_public_urls_controller.rb @@ -32,7 +32,7 @@ class DmsfPublicUrlsController < ApplicationController expires_in 0.years, 'must-revalidate' => true send_data(revision.file.download, filename: filename_for_content_disposition(revision.name), - type: revision.detect_content_type, + type: revision.content_type, disposition: dmsf_public_url.dmsf_file.disposition) rescue StandardError => e Rails.logger.error e.message diff --git a/app/helpers/dmsf_upload_helper.rb b/app/helpers/dmsf_upload_helper.rb index 7ab36977..afb01005 100644 --- a/app/helpers/dmsf_upload_helper.rb +++ b/app/helpers/dmsf_upload_helper.rb @@ -42,7 +42,6 @@ module DmsfUploadHelper else file = DmsfFile.new file.project_id = project.id - file.name = name file.dmsf_folder = folder file.notification = RedmineDmsf.dmsf_default_notifications? end @@ -84,8 +83,6 @@ module DmsfUploadHelper next end - new_revision.disk_filename = new_revision.new_storage_filename - if new_revision.save new_revision.assign_workflow committed_file[:dmsf_workflow_id] begin @@ -97,7 +94,7 @@ module DmsfUploadHelper new_revision.file.attach( io: File.open(committed_file[:tempfile_path]), filename: new_revision.name, - content_type: new_revision.mime_type, + content_type: new_revision.content_type, identify: false ) file.last_revision = new_revision diff --git a/app/models/dmsf_file.rb b/app/models/dmsf_file.rb index d4d0c15f..78ef9396 100644 --- a/app/models/dmsf_file.rb +++ b/app/models/dmsf_file.rb @@ -42,15 +42,6 @@ class DmsfFile < ApplicationRecord scope :visible, -> { where(deleted: STATUS_ACTIVE) } scope :deleted, -> { where(deleted: STATUS_DELETED) } - validates :name, dmsf_file_name: true - validates :name, length: { maximum: 255 } - validates :name, - uniqueness: { - scope: %i[dmsf_folder_id project_id deleted], - conditions: -> { where(deleted: STATUS_ACTIVE) }, - case_sensitive: true - } - acts_as_event( title: proc { |o| @searched_revision = nil @@ -81,7 +72,7 @@ class DmsfFile < ApplicationRecord url: proc { |o| if @searched_revision { controller: 'dmsf_files', action: 'view', id: o.id, download: @searched_revision.id, - filename: o.name } + filename: @searched_revision.name } else { controller: 'dmsf_files', action: 'view', id: o.id, filename: o.name } end @@ -104,7 +95,7 @@ class DmsfFile < ApplicationRecord acts_as_watchable acts_as_searchable( columns: [ - "#{table_name}.name", + "#{DmsfFileRevision.table_name}.name", "#{DmsfFileRevision.table_name}.title", "#{DmsfFileRevision.table_name}.description", "#{DmsfFileRevision.table_name}.comment" @@ -143,11 +134,19 @@ class DmsfFile < ApplicationRecord end def self.find_file_by_name(project, folder, name) - findn_file_by_name project&.id, folder, name + dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id }) + dmsf_files.each do |file| + return file if file.name == name + end + nil end - def self.findn_file_by_name(project_id, folder, name) - visible.find_by project_id: project_id, dmsf_folder_id: folder&.id, name: name + def self.find_file_by_title(project, folder, name) + dmsf_files = visible.where(dmsf_files: { project_id: project&.id, dmsf_folder_id: folder&.id }) + dmsf_files.each do |file| + return file if file.title == name + end + nil end def approval_allowed_zero_minor @@ -155,10 +154,7 @@ class DmsfFile < ApplicationRecord end def last_revision - unless defined?(@last_revision) - @last_revision = deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first - end - @last_revision + @last_revision ||= deleted? ? dmsf_file_revisions.first : dmsf_file_revisions.visible.first end def deleted? @@ -211,16 +207,20 @@ class DmsfFile < ApplicationRecord save end + def name + last_revision&.name.to_s + end + def title - last_revision ? last_revision.title : name + last_revision&.title.to_s end def description - last_revision ? last_revision.description : '' + last_revision&.description.to_s end def version - last_revision ? last_revision.version : '0' + last_revision&.version.to_s end def workflow @@ -228,7 +228,7 @@ class DmsfFile < ApplicationRecord end def size - last_revision ? last_revision.size : 0 + last_revision&.size.to_i end def dmsf_path @@ -313,28 +313,12 @@ class DmsfFile < ApplicationRecord file = DmsfFile.new file.dmsf_folder_id = folder.id if folder file.project_id = project.id - title = last_revision&.title - if DmsfFile.visible.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, name: filename) - basename = File.basename(filename, '.*') - extname = File.extname(filename) - 1.step do |i| - title = "#{basename} (#{i})" - gen_filename = "#{title}#{extname}" - unless DmsfFile.visible.exists?(project_id: file.project_id, dmsf_folder_id: file.dmsf_folder_id, - name: gen_filename) - filename = gen_filename - break - end - end - end - file.name = filename file.notification = RedmineDmsf.dmsf_default_notifications? if file.save && last_revision new_revision = last_revision.clone new_revision.name = filename - new_revision.title = title + new_revision.title = File.basename(filename, '.*') new_revision.dmsf_file = file - new_revision.disk_filename = new_revision.new_storage_filename # Assign the same workflow if it's a global one, or we are in the same project new_revision.workflow = nil new_revision.dmsf_workflow_id = nil @@ -350,7 +334,7 @@ class DmsfFile < ApplicationRecord if last_revision.file.attached? begin new_revision.file.attach( - io: StringIO.new(last_revision.file.blob.download), + io: StringIO.new(last_revision.file.download), filename: filename, content_type: new_revision.file.content_type, identify: false @@ -367,10 +351,18 @@ class DmsfFile < ApplicationRecord v.value = cv.value new_revision.custom_values << v end + # Check the name and title + basename = File.basename(filename, '.*') + extname = File.extname(filename) + i = 1 + while new_revision.invalid? && i < 1_000 + new_revision.title = "#{basename} (#{i})" + new_revision.name = "#{new_revision.title}#{extname}" + i += 1 + end if new_revision.save file.last_revision = new_revision else - errors.add :base, new_revision.errors.full_messages.to_sentence Rails.logger.error new_revision.errors.full_messages.to_sentence file.delete commit: true file = nil @@ -504,29 +496,34 @@ class DmsfFile < ApplicationRecord end def text? - filename = last_revision&.disk_filename - Redmine::MimeType.is_type?('text', filename) || - Redmine::SyntaxHighlighting.filename_supported?(filename) + return false unless last_revision + + filename = last_revision.file&.blob&.filename.to_s + last_revision.file&.blob&.text? || Redmine::SyntaxHighlighting.filename_supported?(filename) end def image? - Redmine::MimeType.is_type?('image', last_revision&.disk_filename) + last_revision && last_revision.file&.blob&.image? end def pdf? - Redmine::MimeType.of(last_revision&.disk_filename) == 'application/pdf' + last_revision&.content_type == 'application/pdf' end def video? - Redmine::MimeType.is_type?('video', last_revision&.disk_filename) + return false unless last_revision + + Redmine::MimeType.is_type?('video', last_revision.file.blob&.filename&.to_s) end def html? - Redmine::MimeType.of(last_revision&.disk_filename) == 'text/html' + last_revision&.content_type == 'text/html' end def office_doc? - case File.extname(last_revision&.disk_filename) + return false unless last_revision + + case File.extname(last_revision.file.blob&.filename&.to_s) when '.odt', '.ods', '.odp', '.odg', # LibreOffice '.doc', '.docx', '.docm', '.xls', '.xlsx', '.xlsm', '.ppt', '.pptx', '.pptm', # MS Office '.rtf' # Universal @@ -537,11 +534,11 @@ class DmsfFile < ApplicationRecord end def markdown? - Redmine::MimeType.of(last_revision&.disk_filename) == 'text/markdown' + last_revision&.content_type == 'text/markdown' end def textile? - Redmine::MimeType.of(last_revision&.disk_filename) == 'text/x-textile' + last_revision&.content_type == 'text/textile' end def disposition @@ -573,8 +570,7 @@ class DmsfFile < ApplicationRecord end rescue StandardError => e Rails.logger.error do - %(An error occurred while generating preview for #{last_revision.file.name} to #{target}\n - Exception was: #{e.message}) + %(An error occurred while generating preview for #{name} to #{target}\nException was: #{e.message}) end '' end @@ -604,11 +600,7 @@ class DmsfFile < ApplicationRecord end def formatted_name(member) - if last_revision - last_revision.formatted_name(member) - else - name - end + last_revision&.formatted_name(member) end def owner?(user) @@ -640,10 +632,6 @@ class DmsfFile < ApplicationRecord nil end - def extension - File.extname(last_revision.disk_filename).strip.downcase[1..] if last_revision - end - def thumbnail(options = {}) size = options[:size].to_i if size.positive? @@ -657,10 +645,10 @@ class DmsfFile < ApplicationRecord size = 100 unless size.positive? target = File.join(Attachment.thumbnails_storage_path, "#{id}_#{last_revision.digest}_#{size}.thumb") begin - Redmine::Thumbnail.generate last_revision.disk_file.to_s, target, size, pdf? + Redmine::Thumbnail.generate last_revision.file.download, target, size, pdf? rescue StandardError => e Rails.logger.error do - %(An error occured while generating thumbnail for #{last_revision.disk_file} to #{target}\n + %(An error occured while generating thumbnail for #{last_revision.file&.blob&.filename} to #{target}\n Exception was: #{e.message}) end nil diff --git a/app/models/dmsf_file_revision.rb b/app/models/dmsf_file_revision.rb index da7231d6..34e04f4e 100644 --- a/app/models/dmsf_file_revision.rb +++ b/app/models/dmsf_file_revision.rb @@ -90,13 +90,9 @@ class DmsfFileRevision < ApplicationRecord } ) - validates :title, presence: true - validates :title, length: { maximum: 255 } + validates :title, presence: true, dmsf_file_name: true, length: { maximum: 255 } validates :major_version, presence: true - validates :name, dmsf_file_name: true - validates :name, length: { maximum: 255 } - validates :disk_filename, length: { maximum: 255 } - validates :name, dmsf_file_extension: true + validates :name, presence: true, dmsf_file_name: true, length: { maximum: 255 }, dmsf_file_extension: true validates :description, length: { maximum: 1.kilobyte } validates :size, dmsf_max_file_size: true @@ -118,8 +114,11 @@ class DmsfFileRevision < ApplicationRecord file.blob&.checksum end - def mime_type - file.blob&.content_type + def content_type + res = file.blob&.content_type + res = Redmine::MimeType.of(file.blob&.filename) if res.blank? + res = 'application/octet-stream' if res.blank? + res end def visible?(_user = nil) @@ -191,49 +190,9 @@ class DmsfFileRevision < ApplicationRecord ver end - def storage_base_path - time = created_at || DateTime.current - DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m') - end - - def disk_file(search_if_not_exists: true) - path = storage_base_path - begin - FileUtils.mkdir_p(path) - rescue StandardError => e - Rails.logger.error e.message - end - filename = path.join(disk_filename) - if search_if_not_exists && !File.exist?(filename) - # Let's search for the physical file in source revisions - dmsf_file.dmsf_file_revisions.where(created_at: ...created_at).order(created_at: :desc).each do |rev| - filename = rev.disk_file - break if File.exist?(filename) - end - end - filename.to_s - end - - def new_storage_filename - raise DmsfAccessError, 'File id is not set' unless dmsf_file&.id - - filename = DmsfHelper.sanitize_filename(name) - timestamp = DateTime.current.strftime('%y%m%d%H%M%S') - timestamp.succ! while File.exist? storage_base_path.join("#{timestamp}_#{dmsf_file.id}_#{filename}") - "#{timestamp}_#{dmsf_file.id}_#{filename}" - end - - def detect_content_type - content_type = mime_type - content_type = Redmine::MimeType.of(disk_filename) if content_type.blank? - content_type = 'application/octet-stream' if content_type.blank? - content_type - end - def clone new_revision = DmsfFileRevision.new new_revision.dmsf_file = dmsf_file - new_revision.disk_filename = disk_filename new_revision.size = size new_revision.title = title new_revision.description = description @@ -325,7 +284,7 @@ class DmsfFileRevision < ApplicationRecord file.attach( io: open_file, filename: dmsf_file.name, - content_type: mime_type.presence || Redmine::MimeType.of(disk_filename), + content_type: content_type, identify: false ) end @@ -396,7 +355,7 @@ class DmsfFileRevision < ApplicationRecord end def protocol - @protocol ||= PROTOCOLS[mime_type.downcase] if mime_type + @protocol ||= PROTOCOLS[content_type.downcase] if content_type.present? @protocol end diff --git a/app/models/dmsf_folder.rb b/app/models/dmsf_folder.rb index eb6fdeee..92ae68f3 100644 --- a/app/models/dmsf_folder.rb +++ b/app/models/dmsf_folder.rb @@ -28,9 +28,9 @@ class DmsfFolder < ApplicationRecord belongs_to :deleted_by_user, class_name: 'User' belongs_to :user - has_many :dmsf_folders, -> { order :title }, dependent: :destroy, inverse_of: :dmsf_folder + has_many :dmsf_folders, dependent: :destroy, inverse_of: :dmsf_folder has_many :dmsf_files, dependent: :destroy - has_many :folder_links, -> { where(target_type: 'DmsfFolder').order(:name) }, + has_many :folder_links, -> { where(target_type: 'DmsfFolder') }, class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder has_many :file_links, -> { where(target_type: 'DmsfFile') }, class_name: 'DmsfLink', foreign_key: 'dmsf_folder_id', dependent: :destroy, inverse_of: :dmsf_folder @@ -91,7 +91,7 @@ class DmsfFolder < ApplicationRecord datetime: proc { |o| o.updated_at }, author: proc { |o| o.user } - validates :title, presence: true, dmsf_file_name: true + validates :title, presence: true, length: { maximum: 255 }, dmsf_folder_name: true validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted], conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true } validates :description, length: { maximum: 65_535 } diff --git a/app/models/dmsf_link.rb b/app/models/dmsf_link.rb index 3ed1789b..50bfe2e9 100644 --- a/app/models/dmsf_link.rb +++ b/app/models/dmsf_link.rb @@ -26,7 +26,9 @@ class DmsfLink < ApplicationRecord belongs_to :deleted_by_user, class_name: 'User' belongs_to :user - validates :name, presence: true, length: { maximum: 255 } + validates :name, presence: true, length: { maximum: 255 }, dmsf_link_name: true + validates :name, uniqueness: { scope: %i[dmsf_folder_id project_id deleted], + conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true } # There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the # issue. validates :external_url, length: { maximum: 255 } diff --git a/app/validators/dmsf_file_name_validator.rb b/app/validators/dmsf_file_name_validator.rb index 0e826998..552e32d5 100644 --- a/app/validators/dmsf_file_name_validator.rb +++ b/app/validators/dmsf_file_name_validator.rb @@ -22,6 +22,38 @@ class DmsfFileNameValidator < ActiveModel::EachValidator ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/ def validate_each(record, attribute, value) + # Check invalid characters record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) + + # Check name uniqueness among files + project_id = record.dmsf_file.project_id + dmsf_folder_id = record.dmsf_file.dmsf_folder_id + id = record.dmsf_file_id + DmsfFile + .visible + .where(project_id: project_id, dmsf_folder_id: dmsf_folder_id) + .where.not(id: id) + .find_each do |file| + if file.name == value || file.title == value + record.errors.add attribute, :taken + break + end + end + + # Check name uniqueness among folders + DmsfFolder.visible.where(project_id: project_id, dmsf_folder_id: dmsf_folder_id).find_each do |folder| + if folder.title == value + record.errors.add attribute, :taken + break + end + end + + # Check name uniqueness among links + DmsfLink.visible.where(project_id: project_id, dmsf_folder_id: dmsf_folder_id).find_each do |link| + if link.name == value + record.errors.add attribute, :taken + break + end + end end end diff --git a/app/validators/dmsf_folder_name_validator.rb b/app/validators/dmsf_folder_name_validator.rb new file mode 100644 index 00000000..8b70035a --- /dev/null +++ b/app/validators/dmsf_folder_name_validator.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# This file is part of Redmine DMSF plugin. +# +# Redmine DMSF plugin 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. +# +# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see +# . + +# File name validator +class DmsfFolderNameValidator < ActiveModel::EachValidator + ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/ + + def validate_each(record, attribute, value) + # Check invalid characters + record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) + + # Check name uniqueness among files + DmsfFile.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |file| + if file.name == value || file.title == value + record.errors.add attribute, :taken + break + end + end + + # Check name uniqueness among links + DmsfLink.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |link| + if link.name == value + record.errors.add attribute, :taken + break + end + end + end +end diff --git a/app/validators/dmsf_link_name_validator.rb b/app/validators/dmsf_link_name_validator.rb new file mode 100644 index 00000000..25be2e0c --- /dev/null +++ b/app/validators/dmsf_link_name_validator.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Redmine plugin for Document Management System "Features" +# +# Vít Jonáš , Karel Pičman +# +# This file is part of Redmine DMSF plugin. +# +# Redmine DMSF plugin 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. +# +# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see +# . + +# File name validator +class DmsfLinkNameValidator < ActiveModel::EachValidator + ALL_INVALID_CHARACTERS = /\A[^#{DmsfFolder::INVALID_CHARACTERS}]*\z/ + + def validate_each(record, attribute, value) + # Check invalid characters + record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) + + # Check name uniqueness among files + DmsfFile.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |file| + if file.name == value || file.title == value + record.errors.add attribute, :taken + break + end + end + + # Check name uniqueness among folders + DmsfFolder.visible.where(project_id: record.project_id, dmsf_folder_id: record.dmsf_folder_id).find_each do |folder| + if folder.title == value + record.errors.add attribute, :taken + break + end + end + end +end diff --git a/app/views/dmsf_context_menus/_file.html.erb b/app/views/dmsf_context_menus/_file.html.erb index e0295785..dd3a53d7 100644 --- a/app/views/dmsf_context_menus/_file.html.erb +++ b/app/views/dmsf_context_menus/_file.html.erb @@ -71,7 +71,7 @@ <% member = Member.find_by(user_id: User.current.id, project_id: dmsf_file.project.id) %> <% filename = dmsf_file.last_revision&.formatted_name(member) %> <%= context_menu_link sprite_icon('download', l(:button_download)), - static_dmsf_file_path(dmsf_file, filename: filename), + static_dmsf_file_path(dmsf_file, filename: filename, download: dmsf_file.last_revision&.id), class: 'icon icon-download', data: { cy: "icon__download--dmsf_file_#{dmsf_file.id}" }, disabled: false %> diff --git a/app/views/dmsf_context_menus/_revisions.html.erb b/app/views/dmsf_context_menus/_revisions.html.erb index add5ddc6..798b73e8 100644 --- a/app/views/dmsf_context_menus/_revisions.html.erb +++ b/app/views/dmsf_context_menus/_revisions.html.erb @@ -40,7 +40,8 @@ <% member = Member.find_by(user_id: User.current.id, project_id: file.project.id) %> <% filename = file.last_revision&.formatted_name(member) %> <%= link_to sprite_icon('download', l(:button_download)), - static_dmsf_file_path(file, filename: filename), class: 'icon icon-download', disabled: false %> + static_dmsf_file_path(file, filename: filename, download: file.last_revision&.id), + class: 'icon icon-download', disabled: false %> <%= render partial: 'dmsf_context_menus/watch', locals: { object: file } %> <%= delete_link(dmsf_file_path(id: file, details: true), back_url: dmsf_folder_path(id: file.project, folder_id: file.dmsf_folder)) if file_delete_allowed %> diff --git a/app/views/dmsf_files/_link.html.erb b/app/views/dmsf_files/_link.html.erb index 7ae88222..adf48b55 100644 --- a/app/views/dmsf_files/_link.html.erb +++ b/app/views/dmsf_files/_link.html.erb @@ -27,7 +27,7 @@ target: '_blank', rel: 'noopener', title: h(dmsf_file.last_revision.try(:tooltip)), - 'data-downloadurl' => "#{dmsf_file.last_revision.detect_content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %> + 'data-downloadurl' => "#{dmsf_file.last_revision.content_type}:#{h(dmsf_file.name)}:#{file_view_url}" %> (<%= number_to_human_size dmsf_file.last_revision.size %>) diff --git a/app/views/dmsf_files/show.api.rsb b/app/views/dmsf_files/show.api.rsb index 0b9d8dec..d4b6ee46 100644 --- a/app/views/dmsf_files/show.api.rsb +++ b/app/views/dmsf_files/show.api.rsb @@ -14,7 +14,7 @@ api.dmsf_file do api.dmsf_string "{{dmsf(#{@file.id},#{@file.name},#{r.id})}}" api.content_url view_dmsf_file_url(@file, download: r) api.size r.size - api.mime_type r.mime_type + api.mime_type r.content_type api.title r.title api.description r.description api.workflow r.workflow diff --git a/app/views/dmsf_files/show.html.erb b/app/views/dmsf_files/show.html.erb index 8b80290d..880aa638 100644 --- a/app/views/dmsf_files/show.html.erb +++ b/app/views/dmsf_files/show.html.erb @@ -133,7 +133,7 @@
<%= content_tag :div, l(:label_mime), class: 'label' %> - <%= content_tag :div, revision.mime_type, class: 'value' %> + <%= content_tag :div, revision.content_type, class: 'value' %>
<% if revision.checksum.present? %>
diff --git a/app/views/layouts/_document.html.erb b/app/views/layouts/_document.html.erb index 8dc26804..47ddebf2 100644 --- a/app/views/layouts/_document.html.erb +++ b/app/views/layouts/_document.html.erb @@ -19,7 +19,8 @@
<%= link_to "#{l(:button_download)} (#{number_to_human_size(@file.size)})", - static_dmsf_file_path(@file, download: @file.last_revision, filename: @file.last_revision.disk_filename), + static_dmsf_file_path(@file, download: @file.last_revision, + filename: @file.last_revision.file&.blob&.filename), class: 'icon icon-download', disabled: false %>
diff --git a/db/migrate/20251015130601_active_storage_migration.rb b/db/migrate/20251015130601_active_storage_migration.rb index 0cc168ed..e844b37e 100644 --- a/db/migrate/20251015130601_active_storage_migration.rb +++ b/db/migrate/20251015130601_active_storage_migration.rb @@ -47,7 +47,7 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0] r.file.attach( io: File.open(path), filename: r.name, - content_type: r.mime_type, + content_type: r.content_type, identify: false ) # Remove the original file @@ -60,18 +60,51 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0] end end end + # Remove columns duplicated in ActiveStorage + remove_column :dmsf_file_revisions, :digest + remove_column :dmsf_file_revisions, :mime_type + remove_column :dmsf_file_revisions, :disk_filename + remove_column :dmsf_files, :name + # We need to keep the size despite the fact that it's duplicated in active_storage_blobs to speed up the main + # document view + # Restore updated_at column + DmsfFileRevision.update_all 'updated_at = temp_updated_at' + remove_column :dmsf_file_revisions, :temp_updated_at $stdout.puts 'Done' end # Active Storage -> File system def down $stdout.puts 'It could be a very long process. Be patient...' + # Restore removed columns + add_column :dmsf_file_revisions, :digest, :string, limit: 64, default: '', null: false + add_column :dmsf_file_revisions, :mime_type, :string + add_column :dmsf_file_revisions, :disk_filename, :string, default: '', null: false + add_column :dmsf_files, :name, :string, default: '', null: false + # Migrate attachments ActiveStorage::Attachment.find_each do |a| r = a.record - new_path = r.disk_file(search_if_not_exists: false) + new_path = disk_file unless File.exist?(new_path) a.blob.open do |f| + # Move the attachment FileUtils.mv f.path, new_path + r.record_timestamps = false # Do not modify updated_at column + DmsfFileRevision.no_touching do + # Mime type + r.mime_type = f.content_type + # Disk filename + r.disk_filename = File.basename(new_path) + # Digest + # We leave the digest calculation to dmsf_create_digests.rake task + r.save + end + r.dmsf_file.record_timestamps = false # Do not modify updated_at column + DmsfFile.no_touching do + # Filename + r.dmsf_file.name = r.dmsf_file.last_revision.name + r.dmsf_file.save + end end key = a.blob.key $stdout.puts "#{File.join(key[0..1], key[2..3], key)} (#{a.blob.filename}) => #{new_path}" @@ -100,4 +133,19 @@ class ActiveStorageMigration < ActiveRecord::Migration[7.0] false end end + + def storage_base_path(dmsf_file_revision) + time = dmsf_file_revision.created_at || DateTime.current + DmsfFile.storage_path.join(time.strftime('%Y')).join time.strftime('%m') + end + + def disk_file(dmsf_file_revision) + path = storage_base_path + begin + FileUtils.mkdir_p path + rescue StandardError => e + Rails.logger.error e.message + end + path.join(dmsf_file_revision.disk_filename).to_s + end end diff --git a/db/migrate/20251017101001_restore_updated_at.rb b/db/migrate/20251017101001_restore_updated_at.rb deleted file mode 100644 index d44b8790..00000000 --- a/db/migrate/20251017101001_restore_updated_at.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -# Redmine plugin for Document Management System "Features" -# -# Karel Pičman -# -# This file is part of Redmine DMSF plugin. -# -# Redmine DMSF plugin 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. -# -# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see -# . - -# Restore DmsfFileRevision.updated_at from DmsfFileRevision.temp_updated_at column -class RestoreUpdatedAt < ActiveRecord::Migration[7.0] - # temp_updated_at => updated_at - def up - DmsfFileRevision.update_all 'updated_at = temp_updated_at' - remove_column :dmsf_file_revisions, :temp_updated_at - end -end diff --git a/db/migrate/20251128131001_remove_duplicities_from_revision.rb b/db/migrate/20251128131001_remove_duplicities_from_revision.rb deleted file mode 100644 index afaf4517..00000000 --- a/db/migrate/20251128131001_remove_duplicities_from_revision.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -# Redmine plugin for Document Management System "Features" -# -# Karel Pičman -# -# This file is part of Redmine DMSF plugin. -# -# Redmine DMSF plugin 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. -# -# Redmine DMSF plugin 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 Redmine DMSF plugin. If not, see -# . - -# Add column -class RemoveDuplicitiesFromRevision < ActiveRecord::Migration[7.0] - def up - remove_column :dmsf_file_revisions, :digest - remove_column :dmsf_file_revisions, :mime_type - # We need to keep the size despite the fact that it's duplicated in active_storage_blobs to speed up the main - # document view - end - - def down - add_column :dmsf_file_revisions, :digest, :string, limit: 64, default: '', null: false - add_column :dmsf_file_revisions, :mime_type, :string - # Recalculation of these columns for all revisions is technically possible but costs are too high. - end -end diff --git a/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb b/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb index 8048b024..d8ff5e86 100644 --- a/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb +++ b/lib/redmine_dmsf/field_formats/dmsf_file_revision_format.rb @@ -91,7 +91,7 @@ module RedmineDmsf rel: 'noopener', class: 'icon icon-file', title: h(revision.try(:tooltip)), - 'data-downloadurl' => "#{revision.detect_content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}" + 'data-downloadurl' => "#{revision.content_type}:#{h(revision.dmsf_file.name)}:#{file_view_url}" ) end end diff --git a/lib/redmine_dmsf/macros.rb b/lib/redmine_dmsf/macros.rb index 55dccadd..391713d4 100644 --- a/lib/redmine_dmsf/macros.rb +++ b/lib/redmine_dmsf/macros.rb @@ -49,7 +49,7 @@ module RedmineDmsf target: '_blank', rel: 'noopener', title: h(revision.tooltip), - 'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}" + 'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}" end # dmsff - link to a folder @@ -278,7 +278,7 @@ module RedmineDmsf target: '_blank', rel: 'noopener', title: h(file.last_revision.try(:tooltip)), - 'data-downloadurl' => "#{file.last_revision.detect_content_type}:#{h(file.name)}:#{url}") + 'data-downloadurl' => "#{file.last_revision.content_type}:#{h(file.name)}:#{url}") end safe_join html end diff --git a/lib/redmine_dmsf/patches/pdf_patch.rb b/lib/redmine_dmsf/patches/pdf_patch.rb index 93e8084a..bb6eed29 100644 --- a/lib/redmine_dmsf/patches/pdf_patch.rb +++ b/lib/redmine_dmsf/patches/pdf_patch.rb @@ -31,7 +31,7 @@ module RedmineDmsf def get_image_filename(attrname) if attrname =~ %r{/dmsf/files/(\d+)/} file = DmsfFile.find_by(id: Regexp.last_match(1)) - file&.last_revision&.disk_file + file.last_revision.file&.blob&.filename if file&.last_revision else super end diff --git a/lib/redmine_dmsf/patches/project_patch.rb b/lib/redmine_dmsf/patches/project_patch.rb index 62a4d826..c8e88c0b 100644 --- a/lib/redmine_dmsf/patches/project_patch.rb +++ b/lib/redmine_dmsf/patches/project_patch.rb @@ -49,9 +49,9 @@ module RedmineDmsf # New methods def self.prepended(base) base.class_eval do - has_many :dmsf_files, -> { where(dmsf_folder_id: nil).order(:name) }, + has_many :dmsf_files, -> { where(dmsf_folder_id: nil) }, class_name: 'DmsfFile', foreign_key: 'project_id', dependent: :destroy - has_many :dmsf_folders, -> { where(dmsf_folder_id: nil).order(:title) }, + has_many :dmsf_folders, -> { where(dmsf_folder_id: nil) }, class_name: 'DmsfFolder', foreign_key: 'project_id', dependent: :destroy has_many :dmsf_workflows, dependent: :destroy has_many :folder_links, -> { where dmsf_folder_id: nil, target_type: 'DmsfFolder' }, diff --git a/lib/redmine_dmsf/webdav/dmsf_resource.rb b/lib/redmine_dmsf/webdav/dmsf_resource.rb index 13bd514d..c47b8bc5 100644 --- a/lib/redmine_dmsf/webdav/dmsf_resource.rb +++ b/lib/redmine_dmsf/webdav/dmsf_resource.rb @@ -55,7 +55,7 @@ module RedmineDmsf end # Gather collection of objects that denote current entities child entities - # Used for listing directories etc, implemented basic caching because otherwise + # Used for listing directories etc., implemented basic caching because otherwise # Our already quite heavy usage of DB would just get silly every time we called # this method. def children @@ -63,12 +63,12 @@ module RedmineDmsf @children = [] if folder # Folders - folder.dmsf_folders.visible.each do |f| - @children.push child(f.title) if DmsfFolder.permissions?(f, allow_system: false) + folder.dmsf_folders.visible.each do |folder| + @children.push child(folder.title) if DmsfFolder.permissions?(folder, allow_system: false) end # Files - folder.dmsf_files.visible.pluck(:name).each do |name| - @children.push child(name) + folder.dmsf_files.visible.each do |file| + @children.push child(file.name) end end end @@ -92,7 +92,7 @@ module RedmineDmsf def content_type if file if file.last_revision - file.last_revision.detect_content_type + file.last_revision.content_type else 'application/octet-stream' end @@ -126,13 +126,7 @@ module RedmineDmsf end def etag - ino = if file&.last_revision && File.exist?(file.last_revision.disk_file) - File.stat(file.last_revision.disk_file).ino - else - 2 - end - format '%x-%x-%x', - node: ino, size: content_length, modified: (last_modified ? last_modified.to_i : 0) + format '%x-%x-%x', node: 0, size: content_length, modified: last_modified.to_i end def content_length @@ -272,10 +266,8 @@ module RedmineDmsf new_revision = dest.resource.file.last_revision.clone new_revision.increase_version DmsfFileRevision::PATCH_VERSION end - # The file on disk must be renamed from .tmp to the correct filetype or else Xapian won't know how to index. - # Copy file.last_revision.disk_file to new_revision.disk_file + # Copy the file new_revision.size = file.last_revision.size - new_revision.disk_filename = new_revision.new_storage_filename new_revision.copy_file_content StringIO.new(file.last_revision.file.download) # Save new_revision.save && dest.resource.file.save @@ -304,7 +296,6 @@ module RedmineDmsf file.last_revision.name = dest.resource.basename file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename) end - file.name = dest.resource.basename # Save Changes if file.last_revision.save && file.save dest.exist? ? NoContent : Created @@ -376,7 +367,6 @@ module RedmineDmsf # Update Revision and names of file (We can link to old physical resource, as it's not changed) new_file.last_revision.name = dest.resource.basename - new_file.name = dest.resource.basename # Save Changes unless new_file.last_revision.save && new_file.save new_file.delete commit: true @@ -587,7 +577,6 @@ module RedmineDmsf else f = DmsfFile.new f.project_id = project.id - f.name = basename f.dmsf_folder = parent.folder f.notification = RedmineDmsf.dmsf_default_notifications? new_revision = DmsfFileRevision.new @@ -627,7 +616,6 @@ module RedmineDmsf raise UnprocessableEntity end - new_revision.disk_filename = new_revision.new_storage_filename unless reuse_revision if new_revision.save if request.body.respond_to?(:rewind) new_revision.copy_file_content request.body @@ -775,7 +763,6 @@ module RedmineDmsf def create_empty_file f = DmsfFile.new f.project_id = project.id - f.name = basename f.dmsf_folder = parent.folder if f.save(validate: false) # Skip validation due to invalid characters in the filename r = DmsfFileRevision.new @@ -786,14 +773,18 @@ module RedmineDmsf r.user = User.current r.name = basename r.size = 0 - r.disk_filename = r.new_storage_filename r.available_custom_fields.each do |cf| # Add default value for CFs not existing next unless cf.default_value r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value }) end if r.save(validate: false) # Skip validation due to invalid characters in the filename - FileUtils.touch r.disk_file(search_if_not_exists: false) + revision.file.attach( + io: File.new(upload.tempfile_path), + filename: file_upload.filename, + content_type: Redmine::MimeType.of(file_upload.filename), + identify: false + ) return f end end diff --git a/lib/redmine_dmsf/webdav/project_resource.rb b/lib/redmine_dmsf/webdav/project_resource.rb index a530f78d..e506a2d8 100644 --- a/lib/redmine_dmsf/webdav/project_resource.rb +++ b/lib/redmine_dmsf/webdav/project_resource.rb @@ -41,8 +41,8 @@ module RedmineDmsf end # Files if User.current.allowed_to?(:view_dmsf_files, project) - project.dmsf_files.visible.pluck(:name).each do |name| - @children.push child(name) + project.dmsf_files.visible.each do |file| + @children.push child(file.name) end end @children diff --git a/test/fixtures/active_storage_attachments.yml b/test/fixtures/active_storage_attachments.yml index 76262387..49558843 100644 --- a/test/fixtures/active_storage_attachments.yml +++ b/test/fixtures/active_storage_attachments.yml @@ -31,14 +31,6 @@ active_storage_attachment_4: blob_id: 4 created_at: <%= Time.now %> -#active_storage_attachment_5: -# id: 5 -# name: 'shared_file' -# record_type: 'DmsfFileRevision' -# record_id: 5 -# blob_id: 5 -# created_at: <%= Time.now %> - active_storage_attachment_6: id: 6 name: 'shared_file' @@ -102,3 +94,11 @@ active_storage_attachment_13: record_id: 13 blob_id: 13 created_at: <%= Time.now %> + +active_storage_attachment_14: + id: 14 + name: 'shared_file' + record_type: 'DmsfFileRevision' + record_id: 14 + blob_id: 14 + created_at: <%= Time.now %> diff --git a/test/fixtures/active_storage_blobs.yml b/test/fixtures/active_storage_blobs.yml index b3523d05..a35b7d8d 100644 --- a/test/fixtures/active_storage_blobs.yml +++ b/test/fixtures/active_storage_blobs.yml @@ -130,3 +130,14 @@ active_storage_blob_13: byte_size: 10179 checksum : 'k08HeKksIVI7PXr1aEVbjg==' created_at: <%= Time.now %> + +active_storage_blob_14: + id: 14 + key: '5lge4yv88jwzt7xl76vri2be1v14' + filename: 'test.html' + content_type: 'text/html' + metadata: '{"identified":true,"analyzed":true}' + service_name: 'test' + byte_size: 10179 + checksum : 'RV3RPuaIjvHzOXpvTLhI3w==' + created_at: <%= Time.now %> diff --git a/test/fixtures/dmsf_file_revisions.yml b/test/fixtures/dmsf_file_revisions.yml index 205d73b2..f63341e2 100644 --- a/test/fixtures/dmsf_file_revisions.yml +++ b/test/fixtures/dmsf_file_revisions.yml @@ -3,8 +3,7 @@ dmsf_file_revisions_001: id: 1 dmsf_file_id: 1 source_dmsf_file_revision_id: NULL - name: "test.txt" - disk_filename: "test.txt" + name: "test.txt" size: 3 title: "Test File" description: 'Some file :-)' @@ -25,7 +24,6 @@ dmsf_file_revisions_002: dmsf_file_id: 2 source_dmsf_file_revision_id: NULL name: "test2.txt" - disk_filename: "test2.txt" size: 3 title: "Test File" description: NULL @@ -46,7 +44,6 @@ dmsf_file_revisions_003: dmsf_file_id: 3 source_dmsf_file_revision_id: NULL name: 'deleted.txt' - disk_filename: 'deleted.txt' size: 3 title: 'Test File' description: NULL @@ -66,7 +63,6 @@ dmsf_file_revisions_004: dmsf_file_id: 4 source_dmsf_file_revision_id: NULL name: 'test4.txt' - disk_filename: 'test4.txt' size: 3 title: 'Test File' description: NULL @@ -86,7 +82,6 @@ dmsf_file_revisions_006: dmsf_file_id: 7 source_dmsf_file_revision_id: NULL name: 'test.gif' - disk_filename: 'test.gif' size: 310 title: 'Test image' description: NULL @@ -106,7 +101,6 @@ dmsf_file_revisions_007: dmsf_file_id: 8 source_dmsf_file_revision_id: NULL name: 'test.pdf' - disk_filename: 'test.pdf' size: 6942 title: 'Test PDF' description: NULL @@ -125,8 +119,7 @@ dmsf_file_revisions_008: id: 8 dmsf_file_id: 9 source_dmsf_file_revision_id: NULL - name: 'myfile.txt' - disk_filename: 'myfile.txt' # The file is not physically present + name: 'myfile.txt' # The file is not physically present size: 0 title: 'My File' description: NULL @@ -146,7 +139,6 @@ dmsf_file_revisions_009: dmsf_file_id: 10 source_dmsf_file_revision_id: NULL name: 'zero.txt' - disk_filename: 'zero.txt' size: 0 title: 'Zero Size File' description: NULL @@ -166,7 +158,6 @@ dmsf_file_revisions_010: dmsf_file_id: 5 source_dmsf_file_revision_id: NULL name: 'test.txt' - disk_filename: 'test.txt' size: 4 title: 'Test File' description: 'Some file :-)' @@ -186,7 +177,6 @@ dmsf_file_revisions_011: dmsf_file_id: 12 source_dmsf_file_revision_id: NULL name: 'test.txt' - disk_filename: 'test.txt' size: 4 title: 'Test File' description: 'Some file :-)' @@ -206,7 +196,6 @@ dmsf_file_revisions_012: dmsf_file_id: 6 source_dmsf_file_revision_id: NULL name: 'test.mp4' - disk_filename: 'test.mp4' size: 4 title: 'test video' description: 'A video :-)' @@ -226,7 +215,6 @@ dmsf_file_revisions_013: dmsf_file_id: 13 source_dmsf_file_revision_id: NULL name: 'test.odt' - disk_filename: 'test.odt' size: 10445 title: 'Test office document' description: 'LibreOffice text' @@ -239,4 +227,23 @@ dmsf_file_revisions_013: user_id: 1 dmsf_workflow_assigned_by_user_id: NULL dmsf_workflow_started_by_user_id: NULL + created_at: 2017-04-01 08:54:00 +02:00 + +dmsf_file_revisions_014: + id: 14 + dmsf_file_id: 14 + source_dmsf_file_revision_id: NULL + name: 'test.html' + size: 10445 + title: 'Webpage' + description: 'HTML document' + workflow: 0 + minor_version: 0 + major_version: 1 + comment: NULL + deleted: 0 + deleted_by_user_id: NULL + user_id: 1 + dmsf_workflow_assigned_by_user_id: NULL + dmsf_workflow_started_by_user_id: NULL created_at: 2017-04-01 08:54:00 +02:00 \ No newline at end of file diff --git a/test/fixtures/dmsf_files.yml b/test/fixtures/dmsf_files.yml index e6e6f86c..e3f5182a 100644 --- a/test/fixtures/dmsf_files.yml +++ b/test/fixtures/dmsf_files.yml @@ -3,7 +3,6 @@ dmsf_files_001: id: 1 project_id: 1 dmsf_folder_id: NULL - name: 'test.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -13,7 +12,6 @@ dmsf_files_002: id: 2 project_id: 2 dmsf_folder_id: NULL - name: 'test.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -23,7 +21,6 @@ dmsf_files_003: id: 3 project_id: 1 dmsf_folder_id: NULL - name: 'deleted.txt' notification: false deleted: 1 deleted_by_user_id: 1 @@ -32,7 +29,6 @@ dmsf_files_004: id: 4 project_id: 1 dmsf_folder_id: 2 - name: 'test.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -41,7 +37,6 @@ dmsf_files_005: id: 5 project_id: 1 dmsf_folder_id: 5 - name: 'test.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -50,7 +45,6 @@ dmsf_files_006: id: 6 project_id: 2 dmsf_folder_id: 3 - name: 'test.mp4' notification: false deleted: 0 deleted_by_user_id: NULL @@ -59,7 +53,6 @@ dmsf_files_007: id: 7 project_id: 1 dmsf_folder_id: 8 - name: 'test.gif' notification: false deleted: 0 deleted_by_user_id: NULL @@ -68,7 +61,6 @@ dmsf_files_008: id: 8 project_id: 1 dmsf_folder_id: NULL - name: 'test.pdf' notification: false deleted: 0 deleted_by_user_id: NULL @@ -77,7 +69,6 @@ dmsf_files_009: id: 9 project_id: 1 dmsf_folder_id: NULL - name: 'myfile.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -86,7 +77,6 @@ dmsf_files_010: id: 10 project_id: 1 dmsf_folder_id: NULL - name: 'zero.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -95,7 +85,6 @@ dmsf_files_011: id: 11 project_id: 1 dmsf_folder_id: 9 - name: 'zero.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -104,7 +93,6 @@ dmsf_files_012: id: 12 project_id: 5 dmsf_folder_id: NULL - name: 'test.txt' notification: false deleted: 0 deleted_by_user_id: NULL @@ -113,6 +101,12 @@ dmsf_files_013: id: 13 project_id: 1 dmsf_folder_id: NULL - name: 'test.odt' + deleted: 0 + deleted_by_user_id: NULL + +dmsf_files_014: + id: 14 + project_id: 1 + dmsf_folder_id: NULL deleted: 0 deleted_by_user_id: NULL \ No newline at end of file diff --git a/test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14 b/test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14 new file mode 100644 index 00000000..09f50aad --- /dev/null +++ b/test/fixtures/files/5l/ge/5lge4yv88jwzt7xl76vri2be1v14 @@ -0,0 +1,6 @@ + + + + Hello world! + + diff --git a/test/functional/dmsf_files_controller_test.rb b/test/functional/dmsf_files_controller_test.rb index d7f6db5c..00a2b163 100644 --- a/test/functional/dmsf_files_controller_test.rb +++ b/test/functional/dmsf_files_controller_test.rb @@ -142,9 +142,9 @@ class DmsfFilesControllerTest < RedmineDmsf::Test::TestCase version_major: @file1.last_revision.major_version, version_minor: @file1.last_revision.minor_version + 1, dmsf_file_revision: { - title: @file1.last_revision.title, - name: @file1.last_revision.name, - description: @file1.last_revision.description, + title: @file1.title, + name: @file1.name, + description: @file1.description, comment: 'New revision' } } diff --git a/test/integration/rest_api/dmsf_file_api_test.rb b/test/integration/rest_api/dmsf_file_api_test.rb index 37a42fb2..6f0779db 100644 --- a/test/integration/rest_api/dmsf_file_api_test.rb +++ b/test/integration/rest_api/dmsf_file_api_test.rb @@ -48,7 +48,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest # test5.txt # http://www.example.com/dmsf/files/1/view?download=5 # 4 - # text/plain + # text/plain # Test File # # 1 @@ -71,7 +71,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest # test.txt # http://www.example.com/dmsf/files/1/view?download=1 # 4 - # text/plain + # text/plain # Test File # Some file :-) # 1 @@ -160,7 +160,7 @@ class DmsfFileApiTest < RedmineDmsf::Test::IntegrationTest assert_response :success revision = DmsfFileRevision.order(:created_at).last assert revision.present? - assert_equal 'text/plain', revision.mime_type + assert_equal 'text/plain', revision.content_type end def test_upload_document_exceeded_attachment_max_size diff --git a/test/integration/webdav/dmsf_webdav_get_test.rb b/test/integration/webdav/dmsf_webdav_get_test.rb index 40b8087b..02baa4e1 100644 --- a/test/integration/webdav/dmsf_webdav_get_test.rb +++ b/test/integration/webdav/dmsf_webdav_get_test.rb @@ -154,11 +154,11 @@ class DmsfWebdavGetTest < RedmineDmsf::Test::IntegrationTest folder = DmsfFolder.find_by(id: 1) assert_not_nil folder assert response.body.match(@folder1.title), - "Expected to find #{folder.title} in return data" + "Expected to find #{folder.title} in the response" file = DmsfFile.find_by(id: 1) assert_not_nil file assert response.body.match(file.name), - "Expected to find #{file.name} in return data" + "Expected to find #{file.name} in the response" end def test_user_assigned_to_project_dmsf_module_not_enabled diff --git a/test/integration/webdav/dmsf_webdav_move_test.rb b/test/integration/webdav/dmsf_webdav_move_test.rb index a9f999d6..eba4bafe 100644 --- a/test/integration/webdav/dmsf_webdav_move_test.rb +++ b/test/integration/webdav/dmsf_webdav_move_test.rb @@ -308,7 +308,7 @@ class DmsfWebdavMoveTest < RedmineDmsf::Test::IntegrationTest temp_file = DmsfFile.find_file_by_name @project1, nil, temp_file_name assert_not temp_file, "File '#{temp_file_name}' should not exist yet." - # Move the original file to AAAAAAAA.tmp. The original file should not changed but a new file should be created. + # Move the original file to AAAAAAAA.tmp. The original file should not be changed but a new file should be created. assert_no_difference '@file1.dmsf_file_revisions.count' do process :move, "/dmsf/webdav/#{@project1.identifier}/#{@file1.name}", diff --git a/test/integration/webdav/dmsf_webdav_put_test.rb b/test/integration/webdav/dmsf_webdav_put_test.rb index 03b1cbe5..8cbbc9da 100644 --- a/test/integration/webdav/dmsf_webdav_put_test.rb +++ b/test/integration/webdav/dmsf_webdav_put_test.rb @@ -255,7 +255,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '1234', headers: credentials assert_response :success - file1 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder: nil, name: 'file1.tmp') + file1 = DmsfFile.find_file_by_name(@project1, nil, 'file1.tmp') assert file1 assert_difference 'file1.dmsf_file_revisions.count', 0 do put "/dmsf/webdav/#{@project1.identifier}/file1.tmp", params: '5678', headers: credentials @@ -268,7 +268,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '1234', headers: credentials assert_response :success - file2 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: '~$file2.txt') + file2 = DmsfFile.find_file_by_name(@project1, nil, '~$file2.txt') assert file2 assert_difference 'file2.dmsf_file_revisions.count', 0 do put "/dmsf/webdav/#{@project1.identifier}/~$file2.txt", params: '5678', headers: credentials @@ -286,7 +286,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest 'dmsf_webdav_strategy' => 'WEBDAV_READ_WRITE' } do put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '1234', headers: credentials assert_response :success - file3 = DmsfFile.find_by(project_id: @project1.id, dmsf_folder_id: nil, name: 'file3.dump') + file3 = DmsfFile.find_file_by_name(@project1, nil, 'file3.dump') assert file3 assert_difference 'file3.dmsf_file_revisions.count', 0 do put "/dmsf/webdav/#{@project1.identifier}/file3.dump", params: '5678', headers: credentials @@ -304,7 +304,7 @@ class DmsfWebdavPutTest < RedmineDmsf::Test::IntegrationTest params: '1234', headers: @admin.merge!({ content_type: :text }) assert_response :created - assert DmsfFile.find_by(project_id: @project5.id, dmsf_folder: nil, name: 'test-1234.txt') + assert DmsfFile.find_file_by_name(@project5, nil, 'test-1234.txt') end def test_put_keep_title diff --git a/test/unit/dmsf_file_revision_test.rb b/test/unit/dmsf_file_revision_test.rb index 69afe587..61ba4c56 100644 --- a/test/unit/dmsf_file_revision_test.rb +++ b/test/unit/dmsf_file_revision_test.rb @@ -28,34 +28,112 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest @revision1 = DmsfFileRevision.find 1 @revision2 = DmsfFileRevision.find 2 @revision3 = DmsfFileRevision.find 3 + @revision7 = DmsfFileRevision.find 7 @revision8 = DmsfFileRevision.find 8 @revision13 = DmsfFileRevision.find 13 @wf1 = DmsfWorkflow.find 1 end - def test_file_title_length_validation - file = DmsfFileRevision.new(title: Array.new(256).map { 'a' }.join, - name: 'Test Revision', - major_version: 1) - assert file.invalid? - assert_equal ['Title is too long (maximum is 255 characters)'], file.errors.full_messages + def test_name_presence + @revision1.name = '' + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, 'Name cannot be blank' end - def test_file_name_length_validation - file = DmsfFileRevision.new(name: Array.new(256).map { 'a' }.join, - title: 'Test Revision', - major_version: 1) - assert file.invalid? - assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages + def test_title_presence + @revision1.title = '' + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, 'Title cannot be blank' end - def test_file_disk_filename_length_validation - file = DmsfFileRevision.new(disk_filename: Array.new(256).map { 'a' }.join, - title: 'Test Revision', - name: 'Test Revision', - major_version: 1) - assert file.invalid? - assert_equal ['Disk filename is too long (maximum is 255 characters)'], file.errors.full_messages + def test_title_length_validation + @revision1.title = String.new('a' * 256) + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, 'Title is too long (maximum is 255 characters)' + end + + def test_name_length_validation + @revision1.name = String.new('a' * 256) + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, 'Name is too long (maximum is 255 characters)' + end + + def test_name_uniqueness_validation + User.current = @admin + + # Duplicity among files names + @revision7.name = @revision1.name + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Name has already been taken' + + # Duplicity among invisible files is all right + @revision7.name = @revision3.name + assert_not @revision7.invalid? + + # Duplicity among files titles + @revision7.name = @revision1.title + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Name has already been taken' + + # Duplicity among folders + @revision7.name = @folder1.title + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Name has already been taken' + + # Duplicity among links + @revision7.name = @folder_link1.name + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Name has already been taken' + + # Name is all right + @revision7.name = 'xxx' + assert @revision7.valid? + end + + def test_title_uniqueness_validation + User.current = @admin + + # Duplicity among files names + @revision7.title = @revision1.name + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Title has already been taken' + + # Duplicity among invisible files is all right + @revision7.title = @revision3.name + assert_not @revision7.invalid? + + # Duplicity among files titles + @revision7.title = @revision1.title + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Title has already been taken' + + # Duplicity among folders + @revision7.title = @folder1.title + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Title has already been taken' + + # Duplicity among links + @revision7.title = @folder_link1.name + assert @revision7.invalid? + assert_includes @revision7.errors.full_messages, 'Title has already been taken' + + # Name is all right + @revision7.title = 'xxx' + assert @revision7.valid? + end + + def test_name_invalid_characters_validation + @revision1.name << DmsfFolder::INVALID_CHARACTERS[0] + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, + "Name #{l('activerecord.errors.messages.error_contains_invalid_character')}" + end + + def test_title_invalid_characters_validation + @revision1.title << DmsfFolder::INVALID_CHARACTERS[0] + assert @revision1.invalid? + assert_includes @revision1.errors.full_messages, + "Title #{l('activerecord.errors.messages.error_contains_invalid_character')}" end def test_delete_restore @@ -70,56 +148,6 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest assert_nil DmsfFileRevision.find_by(id: @revision13.id) end - def test_new_storage_filename - # Create a file. - f = DmsfFile.new - f.project_id = 1 - f.name = 'Testfile.txt' - f.dmsf_folder = nil - f.notification = RedmineDmsf.dmsf_default_notifications? - f.save - - # Create two new revisions, r1 and r2 - r1 = DmsfFileRevision.new - r1.minor_version = 0 - r1.major_version = 1 - r1.dmsf_file = f - r1.user = User.current - r1.name = 'Testfile.txt' - r1.title = DmsfFileRevision.filename_to_title(r1.name) - r1.description = nil - r1.comment = nil - r1.size = 4 - - r2 = r1.clone - r2.minor_version = 1 - - assert r1.valid? - assert r2.valid? - - # This is a very stupid since the generation and storing of files below must be done during the - # same second, so wait until the microsecond part of the DateTime is less than 10 ms, should be - # plenty of time to do the rest then. - wait_timeout = 2_000 - while DateTime.current.usec > 10_000 - wait_timeout -= 10 - flunk 'Waited too long.' if wait_timeout <= 0 - sleep 0.01 - end - - # First, generate the r1 storage filename and save the file - r1.disk_filename = r1.new_storage_filename - assert r1.save - # Just make sure the file exists - File.binwrite r1.disk_file, '1234' - - # Directly after the file has been stored generate the r2 storage filename. - # Hopefully the seconds part of the DateTime.current has not changed and the generated filename will - # be on the same second but it should then be increased by 1. - r2.disk_filename = r2.new_storage_filename - assert_not_equal r1.disk_filename, r2.disk_filename, 'The disk filename should not be equal for two revisions.' - end - def test_invalid_filename_extension with_settings(attachment_extensions_allowed: 'txt') do r1 = DmsfFileRevision.new @@ -334,4 +362,12 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest def test_checksum assert_equal @revision1.checksum, @revision1.file.blob.checksum end + + def test_content_type + assert_equal 'text/plain', @revision1.content_type + @revision1.file.blob.content_type = '' + assert_equal 'text/plain', @revision1.content_type + @revision1.file.blob.filename = 'data' + assert_equal 'application/octet-stream', @revision1.content_type + end end diff --git a/test/unit/dmsf_file_test.rb b/test/unit/dmsf_file_test.rb index afb1bdde..47e5ab09 100644 --- a/test/unit/dmsf_file_test.rb +++ b/test/unit/dmsf_file_test.rb @@ -28,12 +28,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest @wf2 = DmsfWorkflow.find 2 end - def test_file_name_length_validation - file = DmsfFile.new(name: Array.new(256).map { 'a' }.join) - assert file.invalid? - assert_equal ['Name is too long (maximum is 255 characters)'], file.errors.full_messages - end - def test_project_file_count_differs_from_project_visibility_count assert_not_same @project1.dmsf_files.all.size, @project1.dmsf_files.visible.all.size end @@ -188,15 +182,13 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest # Text assert_equal 'attachment', @file1.disposition # Image - assert_equal 'inline', @file7.disposition + assert_equal 'inline', @file6.disposition # PDF - assert_equal 'inline', @file8.disposition + assert_equal 'inline', @file7.disposition # Video - @file1.last_revision.disk_filename = 'test.mp4' - assert_equal 'inline', @file1.disposition + assert_equal 'inline', @file6.disposition # HTML - @file1.last_revision.disk_filename = 'test.html' - assert_equal 'inline', @file1.disposition + assert_equal 'inline', @file14.disposition end def test_image @@ -209,8 +201,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest assert @file1.text? assert_not @file7.text? assert_not @file8.text? - @file1.last_revision.disk_filename = 'test.c' - assert @file1.text? end def test_pdf @@ -221,25 +211,28 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest def test_video assert_not @file1.video? - @file1.last_revision.disk_filename = 'test.mp4' - assert @file1.video? + assert @file6.video? end def test_html assert_not @file1.html? - @file1.last_revision.disk_filename = 'test.html' - assert @file1.html? + assert @file14.html? + end + + def test_office_doc + assert_not @file1.office_doc? + assert @file13.office_doc? end def test_markdown assert_not @file1.markdown? - @file1.last_revision.disk_filename = 'test.md' + @file1.last_revision.file.blob.content_type = 'text/markdown' assert @file1.markdown? end def test_textile assert_not @file1.textile? - @file1.last_revision.disk_filename = 'test.textile' + @file1.last_revision.file.blob.content_type = 'text/textile' assert @file1.textile? end @@ -293,10 +286,6 @@ class DmsfFileTest < RedmineDmsf::Test::UnitTest assert @file1.watched_by?(@jsmith) end - def test_office_doc - assert @file13.office_doc? - end - def test_previewable assert(@file13.previewable?) if RedmineDmsf::Preview.office_available? end diff --git a/test/unit/dmsf_folder_test.rb b/test/unit/dmsf_folder_test.rb index 3fe5e2f5..0b7a3203 100644 --- a/test/unit/dmsf_folder_test.rb +++ b/test/unit/dmsf_folder_test.rb @@ -21,9 +21,64 @@ require File.expand_path('../../test_helper', __FILE__) # Folder tests class DmsfFolderTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + def setup super @link2 = DmsfLink.find 2 + @revision1 = DmsfFileRevision.find 1 + @revision3 = DmsfFileRevision.find 3 + end + + def test_title_presence + @folder1.title = '' + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title cannot be blank' + end + + def test_title_length_validation + @folder1.title = String.new('a' * 256) + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title is too long (maximum is 255 characters)' + end + + def test_title_invalid_characters_validation + @folder1.title << DmsfFolder::INVALID_CHARACTERS[0] + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, + "Title #{l('activerecord.errors.messages.error_contains_invalid_character')}" + end + + def test_title_uniqueness_validation + User.current = @admin + + # Duplicity among files names + @folder1.title = @revision1.name + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title has already been taken' + + # Duplicity among invisible files is all right + @folder1.title = @revision3.name + assert_not @folder1.invalid? + + # Duplicity among files titles + @folder1.title = @revision1.title + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title has already been taken' + + # Duplicity among folders + @folder1.title = @folder6.title + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title has already been taken' + + # Duplicity among links + @folder1.title = @folder_link1.name + assert @folder1.invalid? + assert_includes @folder1.errors.full_messages, 'Title has already been taken' + + # Name is all right + @folder1.title = 'xxx' + assert @folder1.valid? end def test_visibility diff --git a/test/unit/dmsf_link_test.rb b/test/unit/dmsf_link_test.rb index 50ad085e..bb30d98b 100644 --- a/test/unit/dmsf_link_test.rb +++ b/test/unit/dmsf_link_test.rb @@ -21,6 +21,14 @@ require File.expand_path('../../test_helper', __FILE__) # Link tests class DmsfLinksTest < RedmineDmsf::Test::UnitTest + include Redmine::I18n + + def setup + super + @revision1 = DmsfFileRevision.find 1 + @revision3 = DmsfFileRevision.find 3 + end + def test_create_folder_link folder_link = DmsfLink.new folder_link.target_project_id = @project1.id @@ -57,43 +65,77 @@ class DmsfLinksTest < RedmineDmsf::Test::UnitTest assert external_link.save, external_link.errors.full_messages.to_sentence end - def test_validate_name_length - @folder_link1.name = ('a' * 256) - assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved" - assert_equal 1, @folder_link1.errors.size + def test_name_length_validation + @folder_link1.name = String.new('a' * 256) + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, 'Name is too long (maximum is 255 characters)' end def test_validate_name_presence @folder_link1.name = '' - assert_not @folder_link1.save, "Folder link #{@folder_link1.name} should have not been saved" - assert_equal 1, @folder_link1.errors.size + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, 'Name cannot be blank' + end + + def test_title_invalid_characters_validation + @folder_link1.name << DmsfFolder::INVALID_CHARACTERS[0] + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, + "Name #{l('activerecord.errors.messages.error_contains_invalid_character')}" + end + + def test_name_uniqueness_validation + User.current = @admin + + # Duplicity among files names + @folder_link1.name = @revision1.name + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, 'Name has already been taken' + + # Duplicity among invisible files is all right + @folder_link1.name = @revision3.name + assert_not @folder_link1.invalid? + + # Duplicity among files titles + @folder_link1.name = @revision1.title + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, 'Name has already been taken' + + # Duplicity among folders + @folder_link1.name = @folder6.title + assert @folder_link1.invalid? + assert_includes @folder_link1.errors.full_messages, 'Name has already been taken' + + # Name is all right + @folder_link1.name = 'xxx' + assert @folder_link1.valid? end def test_validate_external_url_length @file_link2.target_type = 'DmsfUrl' @file_link2.external_url = "https://localhost/#{'a' * 256}" - assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" - assert_equal 1, @file_link2.errors.size + assert @file_link2.invalid? + assert_includes @file_link2.errors.full_messages, 'URL is too long (maximum is 255 characters)' end def test_validate_external_url_presence @file_link2.target_type = 'DmsfUrl' @file_link2.external_url = '' - assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" - assert_equal 1, @file_link2.errors.size + assert @file_link2.invalid? + assert_includes @file_link2.errors.full_messages, 'URL is invalid' end def test_validate_external_url_invalid @file_link2.target_type = 'DmsfUrl' @file_link2.external_url = 'htt ps://abc.xyz' - assert_not @file_link2.save, "External URL link #{@file_link2.name} should have not been saved" - assert_equal 1, @file_link2.errors.size + assert @file_link2.invalid? + assert_includes @file_link2.errors.full_messages, 'URL is invalid' end def test_validate_external_url_valid @file_link2.target_type = 'DmsfUrl' @file_link2.external_url = 'https://www.google.com/search?q=寿司' - assert @file_link2.save + assert @file_link2.valid? end def test_belongs_to_project @@ -186,7 +228,7 @@ class DmsfLinksTest < RedmineDmsf::Test::UnitTest def test_copy_to_author assert_equal @admin.id, @file_link2.user_id User.current = @jsmith - l = @file_link2.copy_to(@project1, @folder1) + l = @file_link2.copy_to(@project1, @folder6) assert l assert_equal @jsmith.id, l.user_id, 'Author must be updated when copying' end diff --git a/test/unit/dmsf_query_test.rb b/test/unit/dmsf_query_test.rb index f7c081c2..52154b11 100644 --- a/test/unit/dmsf_query_test.rb +++ b/test/unit/dmsf_query_test.rb @@ -37,10 +37,14 @@ class DmsfQueryTest < RedmineDmsf::Test::UnitTest end def test_dmsf_count - n = DmsfFolder.visible.where(project_id: @project1.id).where("title LIKE '%test%'").all.size + - DmsfFile.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size + - DmsfLink.visible.where(project_id: @project1.id).where("name LIKE '%test%'").all.size - assert_equal n - 1, @query401.dmsf_count # One folder is not visible due to the permissions + n = DmsfFolder.where(project_id: @project1.id).where("title LIKE '%test%'").all.size + + DmsfLink.where(project_id: @project1.id).where("name LIKE '%test%'").all.size + + DmsfFile.where(project_id: @project1.id).find_each do |file| + n += 1 if file.name.include?('test') + end + + assert_equal n - 2, @query401.dmsf_count # Two file are not visible due to folder's permissions end def test_dmsf_nodes diff --git a/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb b/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb index 55eb04b0..63fed933 100644 --- a/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb +++ b/test/unit/lib/redmine_dmsf/dmsf_macros_test.rb @@ -350,7 +350,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest target: '_blank', rel: 'noopener', title: h(@file7.last_revision.try(:tooltip)), - 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + 'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}") assert text.include?(link), text end @@ -379,7 +379,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest target: '_blank', rel: 'noopener', title: h(@file7.last_revision.try(:tooltip)), - 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + 'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}") assert text.include?(link), text # TODO: arguments src and with and height are swapped # size = '640x480' @@ -390,7 +390,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest # target: '_blank', # rel: 'noopener', # title: h(@file7.last_revision.try(:tooltip)), - # 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + # 'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}") # assert text.include?(link), text height = '480' text = textilizable("{{dmsftn(#{@file7.id}, height=#{height})}}") @@ -410,7 +410,7 @@ class DmsfMacrosTest < RedmineDmsf::Test::HelperTest target: '_blank', rel: 'noopener', title: h(@file7.last_revision.try(:tooltip)), - 'data-downloadurl' => "#{@file7.last_revision.detect_content_type}:#{h(@file7.name)}:#{url}") + 'data-downloadurl' => "#{@file7.last_revision.content_type}:#{h(@file7.name)}:#{url}") assert text.include?(link), text end diff --git a/test/unit/project_patch_test.rb b/test/unit/project_patch_test.rb index e3ad9943..f815163d 100644 --- a/test/unit/project_patch_test.rb +++ b/test/unit/project_patch_test.rb @@ -56,7 +56,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest def test_dmsf_count User.current = @jsmith hash = @project1.dmsf_count - assert_equal 9, hash[:files] + assert_equal 10, hash[:files] assert_equal 5, hash[:folders] end @@ -70,7 +70,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest def test_copy_dmsf User.current = @jsmith - assert_equal 5, @project1.dmsf_files.visible.all.size + assert_equal 6, @project1.dmsf_files.visible.all.size assert_equal 3, @project1.dmsf_folders.visible.all.size assert_equal 2, @project1.file_links.all.size assert_equal 1, @project1.folder_links.all.size @@ -84,7 +84,7 @@ class ProjectPatchTest < RedmineDmsf::Test::UnitTest @project5.copy_dmsf @project1 - assert_equal 6, @project5.dmsf_files.all.size + assert_equal 7, @project5.dmsf_files.all.size assert_equal 4, @project5.dmsf_folders.all.size assert_equal 2, @project5.file_links.all.size assert_equal 1, @project5.folder_links.all.size diff --git a/test/unit_test.rb b/test/unit_test.rb index 57365f29..bd729f33 100644 --- a/test/unit_test.rb +++ b/test/unit_test.rb @@ -56,10 +56,12 @@ module RedmineDmsf @file2 = DmsfFile.find 2 @file4 = DmsfFile.find 4 @file5 = DmsfFile.find 5 + @file6 = DmsfFile.find 6 @file7 = DmsfFile.find 7 @file8 = DmsfFile.find 8 @file11 = DmsfFile.find 11 @file13 = DmsfFile.find 13 + @file14 = DmsfFile.find 14 @folder_link1 = DmsfLink.find 1 @file_link2 = DmsfLink.find 2 @file_link7 = DmsfLink.find 7