#9 Active Storage - Litmus

This commit is contained in:
Karel Pičman 2025-12-10 16:42:09 +01:00
parent e87094cb57
commit 27435704f6
11 changed files with 18 additions and 268 deletions

View File

@ -90,7 +90,7 @@ class DmsfFileRevision < ApplicationRecord
} }
) )
validates :title, presence: true, dmsf_file_name: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :major_version, presence: true validates :major_version, presence: true
validates :name, presence: true, dmsf_file_name: true, length: { maximum: 255 }, 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 :description, length: { maximum: 1.kilobyte }
@ -133,14 +133,6 @@ class DmsfFileRevision < ApplicationRecord
dmsf_file&.dmsf_folder dmsf_file&.dmsf_folder
end end
def self.remove_extension(filename)
filename[0, (filename.length - File.extname(filename).length)]
end
def self.filename_to_title(filename)
remove_extension(filename).gsub(/_+/, ' ')
end
def delete(commit: false, force: true) def delete(commit: false, force: true)
if dmsf_file.locked_for_user? if dmsf_file.locked_for_user?
errors.add :base, l(:error_file_is_locked) errors.add :base, l(:error_file_is_locked)

View File

@ -91,7 +91,7 @@ class DmsfFolder < ApplicationRecord
datetime: proc { |o| o.updated_at }, datetime: proc { |o| o.updated_at },
author: proc { |o| o.user } author: proc { |o| o.user }
validates :title, presence: true, length: { maximum: 255 }, dmsf_folder_name: true validates :title, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted], validates :title, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true } conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true }
validates :description, length: { maximum: 65_535 } validates :description, length: { maximum: 65_535 }

View File

@ -26,7 +26,7 @@ class DmsfLink < ApplicationRecord
belongs_to :deleted_by_user, class_name: 'User' belongs_to :deleted_by_user, class_name: 'User'
belongs_to :user belongs_to :user
validates :name, presence: true, length: { maximum: 255 }, dmsf_link_name: true validates :name, presence: true, length: { maximum: 255 }, dmsf_file_name: true
validates :name, uniqueness: { scope: %i[dmsf_folder_id project_id deleted], validates :name, uniqueness: { scope: %i[dmsf_folder_id project_id deleted],
conditions: -> { where(deleted: STATUS_ACTIVE) }, case_sensitive: true } 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 # There can be project_id = -1 when attaching links to an issue. The project_id is assigned later when saving the

View File

@ -86,7 +86,7 @@ class DmsfUpload
@token = uploaded[:token] @token = uploaded[:token]
if file.nil? || file.last_revision.nil? if file.nil? || file.last_revision.nil?
@title = DmsfFileRevision.filename_to_title(@name) @title = File.basename(@name, '.*')
@description = uploaded[:comment] @description = uploaded[:comment]
if RedmineDmsf.empty_minor_version_by_default? if RedmineDmsf.empty_minor_version_by_default?
@major_version = 1 @major_version = 1

View File

@ -24,36 +24,5 @@ class DmsfFileNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
# Check invalid characters # Check invalid characters
record.errors.add attribute, :error_contains_invalid_character unless ALL_INVALID_CHARACTERS.match?(value) 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
end end

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# 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
# <https://www.gnu.org/licenses/>.
# 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

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
# Redmine plugin for Document Management System "Features"
#
# Vít Jonáš <vit.jonas@gmail.com>, Karel Pičman <karel.picman@kontron.com>
#
# 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
# <https://www.gnu.org/licenses/>.
# 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

View File

@ -224,8 +224,9 @@ module RedmineDmsf
dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user)) dest = ResourceProxy.new(dest_path, @request, @response, @options.merge(user: @user))
return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil? return PreconditionFailed if !dest.resource.is_a?(DmsfResource) || dest.resource.project.nil?
parent = dest.resource.parent parent = dest_path.end_with?('/') && !collection? ? dest.resource : dest.resource.parent
raise Forbidden unless dest.resource.project.module_enabled?(:dmsf) raise Forbidden unless dest.resource.project.module_enabled?(:dmsf)
if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) || if !parent.exist? || (!User.current.admin? && (!DmsfFolder.permissions?(folder, allow_system: false) ||
!DmsfFolder.permissions?(parent.folder, allow_system: false))) !DmsfFolder.permissions?(parent.folder, allow_system: false)))
raise Forbidden raise Forbidden
@ -241,13 +242,11 @@ module RedmineDmsf
!User.current.allowed_to?(:folder_manipulation, dest.resource.project)) !User.current.allowed_to?(:folder_manipulation, dest.resource.project))
raise Forbidden raise Forbidden
end end
return MethodNotAllowed unless folder # Moving sub-project not enabled return MethodNotAllowed unless folder # Moving subprojects is not enabled
raise Locked if folder.locked_for_user? raise Locked if folder.locked_for_user?
# Change the title # Change the title
folder.title = dest.resource.basename folder.title = dest.resource.basename
return PreconditionFailed unless folder.save
# Move to a new destination # Move to a new destination
folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed folder.move_to(dest.resource.project, parent.folder) ? Created : PreconditionFailed
else else
@ -294,7 +293,7 @@ module RedmineDmsf
# Update Revision and names of file [We can link to old physical resource, as it's not changed] # Update Revision and names of file [We can link to old physical resource, as it's not changed]
if file.last_revision if file.last_revision
file.last_revision.name = dest.resource.basename file.last_revision.name = dest.resource.basename
file.last_revision.title = DmsfFileRevision.filename_to_title(dest.resource.basename) file.last_revision.title = File.basename(dest.resource.basename, '.*')
end end
# Save Changes # Save Changes
if file.last_revision.save && file.save if file.last_revision.save && file.save
@ -337,7 +336,7 @@ module RedmineDmsf
# Manipulate folders on destination project :folder_manipulation # Manipulate folders on destination project :folder_manipulation
# View folders on destination project :view_dmsf_folders # View folders on destination project :view_dmsf_folders
# View files on the source project :view_dmsf_files # View files on the source project :view_dmsf_files
# View fodlers on the source project :view_dmsf_folders # View folders on the source project :view_dmsf_folders
raise Forbidden unless User.current.admin? || raise Forbidden unless User.current.admin? ||
(User.current.allowed_to?(:folder_manipulation, dest.resource.project) && (User.current.allowed_to?(:folder_manipulation, dest.resource.project) &&
User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) && User.current.allowed_to?(:view_dmsf_folders, dest.resource.project) &&
@ -367,6 +366,7 @@ module RedmineDmsf
# Update Revision and names of file (We can link to old physical resource, as it's not changed) # 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.last_revision.name = dest.resource.basename
new_file.last_revision.title = File.basename(dest.resource.basename, '.*')
# Save Changes # Save Changes
unless new_file.last_revision.save && new_file.save unless new_file.last_revision.save && new_file.save
new_file.delete commit: true new_file.delete commit: true
@ -582,7 +582,7 @@ module RedmineDmsf
new_revision = DmsfFileRevision.new new_revision = DmsfFileRevision.new
new_revision.minor_version = 1 new_revision.minor_version = 1
new_revision.major_version = 0 new_revision.major_version = 0
new_revision.title = DmsfFileRevision.filename_to_title(basename) new_revision.title = File.basename(basename, '.*')
end end
new_revision.dmsf_file = f new_revision.dmsf_file = f
@ -764,11 +764,11 @@ module RedmineDmsf
f = DmsfFile.new f = DmsfFile.new
f.project_id = project.id f.project_id = project.id
f.dmsf_folder = parent.folder f.dmsf_folder = parent.folder
if f.save(validate: false) # Skip validation due to invalid characters in the filename if f.save
r = DmsfFileRevision.new r = DmsfFileRevision.new
r.minor_version = 1 r.minor_version = 1
r.major_version = 0 r.major_version = 0
r.title = DmsfFileRevision.filename_to_title(basename) r.title = File.basename(basename, '.*')
r.dmsf_file = f r.dmsf_file = f
r.user = User.current r.user = User.current
r.name = basename r.name = basename
@ -779,10 +779,10 @@ module RedmineDmsf
r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value }) r.custom_field_values << CustomValue.new({ custom_field: cf, value: cf.default_value })
end end
if r.save(validate: false) # Skip validation due to invalid characters in the filename if r.save(validate: false) # Skip validation due to invalid characters in the filename
revision.file.attach( r.file.attach(
io: File.new(upload.tempfile_path), io: File.new(DmsfHelper.temp_filename(basename), File::CREAT),
filename: file_upload.filename, filename: basename,
content_type: Redmine::MimeType.of(file_upload.filename), content_type: 'application/octet-stream',
identify: false identify: false
) )
return f return f

