datashift 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/README.markdown +12 -0
  2. data/Rakefile +4 -2
  3. data/VERSION +1 -1
  4. data/datashift.gemspec +13 -12
  5. data/lib/datashift/delimiters.rb +87 -0
  6. data/lib/datashift/method_detail.rb +5 -0
  7. data/lib/datashift/method_details_manager.rb +5 -1
  8. data/lib/datashift/method_dictionary.rb +3 -1
  9. data/lib/exporters/csv_exporter.rb +158 -10
  10. data/lib/exporters/excel_exporter.rb +6 -2
  11. data/lib/exporters/exporter_base.rb +6 -0
  12. data/lib/helpers/spree_helper.rb +2 -1
  13. data/lib/loaders/paperclip/image_loader.rb +32 -2
  14. data/lib/loaders/spree/product_loader.rb +3 -2
  15. data/lib/thor/export.thor +111 -0
  16. data/lib/thor/{import_excel.thor → import.thor} +40 -2
  17. data/lib/thor/spree/bootstrap_cleanup.thor +7 -4
  18. data/lib/thor/spree/products_images.thor +26 -17
  19. data/lib/thor/spree/reports.thor +30 -40
  20. data/lib/thor/tools.thor +63 -0
  21. data/spec/Gemfile +3 -2
  22. data/spec/csv_exporter_spec.rb +101 -0
  23. data/spec/excel_exporter_spec.rb +1 -1
  24. data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
  25. data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
  26. data/spec/fixtures/test_model_defs.rb +5 -0
  27. data/spec/spree_loader_spec.rb +28 -131
  28. data/spec/spree_method_mapping_spec.rb +1 -1
  29. data/spec/spree_variants_loader_spec.rb +189 -0
  30. metadata +193 -167
  31. data/lib/thor/export_excel.thor +0 -74
  32. data/sandbox/app/controllers/application_controller.rb +0 -3
  33. data/sandbox/config/application.rb +0 -59
  34. data/sandbox/config/database.yml +0 -20
  35. data/sandbox/config/environment.rb +0 -5
  36. data/sandbox/config/environments/development.rb +0 -37
  37. data/tasks/import/csv.rake +0 -51
@@ -43,11 +43,23 @@ To use :
43
43
 
44
44
  To pull the tasks in, add this call to your Rakefile :
45
45
 
46
+ <<<<<<< HEAD
47
+ ```ruby
48
+ DataShift::load_tasks
49
+ ```
50
+
51
+ To keep the availability to only development mode use
52
+
53
+ ```ruby
54
+ DataShift::load_tasks if(Rails.env.development?)
55
+ ```
56
+ =======
46
57
  ```ruby DataShift::load_tasks```
47
58
 
48
59
  To keep the availability to only development mode use
49
60
 
50
61
  ```ruby DataShift::load_tasks if(Rails.env.development?)```
62
+ >>>>>>> a6d5d492fd9df38a4acb8c50423219994b4b5601
51
63
 
52
64
  To use the Thor command line applications :
53
65
 
data/Rakefile CHANGED
@@ -34,13 +34,15 @@ Jeweler::Tasks.new do |gem|
34
34
  gem.homepage = "http://github.com/autotelik/datashift"
35
35
  gem.license = "MIT"
36
36
  gem.summary = %Q{ Shift data betwen applications and Active Record}
37
- gem.description = %Q{A suite of tools to move data between ActiveRecord models,databases,applications like Excel/Open Office, files and projects including Spree}
37
+ gem.description = %Q{Comprehensive Excel and CSV import/export tools. Shift data between ActiveRecord databases, applications, and projects like Spree}
38
38
  gem.email = "rubygems@autotelik.co.uk"
39
39
  gem.authors = ["Thomas Statter"]
40
40
  # dependencies defined in Gemfile
41
41
  gem.files.exclude ['sandbox']
42
42
 
43
- gem.add_dependency ['spreadsheet']
43
+ gem.add_dependency 'spreadsheet'
44
+ gem.add_dependency 'rubyzip'
45
+
44
46
  end
45
47
  Jeweler::RubygemsDotOrgTasks.new
