##// END OF EJS Templates
CSV import delimiter detection broken (#22583)....
Jean-Philippe Lang -
r14975:637676291778
parent child
Show More
@@ -1,249 +1,249
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 '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 content = File.read(filepath, 256, "rb")
54 content = File.read(filepath, 256)
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 62 date_format = lu(user, "date.formats.default", :default => "foo")
63 63 date_format = DATE_FORMATS.first unless DATE_FORMATS.include?(date_format)
64 64
65 65 self.settings.merge!(
66 66 'separator' => separator,
67 67 'wrapper' => wrapper,
68 68 'encoding' => encoding,
69 69 'date_format' => date_format
70 70 )
71 71 end
72 72
73 73 def to_param
74 74 filename
75 75 end
76 76
77 77 # Returns the full path of the file to import
78 78 # It is stored in tmp/imports with a random hex as filename
79 79 def filepath
80 80 if filename.present? && filename =~ /\A[0-9a-f]+\z/
81 81 File.join(Rails.root, "tmp", "imports", filename)
82 82 else
83 83 nil
84 84 end
85 85 end
86 86
87 87 # Returns true if the file to import exists
88 88 def file_exists?
89 89 filepath.present? && File.exists?(filepath)
90 90 end
91 91
92 92 # Returns the headers as an array that
93 93 # can be used for select options
94 94 def columns_options(default=nil)
95 95 i = -1
96 96 headers.map {|h| [h, i+=1]}
97 97 end
98 98
99 99 # Parses the file to import and updates the total number of items
100 100 def parse_file
101 101 count = 0
102 102 read_items {|row, i| count=i}
103 103 update_attribute :total_items, count
104 104 count
105 105 end
106 106
107 107 # Reads the items to import and yields the given block for each item
108 108 def read_items
109 109 i = 0
110 110 headers = true
111 111 read_rows do |row|
112 112 if i == 0 && headers
113 113 headers = false
114 114 next
115 115 end
116 116 i+= 1
117 117 yield row, i if block_given?
118 118 end
119 119 end
120 120
121 121 # Returns the count first rows of the file (including headers)
122 122 def first_rows(count=4)
123 123 rows = []
124 124 read_rows do |row|
125 125 rows << row
126 126 break if rows.size >= count
127 127 end
128 128 rows
129 129 end
130 130
131 131 # Returns an array of headers
132 132 def headers
133 133 first_rows(1).first || []
134 134 end
135 135
136 136 # Returns the mapping options
137 137 def mapping
138 138 settings['mapping'] || {}
139 139 end
140 140
141 141 # Imports items and returns the position of the last processed item
142 142 def run(options={})
143 143 max_items = options[:max_items]
144 144 max_time = options[:max_time]
145 145 current = 0
146 146 imported = 0
147 147 resume_after = items.maximum(:position) || 0
148 148 interrupted = false
149 149 started_on = Time.now
150 150
151 151 read_items do |row, position|
152 152 if (max_items && imported >= max_items) || (max_time && Time.now >= started_on + max_time)
153 153 interrupted = true
154 154 break
155 155 end
156 156 if position > resume_after
157 157 item = items.build
158 158 item.position = position
159 159
160 160 if object = build_object(row)
161 161 if object.save
162 162 item.obj_id = object.id
163 163 else
164 164 item.message = object.errors.full_messages.join("\n")
165 165 end
166 166 end
167 167
168 168 item.save!
169 169 imported += 1
170 170 end
171 171 current = position
172 172 end
173 173
174 174 if imported == 0 || interrupted == false
175 175 if total_items.nil?
176 176 update_attribute :total_items, current
177 177 end
178 178 update_attribute :finished, true
179 179 remove_file
180 180 end
181 181
182 182 current
183 183 end
184 184
185 185 def unsaved_items
186 186 items.where(:obj_id => nil)
187 187 end
188 188
189 189 def saved_items
190 190 items.where("obj_id IS NOT NULL")
191 191 end
192 192
193 193 private
194 194
195 195 def read_rows
196 196 return unless file_exists?
197 197
198 198 csv_options = {:headers => false}
199 199 csv_options[:encoding] = settings['encoding'].to_s.presence || 'UTF-8'
200 200 separator = settings['separator'].to_s
201 201 csv_options[:col_sep] = separator if separator.size == 1
202 202 wrapper = settings['wrapper'].to_s
203 203 csv_options[:quote_char] = wrapper if wrapper.size == 1
204 204
205 205 CSV.foreach(filepath, csv_options) do |row|
206 206 yield row if block_given?
207 207 end
208 208 end
209 209
210 210 def row_value(row, key)
211 211 if index = mapping[key].presence
212 212 row[index.to_i].presence
213 213 end
214 214 end
215 215
216 216 def row_date(row, key)
217 217 if s = row_value(row, key)
218 218 format = settings['date_format']
219 219 format = DATE_FORMATS.first unless DATE_FORMATS.include?(format)
220 220 Date.strptime(s, format) rescue s
221 221 end
222 222 end
223 223
224 224 # Builds a record for the given row and returns it
225 225 # To be implemented by subclasses
226 226 def build_object(row)
227 227 end
228 228
229 229 # Generates a filename used to store the import file
230 230 def generate_filename
231 231 Redmine::Utils.random_hex(16)
232 232 end
233 233
234 234 # Deletes the import file
235 235 def remove_file
236 236 if file_exists?
237 237 begin
238 238 File.delete filepath
239 239 rescue Exception => e
240 240 logger.error "Unable to delete file #{filepath}: #{e.message}" if logger
241 241 end
242 242 end
243 243 end
244 244
245 245 # Returns true if value is a string that represents a true value
246 246 def yes?(value)
247 247 value == lu(user, :general_text_yes) || value == '1'
248 248 end
249 249 end
General Comments 0
You need to be logged in to leave comments. Login now