You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

import.rb 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # Redmine - project management software
  2. # Copyright (C) 2006-2017 Jean-Philippe Lang
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; either version 2
  7. # of the License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  17. require 'csv'
  18. class Import < ActiveRecord::Base
  19. has_many :items, :class_name => 'ImportItem', :dependent => :delete_all
  20. belongs_to :user
  21. serialize :settings
  22. before_destroy :remove_file
  23. validates_presence_of :filename, :user_id
  24. validates_length_of :filename, :maximum => 255
  25. DATE_FORMATS = [
  26. '%Y-%m-%d',
  27. '%d/%m/%Y',
  28. '%m/%d/%Y',
  29. '%d.%m.%Y',
  30. '%d-%m-%Y'
  31. ]
  32. def initialize(*args)
  33. super
  34. self.settings ||= {}
  35. end
  36. def file=(arg)
  37. return unless arg.present? && arg.size > 0
  38. self.filename = generate_filename
  39. Redmine::Utils.save_upload(arg, filepath)
  40. end
  41. def set_default_settings
  42. separator = lu(user, :general_csv_separator)
  43. if file_exists?
  44. begin
  45. content = File.read(filepath, 256)
  46. separator = [',', ';'].sort_by {|sep| content.count(sep) }.last
  47. rescue Exception => e
  48. end
  49. end
  50. wrapper = '"'
  51. encoding = lu(user, :general_csv_encoding)
  52. date_format = lu(user, "date.formats.default", :default => "foo")
  53. date_format = DATE_FORMATS.first unless DATE_FORMATS.include?(date_format)
  54. self.settings.merge!(
  55. 'separator' => separator,
  56. 'wrapper' => wrapper,
  57. 'encoding' => encoding,
  58. 'date_format' => date_format
  59. )
  60. end
  61. def to_param
  62. filename
  63. end
  64. # Returns the full path of the file to import
  65. # It is stored in tmp/imports with a random hex as filename
  66. def filepath
  67. if filename.present? && filename =~ /\A[0-9a-f]+\z/
  68. File.join(Rails.root, "tmp", "imports", filename)
  69. else
  70. nil
  71. end
  72. end
  73. # Returns true if the file to import exists
  74. def file_exists?
  75. filepath.present? && File.exists?(filepath)
  76. end
  77. # Returns the headers as an array that
  78. # can be used for select options
  79. def columns_options(default=nil)
  80. i = -1
  81. headers.map {|h| [h, i+=1]}
  82. end
  83. # Parses the file to import and updates the total number of items
  84. def parse_file
  85. count = 0
  86. read_items {|row, i| count=i}
  87. update_attribute :total_items, count
  88. count
  89. end
  90. # Reads the items to import and yields the given block for each item
  91. def read_items
  92. i = 0
  93. headers = true
  94. read_rows do |row|
  95. if i == 0 && headers
  96. headers = false
  97. next
  98. end
  99. i+= 1
  100. yield row, i if block_given?
  101. end
  102. end
  103. # Returns the count first rows of the file (including headers)
  104. def first_rows(count=4)
  105. rows = []
  106. read_rows do |row|
  107. rows << row
  108. break if rows.size >= count
  109. end
  110. rows
  111. end
  112. # Returns an array of headers
  113. def headers
  114. first_rows(1).first || []
  115. end
  116. # Returns the mapping options
  117. def mapping
  118. settings['mapping'] || {}
  119. end
  120. # Adds a callback that will be called after the item at given position is imported
  121. def add_callback(position, name, *args)
  122. settings['callbacks'] ||= {}
  123. settings['callbacks'][position.to_i] ||= []
  124. settings['callbacks'][position.to_i] << [name, args]
  125. save!
  126. end
  127. # Executes the callbacks for the given object
  128. def do_callbacks(position, object)
  129. if callbacks = (settings['callbacks'] || {}).delete(position)
  130. callbacks.each do |name, args|
  131. send "#{name}_callback", object, *args
  132. end
  133. save!
  134. end
  135. end
  136. # Imports items and returns the position of the last processed item
  137. def run(options={})
  138. max_items = options[:max_items]
  139. max_time = options[:max_time]
  140. current = 0
  141. imported = 0
  142. resume_after = items.maximum(:position) || 0
  143. interrupted = false
  144. started_on = Time.now
  145. read_items do |row, position|
  146. if (max_items && imported >= max_items) || (max_time && Time.now >= started_on + max_time)
  147. interrupted = true
  148. break
  149. end
  150. if position > resume_after
  151. item = items.build
  152. item.position = position
  153. if object = build_object(row, item)
  154. if object.save
  155. item.obj_id = object.id
  156. else
  157. item.message = object.errors.full_messages.join("\n")
  158. end
  159. end
  160. item.save!
  161. imported += 1
  162. do_callbacks(item.position, object)
  163. end
  164. current = position
  165. end
  166. if imported == 0 || interrupted == false
  167. if total_items.nil?
  168. update_attribute :total_items, current
  169. end
  170. update_attribute :finished, true
  171. remove_file
  172. end
  173. current
  174. end
  175. def unsaved_items
  176. items.where(:obj_id => nil)
  177. end
  178. def saved_items
  179. items.where("obj_id IS NOT NULL")
  180. end
  181. private
  182. def read_rows
  183. return unless file_exists?
  184. csv_options = {:headers => false}
  185. csv_options[:encoding] = settings['encoding'].to_s.presence || 'UTF-8'
  186. separator = settings['separator'].to_s
  187. csv_options[:col_sep] = separator if separator.size == 1
  188. wrapper = settings['wrapper'].to_s
  189. csv_options[:quote_char] = wrapper if wrapper.size == 1
  190. CSV.foreach(filepath, csv_options) do |row|
  191. yield row if block_given?
  192. end
  193. end
  194. def row_value(row, key)
  195. if index = mapping[key].presence
  196. row[index.to_i].presence
  197. end
  198. end
  199. def row_date(row, key)
  200. if s = row_value(row, key)
  201. format = settings['date_format']
  202. format = DATE_FORMATS.first unless DATE_FORMATS.include?(format)
  203. Date.strptime(s, format) rescue s
  204. end
  205. end
  206. # Builds a record for the given row and returns it
  207. # To be implemented by subclasses
  208. def build_object(row)
  209. end
  210. # Generates a filename used to store the import file
  211. def generate_filename
  212. Redmine::Utils.random_hex(16)
  213. end
  214. # Deletes the import file
  215. def remove_file
  216. if file_exists?
  217. begin
  218. File.delete filepath
  219. rescue Exception => e
  220. logger.error "Unable to delete file #{filepath}: #{e.message}" if logger
  221. end
  222. end
  223. end
  224. # Returns true if value is a string that represents a true value
  225. def yes?(value)
  226. value == lu(user, :general_text_yes) || value == '1'
  227. end
  228. end