46
48
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.0
1
+ 0.9.0
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "datashift"
8
- s.version = "0.8.0"
8
+ s.version = "0.9.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Thomas Statter"]
12
- s.date = "2012-07-09"
13
- s.description = "A suite of tools to move data between ActiveRecord models,databases,applications like Excel/Open Office, files and projects including Spree"
12
+ s.date = "2012-09-03"
13
+ s.description = "Comprehensive Excel and CSV import/export tools. Shift data between ActiveRecord databases, applications, and projects like Spree"
14
14
  s.email = "rubygems@autotelik.co.uk"
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
28
28
  "lib/applications/jruby/jexcel_file.rb",
29
29
  "lib/applications/jruby/word.rb",
30
30
  "lib/datashift.rb",
31
+ "lib/datashift/delimiters.rb",
31
32
  "lib/datashift/exceptions.rb",
32
33
  "lib/datashift/file_definitions.rb",
33
34
  "lib/datashift/logging.rb",
@@ -69,12 +70,13 @@ Gem::Specification.new do |s|
69
70
  "lib/loaders/spreadsheet_loader.rb",
70
71
  "lib/loaders/spree/image_loader.rb",
71
72
  "lib/loaders/spree/product_loader.rb",
72
- "lib/thor/export_excel.thor",
73
+ "lib/thor/export.thor",
73
74
  "lib/thor/generate_excel.thor",
74
- "lib/thor/import_excel.thor",
75
+ "lib/thor/import.thor",
75
76
  "lib/thor/spree/bootstrap_cleanup.thor",
76
77
  "lib/thor/spree/products_images.thor",
77
78
  "lib/thor/spree/reports.thor",
79
+ "lib/thor/tools.thor",
78
80
  "public/spree/products/large/DEMO_001_ror_bag.jpeg",
79
81
  "public/spree/products/large/DEMO_002_Powerstation.jpg",
80
82
  "public/spree/products/large/DEMO_003_ror_mug.jpeg",
@@ -90,12 +92,8 @@ Gem::Specification.new do |s|
90
92
  "public/spree/products/small/DEMO_001_ror_bag.jpeg",
91
93
  "public/spree/products/small/DEMO_002_Powerstation.jpg",
92
94
  "public/spree/products/small/DEMO_003_ror_mug.jpeg",
93
- "sandbox/app/controllers/application_controller.rb",
94
- "sandbox/config/application.rb",
95
- "sandbox/config/database.yml",
96
- "sandbox/config/environment.rb",
97
- "sandbox/config/environments/development.rb",
98
95
  "spec/Gemfile",
96
+ "spec/csv_exporter_spec.rb",
99
97
  "spec/csv_loader_spec.rb",
100
98
  "spec/datashift_spec.rb",
101
99
  "spec/db/migrate/20110803201325_create_test_bed.rb",
@@ -147,13 +145,13 @@ Gem::Specification.new do |s|
147
145
  "spec/spree_images_loader_spec.rb",
148
146
  "spec/spree_loader_spec.rb",
149
147
  "spec/spree_method_mapping_spec.rb",
148
+ "spec/spree_variants_loader_spec.rb",
150
149
  "spec/thor_spec.rb",
151
150
  "tasks/config/seed_fu_product_template.erb",
152
151
  "tasks/config/tidy_config.txt",
153
152
  "tasks/db_tasks.rake",
154
153
  "tasks/export/excel_generator.rake",
155
154
  "tasks/file_tasks.rake",
156
- "tasks/import/csv.rake",
157
155
  "tasks/import/excel.rake",
158
156
  "tasks/word_to_seedfu.rake",
159
157
  "test/helper.rb",
@@ -162,7 +160,7 @@ Gem::Specification.new do |s|
162
160
  s.homepage = "http://github.com/autotelik/datashift"
163
161
  s.licenses = ["MIT"]
164
162
  s.require_paths = ["lib"]
165
- s.rubygems_version = "1.8.15"
163
+ s.rubygems_version = "1.8.24"
166
164
  s.summary = "Shift data betwen applications and Active Record"
167
165
 
168
166
  if s.respond_to? :specification_version then
