##// END OF EJS Templates
Make Status map-able for CSV import (#22951)....
Jean-Philippe Lang -
r15111:f6754a0f7a37
parent child
Show More
@@ -1,182 +1,186
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueImport < Import
19 19
20 20 # Returns the objects that were imported
21 21 def saved_objects
22 22 object_ids = saved_items.pluck(:obj_id)
23 23 objects = Issue.where(:id => object_ids).order(:id).preload(:tracker, :priority, :status)
24 24 end
25 25
26 26 # Returns a scope of projects that user is allowed to
27 27 # import issue to
28 28 def allowed_target_projects
29 29 Project.allowed_to(user, :import_issues)
30 30 end
31 31
32 32 def project
33 33 project_id = mapping['project_id'].to_i
34 34 allowed_target_projects.find_by_id(project_id) || allowed_target_projects.first
35 35 end
36 36
37 37 # Returns a scope of trackers that user is allowed to
38 38 # import issue to
39 39 def allowed_target_trackers
40 40 Issue.allowed_target_trackers(project, user)
41 41 end
42 42
43 43 def tracker
44 44 if mapping['tracker'].to_s =~ /\Avalue:(\d+)\z/
45 45 tracker_id = $1.to_i
46 46 allowed_target_trackers.find_by_id(tracker_id)
47 47 end
48 48 end
49 49
50 50 # Returns true if missing categories should be created during the import
51 51 def create_categories?
52 52 user.allowed_to?(:manage_categories, project) &&
53 53 mapping['create_categories'] == '1'
54 54 end
55 55
56 56 # Returns true if missing versions should be created during the import
57 57 def create_versions?
58 58 user.allowed_to?(:manage_versions, project) &&
59 59 mapping['create_versions'] == '1'
60 60 end
61 61
62 62 def mappable_custom_fields
63 63 if tracker
64 64 issue = Issue.new
65 65 issue.project = project
66 66 issue.tracker = tracker
67 67 issue.editable_custom_field_values(user).map(&:custom_field)
68 68 elsif project
69 69 project.all_issue_custom_fields
70 70 else
71 71 []
72 72 end
73 73 end
74 74
75 75 private
76 76
77 77 def build_object(row)
78 78 issue = Issue.new
79 79 issue.author = user
80 80 issue.notify = false
81 81
82 82 tracker_id = nil
83 83 if tracker
84 84 tracker_id = tracker.id
85 85 elsif tracker_name = row_value(row, 'tracker')
86 86 tracker_id = allowed_target_trackers.named(tracker_name).first.try(:id)
87 87 end
88 88
89 89 attributes = {
90 90 'project_id' => mapping['project_id'],
91 91 'tracker_id' => tracker_id,
92 92 'subject' => row_value(row, 'subject'),
93 93 'description' => row_value(row, 'description')
94 94 }
95 attributes
95 if status_name = row_value(row, 'status')
96 if status_id = IssueStatus.named(status_name).first.try(:id)
97 attributes['status_id'] = status_id
98 end
99 end
96 100 issue.send :safe_attributes=, attributes, user
97 101
98 102 attributes = {}
99 103 if priority_name = row_value(row, 'priority')
100 104 if priority_id = IssuePriority.active.named(priority_name).first.try(:id)
101 105 attributes['priority_id'] = priority_id
102 106 end
103 107 end
104 108 if issue.project && category_name = row_value(row, 'category')
105 109 if category = issue.project.issue_categories.named(category_name).first
106 110 attributes['category_id'] = category.id
107 111 elsif create_categories?
108 112 category = issue.project.issue_categories.build
109 113 category.name = category_name
110 114 if category.save
111 115 attributes['category_id'] = category.id
112 116 end
113 117 end
114 118 end
115 119 if assignee_name = row_value(row, 'assigned_to')
116 120 if assignee = Principal.detect_by_keyword(issue.assignable_users, assignee_name)
117 121 attributes['assigned_to_id'] = assignee.id
118 122 end
119 123 end
120 124 if issue.project && version_name = row_value(row, 'fixed_version')
121 125 if version = issue.project.versions.named(version_name).first
122 126 attributes['fixed_version_id'] = version.id
123 127 elsif create_versions?
124 128 version = issue.project.versions.build
125 129 version.name = version_name
126 130 if version.save
127 131 attributes['fixed_version_id'] = version.id
128 132 end
129 133 end
130 134 end
131 135 if is_private = row_value(row, 'is_private')
132 136 if yes?(is_private)
133 137 attributes['is_private'] = '1'
134 138 end
135 139 end
136 140 if parent_issue_id = row_value(row, 'parent_issue_id')
137 141 if parent_issue_id =~ /\A(#)?(\d+)\z/
138 142 parent_issue_id = $2
139 143 if $1
140 144 attributes['parent_issue_id'] = parent_issue_id
141 145 elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id)
142 146 attributes['parent_issue_id'] = issue_id
143 147 end
144 148 else
145 149 attributes['parent_issue_id'] = parent_issue_id
146 150 end
147 151 end
148 152 if start_date = row_date(row, 'start_date')
149 153 attributes['start_date'] = start_date
150 154 end
151 155 if due_date = row_date(row, 'due_date')
152 156 attributes['due_date'] = due_date
153 157 end
154 158 if estimated_hours = row_value(row, 'estimated_hours')
155 159 attributes['estimated_hours'] = estimated_hours
156 160 end
157 161 if done_ratio = row_value(row, 'done_ratio')
158 162 attributes['done_ratio'] = done_ratio
159 163 end
160 164
161 165 attributes['custom_field_values'] = issue.custom_field_values.inject({}) do |h, v|
162 166 value = case v.custom_field.field_format
163 167 when 'date'
164 168 row_date(row, "cf_#{v.custom_field.id}")
165 169 else
166 170 row_value(row, "cf_#{v.custom_field.id}")
167 171 end
168 172 if value
169 173 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, issue)
170 174 end
171 175 h
172 176 end
173 177
174 178 issue.send :safe_attributes=, attributes, user
175 179
176 180 if issue.tracker_id != tracker_id
177 181 issue.tracker_id = nil
178 182 end
179 183
180 184 issue
181 185 end
182 186 end
@@ -1,86 +1,90
1 1 <p>
2 2 <label><%= l(:label_project) %></label>
3 3 <%= select_tag 'import_settings[mapping][project_id]',
4 4 options_for_select(project_tree_options_for_select(@import.allowed_target_projects, :selected => @import.project)),
5 5 :id => 'import_mapping_project_id' %>
6 6 </p>
7 7 <p>
8 8 <label><%= l(:label_tracker) %></label>
9 9 <%= mapping_select_tag @import, 'tracker', :required => true,
10 10 :values => @import.allowed_target_trackers.sorted.map {|t| [t.name, t.id]} %>
11 11 </p>
12 <p>
13 <label><%= l(:field_status) %></label>
14 <%= mapping_select_tag @import, 'status' %>
15 </p>
12 16
13 17 <div class="splitcontent">
14 18 <div class="splitcontentleft">
15 19 <p>
16 20 <label><%= l(:field_subject) %></label>
17 21 <%= mapping_select_tag @import, 'subject', :required => true %>
18 22 </p>
19 23 <p>
20 24 <label><%= l(:field_description) %></label>
21 25 <%= mapping_select_tag @import, 'description' %>
22 26 </p>
23 27 <p>
24 28 <label><%= l(:field_priority) %></label>
25 29 <%= mapping_select_tag @import, 'priority' %>
26 30 </p>
27 31 <p>
28 32 <label><%= l(:field_category) %></label>
29 33 <%= mapping_select_tag @import, 'category' %>
30 34 <% if User.current.allowed_to?(:manage_categories, @import.project) %>
31 35 <label class="block">
32 36 <%= check_box_tag 'import_settings[mapping][create_categories]', '1', @import.create_categories? %>
33 37 <%= l(:label_create_missing_values) %>
34 38 </label>
35 39 <% end %>
36 40 </p>
37 41 <p>
38 42 <label><%= l(:field_assigned_to) %></label>
39 43 <%= mapping_select_tag @import, 'assigned_to' %>
40 44 </p>
41 45 <p>
42 46 <label><%= l(:field_fixed_version) %></label>
43 47 <%= mapping_select_tag @import, 'fixed_version' %>
44 48 <% if User.current.allowed_to?(:manage_versions, @import.project) %>
45 49 <label class="block">
46 50 <%= check_box_tag 'import_settings[mapping][create_versions]', '1', @import.create_versions? %>
47 51 <%= l(:label_create_missing_values) %>
48 52 </label>
49 53 <% end %>
50 54 </p>
51 55 <% @custom_fields.each do |field| %>
52 56 <p>
53 57 <label><%= field.name %></label>
54 58 <%= mapping_select_tag @import, "cf_#{field.id}" %>
55 59 </p>
56 60 <% end %>
57 61 </div>
58 62
59 63 <div class="splitcontentright">
60 64 <p>
61 65 <label><%= l(:field_is_private) %></label>
62 66 <%= mapping_select_tag @import, 'is_private' %>
63 67 </p>
64 68 <p>
65 69 <label><%= l(:field_parent_issue) %></label>
66 70 <%= mapping_select_tag @import, 'parent_issue_id' %>
67 71 </p>
68 72 <p>
69 73 <label><%= l(:field_start_date) %></label>
70 74 <%= mapping_select_tag @import, 'start_date' %>
71 75 </p>
72 76 <p>
73 77 <label><%= l(:field_due_date) %></label>
74 78 <%= mapping_select_tag @import, 'due_date' %>
75 79 </p>
76 80 <p>
77 81 <label><%= l(:field_estimated_hours) %></label>
78 82 <%= mapping_select_tag @import, 'estimated_hours' %>
79 83 </p>
80 84 <p>
81 85 <label><%= l(:field_done_ratio) %></label>
82 86 <%= mapping_select_tag @import, 'done_ratio' %>
83 87 </p>
84 88 </div>
85 89 </div>
86 90
@@ -1,4 +1,4
1 priority;subject;description;start_date;due_date;parent;private;progress;custom;version;category;user;estimated_hours;tracker
2 High;First;First description;2015-07-08;2015-08-25;;no;;PostgreSQL;;New category;dlopper;1;bug
3 Normal;Child 1;Child description;;;1;yes;10;MySQL;2.0;New category;;2;feature request
4 Normal;Child of existing issue;Child description;;;#2;no;20;;2.1;Printing;;3;bug
1 priority;subject;description;start_date;due_date;parent;private;progress;custom;version;category;user;estimated_hours;tracker;status
2 High;First;First description;2015-07-08;2015-08-25;;no;;PostgreSQL;;New category;dlopper;1;bug;new
3 Normal;Child 1;Child description;;;1;yes;10;MySQL;2.0;New category;;2;feature request;new
4 Normal;Child of existing issue;Child description;;;#2;no;20;;2.1;Printing;;3;bug;assigned
@@ -1,163 +1,172
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssueImportTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules,
22 22 :users, :email_addresses,
23 23 :roles, :members, :member_roles,
24 24 :issues, :issue_statuses,
25 25 :trackers, :projects_trackers,
26 26 :versions,
27 27 :issue_categories,
28 28 :enumerations,
29 29 :workflows,
30 30 :custom_fields,
31 31 :custom_values,
32 32 :custom_fields_projects,
33 33 :custom_fields_trackers
34 34
35 35 def test_create_versions_should_create_missing_versions
36 36 import = generate_import_with_mapping
37 37 import.mapping.merge!('fixed_version' => '9', 'create_versions' => '1')
38 38 import.save!
39 39
40 40 version = new_record(Version) do
41 41 assert_difference 'Issue.count', 3 do
42 42 import.run
43 43 end
44 44 end
45 45 assert_equal '2.1', version.name
46 46 end
47 47
48 48 def test_create_categories_should_create_missing_categories
49 49 import = generate_import_with_mapping
50 50 import.mapping.merge!('category' => '10', 'create_categories' => '1')
51 51 import.save!
52 52
53 53 category = new_record(IssueCategory) do
54 54 assert_difference 'Issue.count', 3 do
55 55 import.run
56 56 end
57 57 end
58 58 assert_equal 'New category', category.name
59 59 end
60 60
61 61 def test_mapping_with_fixed_tracker
62 62 import = generate_import_with_mapping
63 63 import.mapping.merge!('tracker' => 'value:2')
64 64 import.save!
65 65
66 66 issues = new_records(Issue, 3) { import.run }
67 67 assert_equal [2], issues.map(&:tracker_id).uniq
68 68 end
69 69
70 70 def test_mapping_with_mapped_tracker
71 71 import = generate_import_with_mapping
72 72 import.mapping.merge!('tracker' => '13')
73 73 import.save!
74 74
75 75 issues = new_records(Issue, 3) { import.run }
76 76 assert_equal [1, 2, 1], issues.map(&:tracker_id)
77 77 end
78 78
79 79 def test_should_not_import_with_default_tracker_when_tracker_is_invalid
80 80 Tracker.find_by_name('Feature request').update!(:name => 'Feature')
81 81
82 82 import = generate_import_with_mapping
83 83 import.mapping.merge!('tracker' => '13')
84 84 import.save!
85 85 import.run
86 86
87 87 assert_equal 1, import.unsaved_items.count
88 88 item = import.unsaved_items.first
89 89 assert_include "Tracker cannot be blank", item.message
90 90 end
91 91
92 def test_status_should_be_set
93 import = generate_import_with_mapping
94 import.mapping.merge!('status' => '14')
95 import.save!
96
97 issues = new_records(Issue, 3) { import.run }
98 assert_equal ['New', 'New', 'Assigned'], issues.map(&:status).map(&:name)
99 end
100
92 101 def test_parent_should_be_set
93 102 import = generate_import_with_mapping
94 103 import.mapping.merge!('parent_issue_id' => '5')
95 104 import.save!
96 105
97 106 issues = new_records(Issue, 3) { import.run }
98 107 assert_nil issues[0].parent
99 108 assert_equal issues[0].id, issues[1].parent_id
100 109 assert_equal 2, issues[2].parent_id
101 110 end
102 111
103 112 def test_assignee_should_be_set
104 113 import = generate_import_with_mapping
105 114 import.mapping.merge!('assigned_to' => '11')
106 115 import.save!
107 116
108 117 issues = new_records(Issue, 3) { import.run }
109 118 assert_equal [User.find(3), nil, nil], issues.map(&:assigned_to)
110 119 end
111 120
112 121 def test_user_custom_field_should_be_set
113 122 field = IssueCustomField.generate!(:field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
114 123 import = generate_import_with_mapping
115 124 import.mapping.merge!("cf_#{field.id}" => '11')
116 125 import.save!
117 126
118 127 issues = new_records(Issue, 3) { import.run }
119 128 assert_equal '3', issues.first.custom_field_value(field)
120 129 end
121 130
122 131 def test_is_private_should_be_set_based_on_user_locale
123 132 import = generate_import_with_mapping
124 133 import.mapping.merge!('is_private' => '6')
125 134 import.save!
126 135
127 136 issues = new_records(Issue, 3) { import.run }
128 137 assert_equal [false, true, false], issues.map(&:is_private)
129 138 end
130 139
131 140 def test_dates_should_be_parsed_using_date_format_setting
132 141 field = IssueCustomField.generate!(:field_format => 'date', :is_for_all => true, :trackers => Tracker.all)
133 142 import = generate_import_with_mapping('import_dates.csv')
134 143 import.settings.merge!('date_format' => Import::DATE_FORMATS[1])
135 144 import.mapping.merge!('tracker' => 'value:1', 'subject' => '0', 'start_date' => '1', 'due_date' => '2', "cf_#{field.id}" => '3')
136 145 import.save!
137 146
138 147 issue = new_record(Issue) { import.run } # only 1 valid issue
139 148 assert_equal "Valid dates", issue.subject
140 149 assert_equal Date.parse('2015-07-10'), issue.start_date
141 150 assert_equal Date.parse('2015-08-12'), issue.due_date
142 151 assert_equal '2015-07-14', issue.custom_field_value(field)
143 152 end
144 153
145 154 def test_date_format_should_default_to_user_language
146 155 user = User.generate!(:language => 'fr')
147 156 import = Import.new
148 157 import.user = user
149 158 assert_nil import.settings['date_format']
150 159
151 160 import.set_default_settings
152 161 assert_equal '%d/%m/%Y', import.settings['date_format']
153 162 end
154 163
155 164 def test_run_should_remove_the_file
156 165 import = generate_import_with_mapping
157 166 file_path = import.filepath
158 167 assert File.exists?(file_path)
159 168
160 169 import.run
161 170 assert !File.exists?(file_path)
162 171 end
163 172 end
General Comments 0
You need to be logged in to leave comments. Login now