Feature #33102 » csv_user_import-003.patch
| app/helpers/imports_helper.rb | ||
|---|---|---|
| 52 | 52 | [format, f] | 
| 53 | 53 | end | 
| 54 | 54 | end | 
| 55 | ||
| 56 |   def render_if_exist(options = {}, locals = {}, &block) | |
| 57 | if options[:partial] | |
| 58 | if lookup_context.exists?(options[:partial], lookup_context.prefixes, true) | |
| 59 | render(options, locals, &block) | |
| 60 | end | |
| 61 | else | |
| 62 | render(options, locals, &block) | |
| 63 | end | |
| 64 | end | |
| 55 | 65 | end | 
| app/models/user_import.rb | ||
|---|---|---|
| 1 | # frozen_string_literal: true | |
| 2 | ||
| 3 | # Redmine - project management software | |
| 4 | # Copyright (C) 2006-2020 Jean-Philippe Lang | |
| 5 | # | |
| 6 | # This program is free software; you can redistribute it and/or | |
| 7 | # modify it under the terms of the GNU General Public License | |
| 8 | # as published by the Free Software Foundation; either version 2 | |
| 9 | # of the License, or (at your option) any later version. | |
| 10 | # | |
| 11 | # This program is distributed in the hope that it will be useful, | |
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | # GNU General Public License for more details. | |
| 15 | # | |
| 16 | # You should have received a copy of the GNU General Public License | |
| 17 | # along with this program; if not, write to the Free Software | |
| 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
| 19 | ||
| 20 | class UserImport < Import | |
| 21 |   AUTO_MAPPABLE_FIELDS = { | |
| 22 | 'login' => 'field_login', | |
| 23 | 'firstname' => 'field_firstname', | |
| 24 | 'lastname' => 'field_lastname', | |
| 25 | 'mail' => 'field_mail', | |
| 26 | 'language' => 'field_language', | |
| 27 | 'admin' => 'field_admin', | |
| 28 | 'auth_source' => 'field_auth_source', | |
| 29 | 'password' => 'field_password', | |
| 30 | 'must_change_passwd' => 'field_must_change_passwd', | |
| 31 | 'status' => 'field_status' | |
| 32 | } | |
| 33 | ||
| 34 | def self.menu_item | |
| 35 | :users | |
| 36 | end | |
| 37 | ||
| 38 | def self.layout | |
| 39 | 'admin' | |
| 40 | end | |
| 41 | ||
| 42 | def self.authorized?(user) | |
| 43 | user.admin? | |
| 44 | end | |
| 45 | ||
| 46 | # Returns the objects that were imported | |
| 47 | def saved_objects | |
| 48 | User.where(:id => saved_items.pluck(:obj_id)).order(:id) | |
| 49 | end | |
| 50 | ||
| 51 | def mappable_custom_fields | |
| 52 | UserCustomField.all | |
| 53 | end | |
| 54 | ||
| 55 | private | |
| 56 | ||
| 57 | def build_object(row, item) | |
| 58 | object = User.new | |
| 59 | ||
| 60 |     attributes = { | |
| 61 | :login => row_value(row, 'login'), | |
| 62 | :firstname => row_value(row, 'firstname'), | |
| 63 | :lastname => row_value(row, 'lastname'), | |
| 64 | :mail => row_value(row, 'mail') | |
| 65 | } | |
| 66 | ||
| 67 | lang = nil | |
| 68 | if language = row_value(row, 'language') | |
| 69 | lang = find_language(language) | |
| 70 | end | |
| 71 | attributes[:language] = lang || Setting.default_language | |
| 72 | ||
| 73 | if admin = row_value(row, 'admin') | |
| 74 | if yes?(admin) | |
| 75 | attributes['admin'] = '1' | |
| 76 | end | |
| 77 | end | |
| 78 | ||
| 79 | if auth_source_name = row_value(row, 'auth_source') | |
| 80 | if auth_source = AuthSource.find_by(:name => auth_source_name) | |
| 81 | attributes[:auth_source_id] = auth_source.id | |
| 82 | end | |
| 83 | end | |
| 84 | ||
| 85 | if password = row_value(row, 'password') | |
| 86 | object.password = password | |
| 87 | object.password_confirmation = password | |
| 88 | end | |
| 89 | ||
| 90 | if must_change_passwd = row_value(row, 'must_change_passwd') | |
| 91 | if yes?(must_change_passwd) | |
| 92 | attributes[:must_change_passwd] = '1' | |
| 93 | end | |
| 94 | end | |
| 95 | ||
| 96 | if status_name = row_value(row, 'status') | |
| 97 | if status = User::LABEL_BY_STATUS.key(status_name) | |
| 98 | attributes[:status] = status | |
| 99 | end | |
| 100 | end | |
| 101 | ||
| 102 |     attributes['custom_field_values'] = object.custom_field_values.inject({}) do |h, v| | |
| 103 | value = | |
| 104 | case v.custom_field.field_format | |
| 105 | when 'date' | |
| 106 |           row_date(row, "cf_#{v.custom_field.id}") | |
| 107 | else | |
| 108 |           row_value(row, "cf_#{v.custom_field.id}") | |
| 109 | end | |
| 110 | if value | |
| 111 | h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, object) | |
| 112 | end | |
| 113 | h | |
| 114 | end | |
| 115 | ||
| 116 | object.send(:safe_attributes=, attributes, user) | |
| 117 | object | |
| 118 | end | |
| 119 | end | |
| app/views/imports/_users_fields_mapping.html.erb | ||
|---|---|---|
| 1 | <div class="splitcontent"> | |
| 2 | <div class="splitcontentleft"> | |
| 3 | <p> | |
| 4 | <label for="import_mapping_login"><%= l(:field_login) %></label> | |
| 5 | <%= mapping_select_tag @import, 'login', :required => true %> | |
| 6 | </p> | |
| 7 | <p> | |
| 8 | <label for="import_mapping_firstname"><%= l(:field_firstname) %></label> | |
| 9 | <%= mapping_select_tag @import, 'firstname', :required => true %> | |
| 10 | </p> | |
| 11 | <p> | |
| 12 | <label for="import_mapping_lastname"><%= l(:field_lastname) %></label> | |
| 13 | <%= mapping_select_tag @import, 'lastname', :required => true %> | |
| 14 | </p> | |
| 15 | <p> | |
| 16 | <label for="import_mapping_mail"><%= l(:field_mail) %></label> | |
| 17 | <%= mapping_select_tag @import, 'mail' %> | |
| 18 | </p> | |
| 19 | <p> | |
| 20 | <label for="import_mapping_language"><%= l(:field_language) %></label> | |
| 21 | <%= mapping_select_tag @import, 'language' %> | |
| 22 | </p> | |
| 23 | <p> | |
| 24 | <label for="import_mapping_admin"><%= l(:field_admin) %></label> | |
| 25 | <%= mapping_select_tag @import, 'admin' %> | |
| 26 | </p> | |
| 27 | <p> | |
| 28 | <label for="import_mapping_auth_source_id"><%= l(:field_auth_source) %></label> | |
| 29 | <%= mapping_select_tag @import, 'auth_source' %> | |
| 30 | </p> | |
| 31 | <p> | |
| 32 | <label for="import_mapping_password"><%= l(:field_password) %></label> | |
| 33 | <%= mapping_select_tag @import, 'password' %> | |
| 34 | </p> | |
| 35 | <p> | |
| 36 | <label for="import_mapping_must_change_passwd"><%= l(:field_must_change_passwd) %></label> | |
| 37 | <%= mapping_select_tag @import, 'must_change_passwd' %> | |
| 38 | </p> | |
| 39 | <p> | |
| 40 | <label for="import_mapping_status"><%= l(:field_status) %></label> | |
| 41 | <%= mapping_select_tag @import, 'status' %> | |
| 42 | </p> | |
| 43 | </div> | |
| 44 | ||
| 45 | <div class="splitcontentright"> | |
| 46 | <% @custom_fields.each do |field| %> | |
| 47 | <p> | |
| 48 | <label for="import_mapping_cf_<%= field.id %>"><%= field.name %></label> | |
| 49 |       <%= mapping_select_tag @import, "cf_#{field.id}", :required => field.is_required? %> | |
| 50 | </p> | |
| 51 | <% end %> | |
| 52 | </div> | |
| 53 | </div> | |
| app/views/imports/_users_mapping.html.erb | ||
|---|---|---|
| 1 | <fieldset class="box tabular"> | |
| 2 | <legend><%= l(:label_fields_mapping) %></legend> | |
| 3 | <div id="fields-mapping"> | |
| 4 | <%= render :partial => 'users_fields_mapping' %> | |
| 5 | </div> | |
| 6 | </fieldset> | |
| app/views/imports/_users_mapping.js.erb | ||
|---|---|---|
| 1 | $('#fields-mapping').html('<%= escape_javascript(render :partial => 'users_fields_mapping') %>'); | |
| app/views/imports/_users_saved_objects.html.erb | ||
|---|---|---|
| 1 | <table id="saved-items" class="list"> | |
| 2 | <thead> | |
| 3 | <tr> | |
| 4 | <th><%= t(:field_login) %></th> | |
| 5 | <th><%= t(:field_firstname) %></th> | |
| 6 | <th><%= t(:field_lastname) %></th> | |
| 7 | <th><%= t(:field_mail) %></th> | |
| 8 | <th><%= t(:field_admin) %></th> | |
| 9 | <th><%= t(:field_status) %></th> | |
| 10 | </tr> | |
| 11 | </thead> | |
| 12 | <tbody> | |
| 13 | <% saved_objects.each do |user| %> | |
| 14 | <tr> | |
| 15 | <td><%= avatar(user, :size => "14") %><%= link_to user.login, edit_user_path(user) %></td> | |
| 16 | <td><%= user.firstname %></td> | |
| 17 | <td><%= user.lastname %></td> | |
| 18 | <td><%= mail_to(user.mail) %></td> | |
| 19 | <td><%= checked_image user.admin? %></td> | |
| 20 |     <td><%= l(("status_#{User::LABEL_BY_STATUS[user.status]}")) %> | |
| 21 | </tr> | |
| 22 | <% end %> | |
| 23 | </tbody> | |
| 24 | </table> | |
| app/views/imports/mapping.html.erb | ||
|---|---|---|
| 23 | 23 | </p> | 
| 24 | 24 | <% end %> | 
| 25 | 25 | |
| 26 | <%= render :partial => "#{import_partial_prefix}_sidebar" %> | |
| 26 | <%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %> | |
| 27 | 27 | |
| 28 | 28 | <%= javascript_tag do %> | 
| 29 | 29 | $(document).ready(function() { | 
| app/views/imports/new.html.erb | ||
|---|---|---|
| 12 | 12 | <p><%= submit_tag l(:label_next).html_safe + " »".html_safe, :name => nil %></p> | 
| 13 | 13 | <% end %> | 
| 14 | 14 | |
| 15 | <%= render :partial => "#{import_partial_prefix}_sidebar" %> | |
| 15 | <%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %> | |
| app/views/imports/run.html.erb | ||
|---|---|---|
| 4 | 4 | <div id="import-progress"><div id="progress-label">0 / <%= @import.total_items.to_i %></div></div> | 
| 5 | 5 | </div> | 
| 6 | 6 | |
| 7 | <%= render :partial => "#{import_partial_prefix}_sidebar" %> | |
| 7 | <%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %> | |
| 8 | 8 | |
| 9 | 9 | <%= javascript_tag do %> | 
| 10 | 10 | $(document).ready(function() { | 
| app/views/imports/settings.html.erb | ||
|---|---|---|
| 31 | 31 | <p><%= submit_tag l(:label_next).html_safe + " »".html_safe, :name => nil %></p> | 
| 32 | 32 | <% end %> | 
| 33 | 33 | |
| 34 | <%= render :partial => "#{import_partial_prefix}_sidebar" %> | |
| 34 | <%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %> | |
| app/views/imports/show.html.erb | ||
|---|---|---|
| 27 | 27 | </table> | 
| 28 | 28 | <% end %> | 
| 29 | 29 | |
| 30 | <%= render :partial => "#{import_partial_prefix}_sidebar" %> | |
| 30 | <%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %> | |
| app/views/users/index.html.erb | ||
|---|---|---|
| 1 | 1 | <div class="contextual"> | 
| 2 | 2 | <%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %> | 
| 3 | <%= actions_dropdown do %> | |
| 4 | <% if User.current.allowed_to?(:import_users, nil, :global => true) %> | |
| 5 | <%= link_to l(:button_import), new_users_import_path, :class => 'icon icon-import' %> | |
| 6 | <% end %> | |
| 7 | <% end %> | |
| 3 | 8 | </div> | 
| 4 | 9 | |
| 5 | 10 | <h2><%=l(:label_user_plural)%></h2> | 
| config/locales/en.yml | ||
|---|---|---|
| 1295 | 1295 | text_project_is_public_non_member: Public projects and their contents are available to all logged-in users. | 
| 1296 | 1296 | text_project_is_public_anonymous: Public projects and their contents are openly available on the network. | 
| 1297 | 1297 | label_import_time_entries: Import time entries | 
| 1298 | label_import_users: Import users | |
| config/routes.rb | ||
|---|---|---|
| 66 | 66 | |
| 67 | 67 |   get   '/issues/imports/new', :to => 'imports#new', :defaults => { :type => 'IssueImport' }, :as => 'new_issues_import' | 
| 68 | 68 |   get   '/time_entries/imports/new', :to => 'imports#new', :defaults => { :type => 'TimeEntryImport' }, :as => 'new_time_entries_import' | 
| 69 |   get   '/users/imports/new', :to => 'imports#new', :defaults => { :type => 'UserImport' }, :as => 'new_users_import' | |
| 69 | 70 | post '/imports', :to => 'imports#create', :as => 'imports' | 
| 70 | 71 | get '/imports/:id', :to => 'imports#show', :as => 'import' | 
| 71 | 72 | match '/imports/:id/settings', :to => 'imports#settings', :via => [:get, :post], :as => 'import_settings' | 
| test/fixtures/files/import_users.csv | ||
|---|---|---|
| 1 | row;login;firstname;lastname;mail;language;admin;auth_source;password;must_change_passwd;status;phone_number | |
| 2 | 1;user1;One;CSV;user1@somenet.foo;en;yes;;password;yes;active;000-1111-2222 | |
| 3 | 2;user2;Two;Import;user2@somenet.foo;ja;no;;password;no;locked;333-4444-5555 | |
| 4 | 3;user3;Three;User;user3@somenet.foo;-;no;LDAP test server;password;no;registered;666-7777-8888 | |
| test/fixtures/views/_partial.html.erb | ||
|---|---|---|
| 1 | partial html | |
| test/helpers/imports_helper_test.rb | ||
|---|---|---|
| 1 | # frozen_string_literal: true | |
| 2 | ||
| 3 | # Redmine - project management software | |
| 4 | # Copyright (C) 2006-2020 Jean-Philippe Lang | |
| 5 | # | |
| 6 | # This program is free software; you can redistribute it and/or | |
| 7 | # modify it under the terms of the GNU General Public License | |
| 8 | # as published by the Free Software Foundation; either version 2 | |
| 9 | # of the License, or (at your option) any later version. | |
| 10 | # | |
| 11 | # This program is distributed in the hope that it will be useful, | |
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | # GNU General Public License for more details. | |
| 15 | # | |
| 16 | # You should have received a copy of the GNU General Public License | |
| 17 | # along with this program; if not, write to the Free Software | |
| 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
| 19 | ||
| 20 | require File.expand_path('../../test_helper', __FILE__) | |
| 21 | ||
| 22 | class ImporstHelperTest < Redmine::HelperTest | |
| 23 | include ImportsHelper | |
| 24 | ||
| 25 | def setup | |
| 26 | controller.prepend_view_path "test/fixtures/views" | |
| 27 | end | |
| 28 | ||
| 29 | def test_redner_if_exist_should_be_render_partial | |
| 30 | assert_equal "partial html\n", render_if_exist(:partial => 'partial') | |
| 31 | end | |
| 32 | ||
| 33 | def test_redner_if_exist_should_be_render_nil | |
| 34 | assert_nil render_if_exist(:partial => 'non_exist_partial') | |
| 35 | end | |
| 36 | end | |
| test/unit/user_import_test.rb | ||
|---|---|---|
| 1 | # frozen_string_literal: true | |
| 2 | ||
| 3 | # Redmine - project management software | |
| 4 | # Copyright (C) 2006-2020 Jean-Philippe Lang | |
| 5 | # | |
| 6 | # This program is free software; you can redistribute it and/or | |
| 7 | # modify it under the terms of the GNU General Public License | |
| 8 | # as published by the Free Software Foundation; either version 2 | |
| 9 | # of the License, or (at your option) any later version. | |
| 10 | # | |
| 11 | # This program is distributed in the hope that it will be useful, | |
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | # GNU General Public License for more details. | |
| 15 | # | |
| 16 | # You should have received a copy of the GNU General Public License | |
| 17 | # along with this program; if not, write to the Free Software | |
| 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
| 19 | ||
| 20 | require File.expand_path('../../test_helper', __FILE__) | |
| 21 | ||
| 22 | class UserImportTest < ActiveSupport::TestCase | |
| 23 | ||
| 24 | include Redmine::I18n | |
| 25 | ||
| 26 | def setup | |
| 27 | set_language_if_valid 'en' | |
| 28 | User.current = nil | |
| 29 | end | |
| 30 | ||
| 31 | def test_authorized | |
| 32 | assert UserImport.authorized?(User.find(1)) # admins | |
| 33 | assert !UserImport.authorized?(User.find(2)) # dose not admin | |
| 34 | assert !UserImport.authorized?(User.find(6)) # dows not admin | |
| 35 | end | |
| 36 | ||
| 37 | def test_maps_login | |
| 38 | import = generate_import_with_mapping | |
| 39 |     first, second, third = new_records(User, 3) { import.run } | |
| 40 | assert_equal 'user1', first.login | |
| 41 | assert_equal 'user2', second.login | |
| 42 | assert_equal 'user3', third.login | |
| 43 | end | |
| 44 | ||
| 45 | def test_maps_firstname | |
| 46 | import = generate_import_with_mapping | |
| 47 |     first, second, third = new_records(User, 3) { import.run } | |
| 48 | assert_equal 'One', first.firstname | |
| 49 | assert_equal 'Two', second.firstname | |
| 50 | assert_equal 'Three', third.firstname | |
| 51 | end | |
| 52 | ||
| 53 | def test_maps_lastname | |
| 54 | import = generate_import_with_mapping | |
| 55 |     first, second, third = new_records(User, 3) { import.run } | |
| 56 | assert_equal 'CSV', first.lastname | |
| 57 | assert_equal 'Import', second.lastname | |
| 58 | assert_equal 'User', third.lastname | |
| 59 | end | |
| 60 | ||
| 61 | def test_maps_mail | |
| 62 | import = generate_import_with_mapping | |
| 63 |     first, second, third = new_records(User, 3) { import.run } | |
| 64 | assert_equal 'user1@somenet.foo', first.mail | |
| 65 | assert_equal 'user2@somenet.foo', second.mail | |
| 66 | assert_equal 'user3@somenet.foo', third.mail | |
| 67 | end | |
| 68 | ||
| 69 | def test_maps_language | |
| 70 | default_language = 'fr' | |
| 71 | with_settings :default_language => default_language do | |
| 72 | import = generate_import_with_mapping | |
| 73 |       first, second, third = new_records(User, 3) { import.run } | |
| 74 | assert_equal 'en', first.language | |
| 75 | assert_equal 'ja', second.language | |
| 76 | assert_equal default_language, third.language | |
| 77 | end | |
| 78 | end | |
| 79 | ||
| 80 | def test_maps_admin | |
| 81 | import = generate_import_with_mapping | |
| 82 |     first, second, third = new_records(User, 3) { import.run } | |
| 83 | assert first.admin? | |
| 84 | assert_not second.admin? | |
| 85 | assert_not third.admin? | |
| 86 | end | |
| 87 | ||
| 88 | def test_maps_auth_information | |
| 89 | import = generate_import_with_mapping | |
| 90 |     first, second, third = new_records(User, 3) { import.run } | |
| 91 | # use password | |
| 92 | assert User.try_to_login(first.login, 'password', false) | |
| 93 | assert User.try_to_login(second.login, 'password', false) | |
| 94 | # use auth_source | |
| 95 | assert_nil first.auth_source | |
| 96 | assert_nil second.auth_source | |
| 97 | assert third.auth_source | |
| 98 | assert_equal 'LDAP test server', third.auth_source.name | |
| 99 | AuthSourceLdap.any_instance.expects(:authenticate).with(third.login, 'ldapassword').returns(true) | |
| 100 | assert User.try_to_login(third.login, 'ldapassword', false) | |
| 101 | end | |
| 102 | ||
| 103 | def test_map_must_change_password | |
| 104 | import = generate_import_with_mapping | |
| 105 |     first, second, third = new_records(User, 3) { import.run } | |
| 106 | assert first.must_change_password? | |
| 107 | assert_not second.must_change_password? | |
| 108 | assert_not third.must_change_password? | |
| 109 | end | |
| 110 | ||
| 111 | def test_maps_status | |
| 112 | import = generate_import_with_mapping | |
| 113 |     first, second, third = new_records(User, 3) { import.run } | |
| 114 | assert first.active? | |
| 115 | assert second.locked? | |
| 116 | assert third.registered? | |
| 117 | end | |
| 118 | ||
| 119 | def test_maps_custom_fields | |
| 120 | phone_number_cf = UserCustomField.find(4) | |
| 121 | ||
| 122 | import = generate_import_with_mapping | |
| 123 |     import.mapping.merge!("cf_#{phone_number_cf.id}" => '11') | |
| 124 | import.save! | |
| 125 |     first, second, third = new_records(User, 3) { import.run } | |
| 126 | ||
| 127 | assert_equal '000-1111-2222', first.custom_field_value(phone_number_cf) | |
| 128 | assert_equal '333-4444-5555', second.custom_field_value(phone_number_cf) | |
| 129 | assert_equal '666-7777-8888', third.custom_field_value(phone_number_cf) | |
| 130 | end | |
| 131 | ||
| 132 | protected | |
| 133 | ||
| 134 | def generate_import(fixture_name='import_users.csv') | |
| 135 | import = UserImport.new | |
| 136 | import.user_id = 1 | |
| 137 | import.file = uploaded_test_file(fixture_name, 'text/csv') | |
| 138 | import.save! | |
| 139 | import | |
| 140 | end | |
| 141 | ||
| 142 | def generate_import_with_mapping(fixture_name='import_users.csv') | |
| 143 | import = generate_import(fixture_name) | |
| 144 | ||
| 145 |     import.settings = { | |
| 146 | 'separator' => ';', 'wrapper' => '"', 'encoding' => 'UTF-8', | |
| 147 |       'mapping' => { | |
| 148 | 'login' => '1', | |
| 149 | 'firstname' => '2', | |
| 150 | 'lastname' => '3', | |
| 151 | 'mail' => '4', | |
| 152 | 'language' => '5', | |
| 153 | 'admin' => '6', | |
| 154 | 'auth_source' => '7', | |
| 155 | 'password' => '8', | |
| 156 | 'must_change_passwd' => '9', | |
| 157 | 'status' => '10', | |
| 158 | } | |
| 159 | } | |
| 160 | import.save! | |
| 161 | import | |
| 162 | end | |
| 163 | end | |