import_from_csv.rake

script with some fixes - Andrey Anufrienko, 2013-08-23 13:32

Download (6.75 KB)

 
1
# Redmine - project management software
2
# Copyright (C) 2006-2013  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

    
18
#require 'iconv' if RUBY_VERSION < '1.9'
19
require 'pp'
20

    
21
desc <<-END_DESC
22
Import users, projects, and issues from CSV files.
23

24
Available options (provide at least one) :
25
  * users    => the path to a CSV file with one user per line
26
  * projects => the path to a CSV file with one project per line
27
  * issues   => the path to a CSV file with one issue per line
28

29
Example:
30
  rake redmine:import_from_csv users=/tmp/users.csv projects=/tmp/projects.csv issues=/tmp/issues.csv RAILS_ENV=production
31
END_DESC
32

    
33
namespace :redmine do
34
  task :import_from_csv => :environment do
35

    
36
    #go in rails root so that files paths can be relative
37
    Dir.chdir(Rails.root)
38

    
39
    class CSVImport
40
      attr_accessor :options, :content, :mappings
41

    
42
      def initialize(options)
43
        @options = options
44
        @content = {}
45
        @mappings = {}
46
      end
47

    
48
      # Usage help
49
      def self.usage
50
        $stderr.puts 'Missing options! Try: rake -D redmine:import_from_csv to get some help.'
51
        exit 1
52
      end
53

    
54
      # Global validation step
55
      def validate
56
        validate_params
57
        parse_files
58
        process_mappings
59
      end
60

    
61
      # Validate params and exits if user don't say "y"
62
      def validate_params
63
        puts
64
        puts "You're about to import data in your '#{Rails.env}' instance."
65
        puts "You'll use the following source files:"
66
        puts "  users: #{options['users'] || '-'} "
67
        puts "  projects: #{options['projects'] || '-'}"
68
        puts "  issues: #{options['issues'] || '-'}"
69
        puts
70
        puts "/!\\ Make sure to have a backup of your database before continuing."
71
        puts
72
        print 'Is this ok ? [y/n]: '
73
        STDOUT.flush
74
        ok = STDIN.gets.chomp!
75
        exit 2 if ok != 'y'
76
        puts
77
      end
78

    
79
      # Try to read each file and parse its CSV content
80
      def parse_files
81
        options.each do |type, filename|
82
          begin
83
            content[type] = FCSV.read(filename)
84
          rescue CSV::MalformedCSVError
85
            $stderr.puts "Error parsing #{filename}: #{$!.message}"
86
            exit 1
87
          rescue Errno::ENOENT, Errno::EACCES
88
            $stderr.puts "Error reading #{filename}: #{$!.message}"
89
            exit 1
90
          end
91
        end
92
      end
93

    
94
      # Validates if fields exist
95
      def process_mappings
96
        errors = 0
97
        content.each do |type, lines|
98
          fields = lines.shift
99
          klass = type.classify.constantize
100
          mappings[type] = []
101
          fields.each do |field|
102
            next if field == "project_identifier" && type == "issues"
103
            if field.match(/^customfield(\d+)$/)
104
              cf = CustomField.where(:type => "#{klass}CustomField", :id => $1).first
105
              if cf.present?
106
                mappings[type] << cf
107
              else
108
                $stderr.puts "Unable to find CustomField with type=#{klass}CustomField and id=#{$1}"
109
                errors += 1
110
              end
111
            else
112
              if klass.column_names.include?(field) || klass.instance_methods.include?(:"#{field}=")
113
                mappings[type] << field
114
              else
115
                $stderr.puts "No field #{klass}##{field}"
116
                errors += 1
117
              end
118
            end
119
          end
120
        end
121
        exit 1 if errors > 0
122
      end
123

    
124
      # Runs the migration
125
      def run
126
        errors = []
127
        puts
128
        puts "Starting data import."
129
        puts
130
        %w(users projects issues).each do |type|
131
          next unless content[type]
132
          klass = type.classify.constantize
133
          print "#{klass}: "
134
          my_custom_fields = Array.new
135
          content[type].each do |attributes|
136
            object = klass.new
137
            object.tracker = Tracker.first if klass == Issue
138
            attributes.each_with_index do |value, index|
139
              field = mappings[type][index]
140
              if type == "issues" && field == "project_identifier"
141
                object.project_id = Project.where("name = ? or name = ?", value, value).first
142
              elsif field.is_a?(String)
143
                object.send("#{field}=", value)
144
              else
145
                #customfield
146
                v = CustomValue.new :custom_field_id => field.id,
147
                                    :value => value                
148
                v.customized_type = "Issue"
149
                my_custom_fields.push(v)
150
              end
151
            end
152
            if object.valid?
153
              print "."
154
              object.save
155
                #we have to create custom fields after creating issue because we need issue's ID
156
                my_custom_fields.each do |mcf|
157
                        mcf.customized_id = object.id
158
                        #we have to replace existing default (zero) values with ones from CSV
159
                        cv = CustomValue.where(:customized_type => mcf.customized_type, :custom_field_id => mcf.custom_field_id, :customized_id => mcf.customized_id).first
160
                        if cv.present?
161
                                cv.value = mcf.value
162
                                cv.save
163
                        else
164
                                mcf.save
165
                        end
166
                end
167
            else
168
              print "E"
169
              errors << "Cannot save following line in #{type}: #{attributes.join(",")}\n  => errors: #{object.errors.messages.inspect}\n  => object: #{object.inspect}"
170
            end
171
          end
172
          puts
173
        end
174
        puts
175
        if errors.any?
176
          puts "Errors:"
177
          errors.each{|e| puts e}
178
        end
179
      end
180
    end
181
      
182
    # Extract options
183
    options = {}
184
    %w(users projects issues).each do |type|
185
      options[type] = ENV[type].chomp if ENV[type]
186
    end
187

    
188
    # Exit if no valid params
189
    CSVImport.usage if options.blank?
190

    
191
    importer = CSVImport.new(options)
192

    
193
    # Validate input
194
    importer.validate
195

    
196
    # Go!
197
    old_notified_events = Setting.notified_events
198
    begin
199
      # Turn off email notifications temporarily
200
      Setting.notified_events = []
201
      # Run the migration
202
      importer.run
203
    ensure
204
      # Restore previous notification settings even if the migration fails
205
      Setting.notified_events = old_notified_events
206
    end
207
  end
208
end