##// END OF EJS Templates
Set default date format based on user locale (#950)....
Jean-Philippe Lang -
r14114:67ea285dea51
parent child
Show More
@@ -1,245 +1,249
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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 'csv'
19 19
20 20 class Import < ActiveRecord::Base
21 21 has_many :items, :class_name => 'ImportItem', :dependent => :delete_all
22 22 belongs_to :user
23 23 serialize :settings
24 24
25 25 before_destroy :remove_file
26 26
27 27 validates_presence_of :filename, :user_id
28 28 validates_length_of :filename, :maximum => 255
29 29
30 30 DATE_FORMATS = [
31 31 '%Y-%m-%d',
32 32 '%d/%m/%Y',
33 33 '%m/%d/%Y',
34 34 '%d.%m.%Y',
35 35 '%d-%m-%Y'
36 36 ]
37 37
38 38 def initialize(*args)
39 39 super
40 40 self.settings ||= {}
41 41 end
42 42
43 43 def file=(arg)
44 44 return unless arg.present? && arg.size > 0
45 45
46 46 self.filename = generate_filename
47 47 Redmine::Utils.save_upload(arg, filepath)
48 48 end
49 49
50 50 def set_default_settings
51 51 separator = lu(user, :general_csv_separator)
52 52 if file_exists?
53 53 begin
54 54 content = File.read(filepath, 256, "rb")
55 55 separator = [',', ';'].sort_by {|sep| content.count(sep) }.last
56 56 rescue Exception => e
57 57 end
58 58 end
59 59 wrapper = '"'
60 60 encoding = lu(user, :general_csv_encoding)
61 61
62 date_format = lu(user, "date.formats.default", :default => "foo")
63 date_format = DATE_FORMATS.first unless DATE_FORMATS.include?(date_format)
64
62 65 self.settings.merge!(
63 66 'separator' => separator,
64 67 'wrapper' => wrapper,
65 'encoding' => encoding
68 'encoding' => encoding,
69 'date_format' => date_format
66 70 )
67 71 end
68 72
69 73 def to_param
70 74 filename
71 75 end
72 76
73 77 # Returns the full path of the file to import
74 78 # It is stored in tmp/imports with a random hex as filename
75 79 def filepath
76 80 if filename.present? && filename =~ /\A[0-9a-f]+\z/
77 81 File.join(Rails.root, "tmp", "imports", filename)
78 82 else
79 83 nil
80 84 end
81 85 end
82 86
83 87 # Returns true if the file to import exists
84 88 def file_exists?
85 89 filepath.present? && File.exists?(filepath)
86 90 end
87 91
88 92 # Returns the headers as an array that
89 93 # can be used for select options
90 94 def columns_options(default=nil)
91 95 i = -1
92 96 headers.map {|h| [h, i+=1]}
93 97 end
94 98
95 99 # Parses the file to import and updates the total number of items
96 100 def parse_file
97 101 count = 0
98 102 read_items {|row, i| count=i}
99 103 update_attribute :total_items, count
100 104 count
101 105 end
102 106
103 107 # Reads the items to import and yields the given block for each item
104 108 def read_items
105 109 i = 0
106 110 headers = true
107 111 read_rows do |row|
108 112 if i == 0 && headers
109 113 headers = false
110 114 next
111 115 end
112 116 i+= 1
113 117 yield row, i if block_given?
114 118 end
115 119 end
116 120
117 121 # Returns the count first rows of the file (including headers)
118 122 def first_rows(count=4)
119 123 rows = []
120 124 read_rows do |row|
121 125 rows << row
122 126 break if rows.size >= count
123 127 end
124 128 rows
125 129 end
126 130
127 131 # Returns an array of headers
128 132 def headers
129 133 first_rows(1).first || []
130 134 end
131 135
132 136 # Returns the mapping options
133 137 def mapping
134 138 settings['mapping'] || {}
135 139 end
136 140
137 141 # Imports items and returns the position of the last processed item
138 142 def run(options={})
139 143 max_items = options[:max_items]
140 144 max_time = options[:max_time]
141 145 current = 0
142 146 imported = 0
143 147 resume_after = items.maximum(:position) || 0
144 148 interrupted = false
145 149 started_on = Time.now
146 150
147 151 read_items do |row, position|
148 152 if (max_items && imported >= max_items) || (max_time && Time.now >= started_on + max_time)
149 153 interrupted = true
150 154 break
151 155 end
152 156 if position > resume_after
153 157 item = items.build
154 158 item.position = position
155 159
156 160 if object = build_object(row)
157 161 if object.save
158 162 item.obj_id = object.id
159 163 else
160 164 item.message = object.errors.full_messages.join("\n")
161 165 end
162 166 end
163 167
164 168 item.save!
165 169 imported += 1
166 170 end
167 171 current = position
168 172 end
169 173
170 174 if imported == 0 || interrupted == false
171 175 if total_items.nil?
172 176 update_attribute :total_items, current
173 177 end
174 178 update_attribute :finished, true
175 179 remove_file
176 180 end
177 181
178 182 current
179 183 end
180 184
181 185 def unsaved_items
182 186 items.where(:obj_id => nil)
183 187 end
184 188
185 189 def saved_items
186 190 items.where("obj_id IS NOT NULL")
187 191 end
188 192
189 193 private
190 194
191 195 def read_rows
192 196 return unless file_exists?
193 197
194 198 csv_options = {:headers => false}
195 199 csv_options[:encoding] = settings['encoding'].to_s.presence || 'UTF-8'
196 200 separator = settings['separator'].to_s
197 201 csv_options[:col_sep] = separator if separator.size == 1
198 202 wrapper = settings['wrapper'].to_s
199 203 csv_options[:quote_char] = wrapper if wrapper.size == 1
200 204
201 205 CSV.foreach(filepath, csv_options) do |row|
202 206 yield row if block_given?
203 207 end
204 208 end
205 209
206 210 def row_value(row, key)
207 211 if index = mapping[key].presence
208 212 row[index.to_i].presence
209 213 end
210 214 end
211 215
212 216 def row_date(row, key)
213 217 if s = row_value(row, key)
214 218 format = settings['date_format']
215 219 format = DATE_FORMATS.first unless DATE_FORMATS.include?(format)
216 220 Date.strptime(s, format) rescue s
217 221 end
218 222 end
219 223
220 224 # Builds a record for the given row and returns it
221 225 # To be implemented by subclasses
222 226 def build_object(row)
223 227 end
224 228
225 229 # Generates a filename used to store the import file
226 230 def generate_filename
227 231 Redmine::Utils.random_hex(16)
228 232 end
229 233
230 234 # Deletes the import file
231 235 def remove_file
232 236 if file_exists?
233 237 begin
234 238 File.delete filepath
235 239 rescue Exception => e
236 240 logger.error "Unable to delete file #{filepath}: #{e.message}" if logger
237 241 end
238 242 end
239 243 end
240 244
241 245 # Returns true if value is a string that represents a true value
242 246 def yes?(value)
243 247 value == lu(user, :general_text_yes) || value == '1'
244 248 end
245 249 end
@@ -1,103 +1,113
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 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_parent_should_be_set
62 62 import = generate_import_with_mapping
63 63 import.mapping.merge!('parent_issue_id' => '5')
64 64 import.save!
65 65
66 66 issues = new_records(Issue, 3) { import.run }
67 67 assert_nil issues[0].parent
68 68 assert_equal issues[0].id, issues[1].parent_id
69 69 assert_equal 2, issues[2].parent_id
70 70 end
71 71
72 72 def test_is_private_should_be_set_based_on_user_locale
73 73 import = generate_import_with_mapping
74 74 import.mapping.merge!('is_private' => '6')
75 75 import.save!
76 76
77 77 issues = new_records(Issue, 3) { import.run }
78 78 assert_equal [false, true, false], issues.map(&:is_private)
79 79 end
80 80
81 81 def test_dates_should_be_parsed_using_date_format_setting
82 82 field = IssueCustomField.generate!(:field_format => 'date', :is_for_all => true, :trackers => Tracker.all)
83 83 import = generate_import_with_mapping('import_dates.csv')
84 84 import.settings.merge!('date_format' => Import::DATE_FORMATS[1])
85 85 import.mapping.merge!('subject' => '0', 'start_date' => '1', 'due_date' => '2', "cf_#{field.id}" => '3')
86 86 import.save!
87 87
88 88 issue = new_record(Issue) { import.run } # only 1 valid issue
89 89 assert_equal "Valid dates", issue.subject
90 90 assert_equal Date.parse('2015-07-10'), issue.start_date
91 91 assert_equal Date.parse('2015-08-12'), issue.due_date
92 92 assert_equal '2015-07-14', issue.custom_field_value(field)
93 93 end
94 94
95 def test_date_format_should_default_to_user_language
96 user = User.generate!(:language => 'fr')
97 import = Import.new
98 import.user = user
99 assert_nil import.settings['date_format']
100
101 import.set_default_settings
102 assert_equal '%d/%m/%Y', import.settings['date_format']
103 end
104
95 105 def test_run_should_remove_the_file
96 106 import = generate_import_with_mapping
97 107 file_path = import.filepath
98 108 assert File.exists?(file_path)
99 109
100 110 import.run
101 111 assert !File.exists?(file_path)
102 112 end
103 113 end
General Comments 0
You need to be logged in to leave comments. Login now