@@ -170,11 +168,14 @@ Gem::Specification.new do |s|
170
168
 
171
169
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
172
170
  s.add_runtime_dependency(%q<spreadsheet>, [">= 0"])
171
+ s.add_runtime_dependency(%q<rubyzip>, [">= 0"])
173
172
  else
174
173
  s.add_dependency(%q<spreadsheet>, [">= 0"])
174
+ s.add_dependency(%q<rubyzip>, [">= 0"])
175
175
  end
176
176
  else
177
177
  s.add_dependency(%q<spreadsheet>, [">= 0"])
178
+ s.add_dependency(%q<rubyzip>, [">= 0"])
178
179
  end
179
180
  end
180
181
 
@@ -0,0 +1,87 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT
5
+ #
6
+ # Details:: Module providing standard location for delimiters used in both export/import
7
+ #
8
+ # For example we support multiple entries in a single column, so the string
9
+ # needs to be formatted with recognisable delimiters seperating each of the multiple values.
10
+ #
11
+ module DataShift
12
+
13
+
14
+ module Delimiters
15
+
16
+
17
+ # Support multiple associations being added to a base object to be specified in a single column.
18
+ #
19
+ # Entry represents the association to find via supplied name, value to use in the lookup.
20
+ # Can contain multiple lookup name/value pairs, separated by multi_assoc_delim ( | )
21
+ #
22
+ # Default syntax :
23
+ #
24
+ # Name1:value1, value2|Name2:value1, value2, value3|Name3:value1, value2
25
+ #
26
+ # E.G.
27
+ # Association Properties, has a column named Size, and another called Colour,
28
+ # and this combination could be used to lookup multiple associations to add to the main model Jumper
29
+ #
30
+ # Size:small # => generates find_by_size( 'small' )
31
+ # Size:large # => generates find_by_size( 'large' )
32
+ # Colour:red,green,blue # => generates find_all_by_colour( ['red','green','blue'] )
33
+ #
34
+ # Size:large|Size:medium|Size:large
35
+ # => Find 3 different associations, perform lookup via column called Size
36
+ # => Jumper.properties << [ small, medium, large ]
37
+ #
38
+ def self.name_value_delim
39
+ @name_value_delim ||= ':'
40
+ @name_value_delim
41
+ end
42
+
43
+ def self.set_name_value_delim(x) @name_value_delim = x; end
44
+ # TODO - support embedded object creation/update via hash (which hopefully we should be able to just forward to AR)
45
+ #
46
+ # |Category|
47
+ # name:new{ :date => '20110102', :owner = > 'blah'}
48
+ #
49
+
50
+
51
+ def self.multi_value_delim
52
+ @multi_value_delim ||= ','
53
+ @multi_value_delim
54
+ end
55
+
56
+ def self.set_multi_value_delim(x) @multi_value_delim = x; end
57
+
58
+ # Multiple objects can be embedded in single columns.
59
+ # In this example a single Category column contains 3 separate entries, New, SecondHand, Retro
60
+ # object creation/update via hash (which hopefully we should be able to just forward to AR)
61
+ #
62
+ # | Category |
63
+ # 'name =>New, :a => 1, :b => 2|name => SecondHand, :a => 6, :b => 34|Name:Old, :a => 12, :b => 67', 'Next Column'
64
+ #
65
+ def self.multi_assoc_delim
66
+ @multi_assoc_delim ||= '|'
67
+ @multi_assoc_delim
68
+ end
69
+
70
+
71
+ def self.set_multi_assoc_delim(x) @multi_assoc_delim = x; end
72
+
73
+
74
+ def self.csv_delim
75
+ @csv_delim ||= ','
76
+ @csv_delim
77
+ end
78
+
79
+ def self.set_csv_delim(x) @csv_delim = x; end
80
+
81
+ def self.eol
82
+ "\n"
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -25,6 +25,11 @@ module DataShift
25
25
  @type_enum
26
26
  end
27
27
 
28
+ def self.association_types_enum
29
+ @assoc_type_enum ||= Set[:belongs_to, :has_one, :has_many]
30
+ @assoc_type_enum
31
+ end
32
+
28
33
  # When looking up an association, try each of these in turn till a match
