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.
- data/README.markdown +12 -0
- data/Rakefile +4 -2
- data/VERSION +1 -1
- data/datashift.gemspec +13 -12
- data/lib/datashift/delimiters.rb +87 -0
- data/lib/datashift/method_detail.rb +5 -0
- data/lib/datashift/method_details_manager.rb +5 -1
- data/lib/datashift/method_dictionary.rb +3 -1
- data/lib/exporters/csv_exporter.rb +158 -10
- data/lib/exporters/excel_exporter.rb +6 -2
- data/lib/exporters/exporter_base.rb +6 -0
- data/lib/helpers/spree_helper.rb +2 -1
- data/lib/loaders/paperclip/image_loader.rb +32 -2
- data/lib/loaders/spree/product_loader.rb +3 -2
- data/lib/thor/export.thor +111 -0
- data/lib/thor/{import_excel.thor → import.thor} +40 -2
- data/lib/thor/spree/bootstrap_cleanup.thor +7 -4
- data/lib/thor/spree/products_images.thor +26 -17
- data/lib/thor/spree/reports.thor +30 -40
- data/lib/thor/tools.thor +63 -0
- data/spec/Gemfile +3 -2
- data/spec/csv_exporter_spec.rb +101 -0
- data/spec/excel_exporter_spec.rb +1 -1
- data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
- data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
- data/spec/fixtures/test_model_defs.rb +5 -0
- data/spec/spree_loader_spec.rb +28 -131
- data/spec/spree_method_mapping_spec.rb +1 -1
- data/spec/spree_variants_loader_spec.rb +189 -0
- metadata +193 -167
- data/lib/thor/export_excel.thor +0 -74
- data/sandbox/app/controllers/application_controller.rb +0 -3
- data/sandbox/config/application.rb +0 -59
- data/sandbox/config/database.yml +0 -20
- data/sandbox/config/environment.rb +0 -5
- data/sandbox/config/environments/development.rb +0 -37
- data/tasks/import/csv.rake +0 -51
data/README.markdown
CHANGED
@@ -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{
|
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
|
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.
|
1
|
+
0.9.0
|
data/datashift.gemspec
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "datashift"
|
8
|
-
s.version = "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-
|
13
|
-
s.description = "
|
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/
|
73
|
+
"lib/thor/export.thor",
|
73
74
|
"lib/thor/generate_excel.thor",
|
74
|
-
"lib/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.
|
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
|
-
|
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 :
|
16
|
-
|
16
|
+
attr_accessor :text_delim
|
17
|
+
|
17
18
|
def initialize(filename)
|
18
|
-
|
19
|
-
@
|
19
|
+
super(filename)
|
20
|
+
@text_delim = "\'"
|
20
21
|
end
|
21
22
|
|
22
|
-
# Create CSV file representing supplied Model
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
@
|
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
|
-
|
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
|
-
|
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
|