浏览代码

Import user accounts from CSV file (#33102).

Patch by Takenori TAKAKI.


git-svn-id: http://svn.redmine.org/redmine/trunk@19799 e93f8b46-1217-0410-a6f0-8f06a7374b81
tags/4.2.0
Go MAEDA 4 年前
父节点
当前提交
ebf5d219c9

+ 10
- 0
app/helpers/application_helper.rb 查看文件

@@ -1700,6 +1700,16 @@ module ApplicationHelper
}
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

private

def wiki_helper

+ 118
- 0
app/models/user_import.rb 查看文件

@@ -0,0 +1,118 @@
# 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.each_with_object({}) do |v, h|
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
end

object.send(:safe_attributes=, attributes, user)
object
end
end

+ 53
- 0
app/views/imports/_users_fields_mapping.html.erb 查看文件

@@ -0,0 +1,53 @@
<div class="splitcontent">
<div class="splitcontentleft">
<p>
<label for="import_mapping_login"><%= l(:field_login) %></label>
<%= mapping_select_tag @import, 'login', :required => true %>
</p>
<p>
<label for="import_mapping_firstname"><%= l(:field_firstname) %></label>
<%= mapping_select_tag @import, 'firstname', :required => true %>
</p>
<p>
<label for="import_mapping_lastname"><%= l(:field_lastname) %></label>
<%= mapping_select_tag @import, 'lastname', :required => true %>
</p>
<p>
<label for="import_mapping_mail"><%= l(:field_mail) %></label>
<%= mapping_select_tag @import, 'mail' %>
</p>
<p>
<label for="import_mapping_language"><%= l(:field_language) %></label>
<%= mapping_select_tag @import, 'language' %>
</p>
<p>
<label for="import_mapping_admin"><%= l(:field_admin) %></label>
<%= mapping_select_tag @import, 'admin' %>
</p>
<p>
<label for="import_mapping_auth_source_id"><%= l(:field_auth_source) %></label>
<%= mapping_select_tag @import, 'auth_source' %>
</p>
<p>
<label for="import_mapping_password"><%= l(:field_password) %></label>
<%= mapping_select_tag @import, 'password' %>
</p>
<p>
<label for="import_mapping_must_change_passwd"><%= l(:field_must_change_passwd) %></label>
<%= mapping_select_tag @import, 'must_change_passwd' %>
</p>
<p>
<label for="import_mapping_status"><%= l(:field_status) %></label>
<%= mapping_select_tag @import, 'status' %>
</p>
</div>

<div class="splitcontentright">
<% @custom_fields.each do |field| %>
<p>
<label for="import_mapping_cf_<%= field.id %>"><%= field.name %></label>
<%= mapping_select_tag @import, "cf_#{field.id}", :required => field.is_required? %>
</p>
<% end %>
</div>
</div>

+ 6
- 0
app/views/imports/_users_mapping.html.erb 查看文件

@@ -0,0 +1,6 @@
<fieldset class="box tabular">
<legend><%= l(:label_fields_mapping) %></legend>
<div id="fields-mapping">
<%= render :partial => 'users_fields_mapping' %>
</div>
</fieldset>

+ 1
- 0
app/views/imports/_users_mapping.js.erb 查看文件

@@ -0,0 +1 @@
$('#fields-mapping').html('<%= escape_javascript(render :partial => 'users_fields_mapping') %>');

+ 24
- 0
app/views/imports/_users_saved_objects.html.erb 查看文件

@@ -0,0 +1,24 @@
<table id="saved-items" class="list">
<thead>
<tr>
<th><%= t(:field_login) %></th>
<th><%= t(:field_firstname) %></th>
<th><%= t(:field_lastname) %></th>
<th><%= t(:field_mail) %></th>
<th><%= t(:field_admin) %></th>
<th><%= t(:field_status) %></th>
</tr>
</thead>
<tbody>
<% saved_objects.each do |user| %>
<tr>
<td><%= avatar(user, :size => "14") %><%= link_to user.login, edit_user_path(user) %></td>
<td><%= user.firstname %></td>
<td><%= user.lastname %></td>
<td><%= mail_to(user.mail) %></td>
<td><%= checked_image user.admin? %></td>
<td><%= l(("status_#{User::LABEL_BY_STATUS[user.status]}")) %>
</tr>
<% end %>
</tbody>
</table>

+ 1
- 1
app/views/imports/mapping.html.erb 查看文件

@@ -23,7 +23,7 @@
</p>
<% end %>

<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %>

<%= javascript_tag do %>
$(document).ready(function() {

+ 1
- 1
app/views/imports/new.html.erb 查看文件

@@ -12,4 +12,4 @@
<p><%= submit_tag l(:label_next).html_safe + " &#187;".html_safe, :name => nil %></p>
<% end %>

<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %>

+ 1
- 1
app/views/imports/run.html.erb 查看文件

@@ -4,7 +4,7 @@
<div id="import-progress"><div id="progress-label">0 / <%= @import.total_items.to_i %></div></div>
</div>

<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %>

<%= javascript_tag do %>
$(document).ready(function() {

+ 1
- 1
app/views/imports/settings.html.erb 查看文件

@@ -31,4 +31,4 @@
<p><%= submit_tag l(:label_next).html_safe + " &#187;".html_safe, :name => nil %></p>
<% end %>

<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %>

+ 1
- 1
app/views/imports/show.html.erb 查看文件

@@ -27,4 +27,4 @@
</table>
<% end %>

<%= render :partial => "#{import_partial_prefix}_sidebar" %>
<%= render_if_exist :partial => "#{import_partial_prefix}_sidebar" %>

+ 5
- 0
app/views/users/index.html.erb 查看文件

@@ -1,5 +1,10 @@
<div class="contextual">
<%= link_to l(:label_user_new), new_user_path, :class => 'icon icon-add' %>
<%= actions_dropdown do %>
<% if User.current.allowed_to?(:import_users, nil, :global => true) %>
<%= link_to l(:button_import), new_users_import_path, :class => 'icon icon-import' %>
<% end %>
<% end %>
</div>

<h2><%=l(:label_user_plural)%></h2>

+ 1
- 0
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

+ 1
- 0
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'

+ 4
- 0
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

+ 1
- 0
test/fixtures/views/_partial.html.erb 查看文件

@@ -0,0 +1 @@
partial html

+ 10
- 0
test/helpers/application_helper_test.rb 查看文件

@@ -1911,6 +1911,16 @@ class ApplicationHelperTest < Redmine::HelperTest
assert_match(/name="new_issue-[a-z0-9]{8}"/, labelled_form_for(Issue.new){})
end

def test_redner_if_exist_should_be_render_partial
controller.prepend_view_path "test/fixtures/views"
assert_equal "partial html\n", render_if_exist(:partial => 'partial')
end

def test_redner_if_exist_should_be_render_nil
controller.prepend_view_path "test/fixtures/views"
assert_nil render_if_exist(:partial => 'non_exist_partial')
end

private

def wiki_links_with_special_characters

+ 162
- 0
test/unit/user_import_test.rb 查看文件

@@ -0,0 +1,162 @@
# 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["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

正在加载...
取消
保存