29
34
  # i.e find_by_name .. find_by_title and so on, lastly try the raw id
30
35
  @@insistent_find_by_list ||= [:name, :title, :id]
@@ -47,9 +47,13 @@ module DataShift
47
47
  end
48
48
 
49
49
  def get_list( type )
50
- @method_details_list[type.to_sym]
50
+ @method_details_list[type.to_sym] || []
51
51
  end
52
52
 
53
+ def get_operators( op_type )
54
+ get_list(op_type).collect { |md| md.operator }
55
+ end
56
+
53
57
  def all_available_operators
54
58
  method_details_list.values.flatten.collect(&:operator)
55
59
  end
@@ -59,7 +59,9 @@ module DataShift
59
59
  # the raw form i.e without the '=' for consistency
60
60
  if( options[:reload] || assignments[klass].nil? )
61
61
 
62
- assignments[klass] = klass.column_names
62
+ # TODO investigate difference with attribute_names - maybe column names can be assigned to an attribute
63
+ # so in terms of method calls on klass attribute_names might be safer
64
+ assignments[klass] = klass.column_names
63
65
 
64
66
  if(options[:instance_methods] == true)
65
67
  setters = klass.instance_methods.grep(/\w+=/).collect {|x| x.to_s }
@@ -7,30 +7,178 @@
7
7
  #
8
8
  #
9
9
  require 'exporter_base'
10
+ require 'csv'
10
11
 
11
12
  module DataShift
12
13
 
13
14
  class CsvExporter < ExporterBase
14
15
 
15
- attr_accessor :excel, :filename
16
-
16
+ attr_accessor :text_delim
17
+
17
18
  def initialize(filename)
18
- @excel = nil
19
- @filename = filename
19
+ super(filename)
20
+ @text_delim = "\'"
20
21
  end
21
22
 
22
- # Create CSV file representing supplied Model
23
23
 
24
- def generate(model, options = {})
25
-
26
- @filename = options[:filename] if options[:filename]
24
+ # Return opposite of text delim - "hello, 'barry'" => '"hello, "barry""'
25
+ def escape_text_delim
26
+ return '"' if @text_delim == "\'"
27
+ "\'"
28
+ end
29
+
30
+ # Create CSV file from set of ActiveRecord objects
31
+ # Options :
32
+ # => :filename
33
+ # => :text_delim => Char to use to delim columns, useful when data contain embedded ','
34
+ # => ::methods => List of methods to additionally call on each record
35
+ #
36
+ def export(records, options = {})
37
+
38
+ raise ArgumentError.new('Please supply array of records to export') unless records.is_a? Array
39
+
40
+ first = records[0]
41
+
42
+ return unless(first.is_a?(ActiveRecord::Base))
43
+
44
+ f = options[:filename] || filename()
45
+
46
+ @text_delim = options[:text_delim] if(options[:text_delim])
47
+
48
+ File.open(f, "w") do |csv|
49
+
50
+ headers = first.serializable_hash.keys.collect
51
+
52
+ [*options[:methods]].each do |c| headers << c if(first.respond_to?(c)) end if(options[:methods])
53
+
54
+ csv << headers.join(Delimiters::csv_delim) << Delimiters::eol
55
+
56
+ records.each do |r|
57
+ next unless(r.is_a?(ActiveRecord::Base))
58
+
59
+ csv << record_to_csv(r, options) << Delimiters::eol
60
+ end
61
+ end
27
62
  end
63
+
64
+ # Create an Excel file from list of ActiveRecord objects
65
+ # Specify which associations to export via :with or :exclude
66
+ # Possible values are : [:assignment, :belongs_to, :has_one, :has_many]
67
+ #
68
+ def export_with_associations(klass, records, options = {})
69
+
70
+ f = options[:filename] || filename()
71
+
72
+ @text_delim = options[:text_delim] if(options[:text_delim])
73
+
74
+ MethodDictionary.find_operators( klass )
75
+
76
+ # builds all possible operators
77
+ MethodDictionary.build_method_details( klass )
78
+
79
+ work_list = options[:with] ? Set(options[:with]) : MethodDetail::supported_types_enum
80
+
81
+ assoc_work_list = work_list.dup
28
82
 
