datashift 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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