View File

@ -58,70 +58,6 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest
assert_includes @revision1.errors.full_messages, 'Name is too long (maximum is 255 characters)' assert_includes @revision1.errors.full_messages, 'Name is too long (maximum is 255 characters)'
end 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 def test_name_invalid_characters_validation
@revision1.name << DmsfFolder::INVALID_CHARACTERS[0] @revision1.name << DmsfFolder::INVALID_CHARACTERS[0]
assert @revision1.invalid? assert @revision1.invalid?
@ -156,7 +92,7 @@ class DmsfFileRevisionTest < RedmineDmsf::Test::UnitTest
r1.dmsf_file = @file1 # name test.txt r1.dmsf_file = @file1 # name test.txt
r1.user = User.current r1.user = User.current
r1.name = 'test.txt.png' r1.name = 'test.txt.png'
r1.title = DmsfFileRevision.filename_to_title(r1.name) r1.title = File.basename(r1.name, '.*')
r1.description = nil r1.description = nil
r1.comment = nil r1.comment = nil
r1.size = 4 r1.size = 4

View File

@ -49,38 +49,6 @@ class DmsfFolderTest < RedmineDmsf::Test::UnitTest
"Title #{l('activerecord.errors.messages.error_contains_invalid_character')}" "Title #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end 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 def test_visibility
# The role has got permissions # The role has got permissions
User.current = @jsmith User.current = @jsmith

View File

@ -84,33 +84,6 @@ class DmsfLinksTest < RedmineDmsf::Test::UnitTest
"Name #{l('activerecord.errors.messages.error_contains_invalid_character')}" "Name #{l('activerecord.errors.messages.error_contains_invalid_character')}"
end 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 def test_validate_external_url_length
@file_link2.target_type = 'DmsfUrl' @file_link2.target_type = 'DmsfUrl'
@file_link2.external_url = "https://localhost/#{'a' * 256}" @file_link2.external_url = "https://localhost/#{'a' * 256}"