83
+ details_mgr = MethodDictionary.method_details_mgrs[klass]
84
+
85
+ headers, assoc_operators, assignments = [], [], []
86
+
87
+ # headers
88
+ if(work_list.include?(:assignment))
89
+ assignments << details_mgr.get_list(:assignment).collect( &:operator)
90
+ assoc_work_list.delete :assignment
91
+ end
92
+
93
+ headers << assignments.flatten!
94
+ # based on users requested list ... belongs_to has_one, has_many etc ... select only those operators
95
+ assoc_operators = assoc_work_list.collect do |op_type|
96
+ details_mgr.get_list(op_type).collect(&:operator).flatten
97
+ end
98
+
99
+ assoc_operators.flatten!
100
+
101
+ File.open(f, "w") do |csv|
102
+
103
+ csv << headers.join(Delimiters::csv_delim)
104
+
105
+ csv << Delimiters::csv_delim << assoc_operators.join(Delimiters::csv_delim) unless(assoc_operators.empty?)
106
+
107
+ csv << Delimiters::eol
108
+
109
+ records.each do |r|
110
+ next unless(r.is_a?(ActiveRecord::Base))
111
+
112
+ csv_columns = []
113
+ # need to make sure order matches headers
114
+ # could look at collection headers via serializable hash.keys and then use
115
+ # csv << record_to_csv(r) ??
116
+ assignments.each {|x| csv << escape(r.send(x)) << Delimiters::csv_delim }
117
+
118
+ # done records basic attributes now deal with associations
119
+
120
+ #assoc_work_list.each do |op_type|
121
+ # details_mgr.get_operators(op_type).each do |operator|
122
+ assoc_operators.each do |operator|
123
+ assoc_object = r.send(operator)
124
+
125
+ if(assoc_object.is_a?ActiveRecord::Base)
126
+ column_text = record_to_column(assoc_object) # belongs_to or has_one
127
+
128
+ csv << "#{@text_delim}#{column_text}#{@text_delim}" << Delimiters::csv_delim
129
+ #csv << record_to_csv(r)
130
+
131
+ elsif(assoc_object.is_a? Array)
132
+ items_to_s = assoc_object.collect {|x| record_to_column(x) }
133
+
134
+ # create a single column
135
+ csv << "#{@text_delim}#{items_to_s.join(Delimiters::multi_assoc_delim)}#{@text_delim}" << Delimiters::csv_delim
136
+
137
+ else
138
+ csv << Delimiters::csv_delim
139
+ end
140
+ #end
141
+ end
142
+
143
+ csv << Delimiters::eol # next record
144
+
145
+ end
146
+ end
147
+ end
148
+
149
+
150
+ # Convert an AR instance to a single CSV column
29
151
 
30
- # Create an Csv file representing supplied Model
152
+ def record_to_column(record)
153
+
154
+ csv_data = []
155
+ record.serializable_hash.each do |name, value|
156
+ value = 'nil' if value.nil?
157
+ text = value.to_s.gsub(@text_delim, escape_text_delim())
158
+ csv_data << "#{name.to_sym} => #{text}"
159
+ end
160
+ "#{csv_data.join(Delimiters::csv_delim)}"
161
+ end
162
+
163
+
164
+ # Convert an AR instance to a set of CSV columns
165
+ def record_to_csv(record, options = {})
166
+ csv_data = record.serializable_hash.values.collect { |value| escape(value) }
31
167
 
32
- def export(items, options = {})
168
+ [*options[:methods]].each { |x| csv_data << escape(record.send(x)) if(record.respond_to?(x)) } if(options[:methods])
169
+
170
+ csv_data.join( Delimiters::csv_delim )
33
171
  end
172
+
173
+
174
+ private
34
175
 
176
+ def escape(value)
177
+ text = value.to_s.gsub(@text_delim, escape_text_delim())
178
+
179
+ text = "#{@text_delim}#{text}#{@text_delim}" if(text.include?(Delimiters::csv_delim))
180
+ text
181
+ end
182
+
35
183
  end
36
184
  end