diff --git a/app/helpers/imports_helper.rb b/app/helpers/imports_helper.rb
index c2d4c59ea..25278e6fc 100644
--- a/app/helpers/imports_helper.rb
+++ b/app/helpers/imports_helper.rb
@@ -52,4 +52,14 @@ module ImportsHelper
[format, f]
end
end
+
+ def render_if_exist(options = {}, locals = {}, &block)
+ if options[:partial]
+ if lookup_context.exists?(options[:partial], lookup_context.prefixes, true)
+ render(options, locals, &block)
+ end
+ else
+ render(options, locals, &block)
+ end
+ end
end
diff --git a/app/models/user_import.rb b/app/models/user_import.rb
new file mode 100644
index 000000000..2813ee5ff
--- /dev/null
+++ b/app/models/user_import.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2020 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class UserImport < Import
+ AUTO_MAPPABLE_FIELDS = {
+ 'login' => 'field_login',
+ 'firstname' => 'field_firstname',
+ 'lastname' => 'field_lastname',
+ 'mail' => 'field_mail',
+ 'language' => 'field_language',
+ 'admin' => 'field_admin',
+ 'auth_source' => 'field_auth_source',
+ 'password' => 'field_password',
+ 'must_change_passwd' => 'field_must_change_passwd',
+ 'status' => 'field_status'
+ }
+
+ def self.menu_item
+ :users
+ end
+
+ def self.layout
+ 'admin'
+ end
+
+ def self.authorized?(user)
+ user.admin?
+ end
+
+ # Returns the objects that were imported
+ def saved_objects
+ User.where(:id => saved_items.pluck(:obj_id)).order(:id)
+ end
+
+ def mappable_custom_fields
+ UserCustomField.all
+ end
+
+ private
+
+ def build_object(row, item)
+ object = User.new
+
+ attributes = {
+ :login => row_value(row, 'login'),
+ :firstname => row_value(row, 'firstname'),
+ :lastname => row_value(row, 'lastname'),
+ :mail => row_value(row, 'mail')
+ }
+
+ lang = nil
+ if language = row_value(row, 'language')
+ lang = find_language(language)
+ end
+ attributes[:language] = lang || Setting.default_language
+
+ if admin = row_value(row, 'admin')
+ if yes?(admin)
+ attributes['admin'] = '1'
+ end
+ end
+
+ if auth_source_name = row_value(row, 'auth_source')
+ if auth_source = AuthSource.find_by(:name => auth_source_name)
+ attributes[:auth_source_id] = auth_source.id
+ end
+ end
+
+ if password = row_value(row, 'password')
+ object.password = password
+ object.password_confirmation = password
+ end
+
+ if must_change_passwd = row_value(row, 'must_change_passwd')
+ if yes?(must_change_passwd)
+ attributes[:must_change_passwd] = '1'
+ end
+ end
+
+ if status_name = row_value(row, 'status')
+ if status = User::LABEL_BY_STATUS.key(status_name)
+ attributes[:status] = status
+ end
+ end
+
+ attributes['custom_field_values'] = object.custom_field_values.inject({}) do |h, v|
+ value =
+ case v.custom_field.field_format
+ when 'date'
+ row_date(row, "cf_#{v.custom_field.id}")
+ else
+ row_value(row, "cf_#{v.custom_field.id}")
+ end
+ if value
+ h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, object)
+ end
+ h
+ end
+
+ object.send(:safe_attributes=, attributes, user)
+ object
+ end
+end
diff --git a/app/views/imports/_users_fields_mapping.html.erb b/app/views/imports/_users_fields_mapping.html.erb
new file mode 100644
index 000000000..f2b2aaf90
--- /dev/null
+++ b/app/views/imports/_users_fields_mapping.html.erb
@@ -0,0 +1,53 @@
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 89fd151c4..0836d2593 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1295,3 +1295,4 @@ en:
text_project_is_public_non_member: Public projects and their contents are available to all logged-in users.
text_project_is_public_anonymous: Public projects and their contents are openly available on the network.
label_import_time_entries: Import time entries
+ label_import_users: Import users
diff --git a/config/routes.rb b/config/routes.rb
index 7e8cdeac9..03071fad9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -66,6 +66,7 @@ Rails.application.routes.draw do
get '/issues/imports/new', :to => 'imports#new', :defaults => { :type => 'IssueImport' }, :as => 'new_issues_import'
get '/time_entries/imports/new', :to => 'imports#new', :defaults => { :type => 'TimeEntryImport' }, :as => 'new_time_entries_import'
+ get '/users/imports/new', :to => 'imports#new', :defaults => { :type => 'UserImport' }, :as => 'new_users_import'
post '/imports', :to => 'imports#create', :as => 'imports'
get '/imports/:id', :to => 'imports#show', :as => 'import'
match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings'
diff --git a/test/fixtures/files/import_users.csv b/test/fixtures/files/import_users.csv
new file mode 100644
index 000000000..e31f4e0b9
--- /dev/null
+++ b/test/fixtures/files/import_users.csv
@@ -0,0 +1,4 @@
+row;login;firstname;lastname;mail;language;admin;auth_source;password;must_change_passwd;status;phone_number
+1;user1;One;CSV;user1@somenet.foo;en;yes;;password;yes;active;000-1111-2222
+2;user2;Two;Import;user2@somenet.foo;ja;no;;password;no;locked;333-4444-5555
+3;user3;Three;User;user3@somenet.foo;-;no;LDAP test server;password;no;registered;666-7777-8888
diff --git a/test/fixtures/views/_partial.html.erb b/test/fixtures/views/_partial.html.erb
new file mode 100644
index 000000000..29ccbb1b9
--- /dev/null
+++ b/test/fixtures/views/_partial.html.erb
@@ -0,0 +1 @@
+partial html
diff --git a/test/helpers/imports_helper_test.rb b/test/helpers/imports_helper_test.rb
new file mode 100644
index 000000000..056e89ef5
--- /dev/null
+++ b/test/helpers/imports_helper_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2020 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ImporstHelperTest < Redmine::HelperTest
+ include ImportsHelper
+
+ def setup
+ controller.prepend_view_path "test/fixtures/views"
+ end
+
+ def test_redner_if_exist_should_be_render_partial
+ assert_equal "partial html\n", render_if_exist(:partial => 'partial')
+ end
+
+ def test_redner_if_exist_should_be_render_nil
+ assert_nil render_if_exist(:partial => 'non_exist_partial')
+ end
+end
diff --git a/test/unit/user_import_test.rb b/test/unit/user_import_test.rb
new file mode 100644
index 000000000..082cb8360
--- /dev/null
+++ b/test/unit/user_import_test.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2020 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class UserImportTest < ActiveSupport::TestCase
+
+ include Redmine::I18n
+
+ def setup
+ set_language_if_valid 'en'
+ User.current = nil
+ end
+
+ def test_authorized
+ assert UserImport.authorized?(User.find(1)) # admins
+ assert !UserImport.authorized?(User.find(2)) # dose not admin
+ assert !UserImport.authorized?(User.find(6)) # dows not admin
+ end
+
+ def test_maps_login
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert_equal 'user1', first.login
+ assert_equal 'user2', second.login
+ assert_equal 'user3', third.login
+ end
+
+ def test_maps_firstname
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert_equal 'One', first.firstname
+ assert_equal 'Two', second.firstname
+ assert_equal 'Three', third.firstname
+ end
+
+ def test_maps_lastname
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert_equal 'CSV', first.lastname
+ assert_equal 'Import', second.lastname
+ assert_equal 'User', third.lastname
+ end
+
+ def test_maps_mail
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert_equal 'user1@somenet.foo', first.mail
+ assert_equal 'user2@somenet.foo', second.mail
+ assert_equal 'user3@somenet.foo', third.mail
+ end
+
+ def test_maps_language
+ default_language = 'fr'
+ with_settings :default_language => default_language do
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert_equal 'en', first.language
+ assert_equal 'ja', second.language
+ assert_equal default_language, third.language
+ end
+ end
+
+ def test_maps_admin
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert first.admin?
+ assert_not second.admin?
+ assert_not third.admin?
+ end
+
+ def test_maps_auth_information
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ # use password
+ assert User.try_to_login(first.login, 'password', false)
+ assert User.try_to_login(second.login, 'password', false)
+ # use auth_source
+ assert_nil first.auth_source
+ assert_nil second.auth_source
+ assert third.auth_source
+ assert_equal 'LDAP test server', third.auth_source.name
+ AuthSourceLdap.any_instance.expects(:authenticate).with(third.login, 'ldapassword').returns(true)
+ assert User.try_to_login(third.login, 'ldapassword', false)
+ end
+
+ def test_map_must_change_password
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert first.must_change_password?
+ assert_not second.must_change_password?
+ assert_not third.must_change_password?
+ end
+
+ def test_maps_status
+ import = generate_import_with_mapping
+ first, second, third = new_records(User, 3) { import.run }
+ assert first.active?
+ assert second.locked?
+ assert third.registered?
+ end
+
+ def test_maps_custom_fields
+ phone_number_cf = UserCustomField.find(4)
+
+ import = generate_import_with_mapping
+ import.mapping.merge!("cf_#{phone_number_cf.id}" => '11')
+ import.save!
+ first, second, third = new_records(User, 3) { import.run }
+
+ assert_equal '000-1111-2222', first.custom_field_value(phone_number_cf)
+ assert_equal '333-4444-5555', second.custom_field_value(phone_number_cf)
+ assert_equal '666-7777-8888', third.custom_field_value(phone_number_cf)
+ end
+
+ protected
+
+ def generate_import(fixture_name='import_users.csv')
+ import = UserImport.new
+ import.user_id = 1
+ import.file = uploaded_test_file(fixture_name, 'text/csv')
+ import.save!
+ import
+ end
+
+ def generate_import_with_mapping(fixture_name='import_users.csv')
+ import = generate_import(fixture_name)
+
+ import.settings = {
+ 'separator' => ';', 'wrapper' => '"', 'encoding' => 'UTF-8',
+ 'mapping' => {
+ 'login' => '1',
+ 'firstname' => '2',
+ 'lastname' => '3',
+ 'mail' => '4',
+ 'language' => '5',
+ 'admin' => '6',
+ 'auth_source' => '7',
+ 'password' => '8',
+ 'must_change_passwd' => '9',
+ 'status' => '10',
+ }
+ }
+ import.save!
+ import
+ end
+end