import_products 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +23 -0
- data/README.md +83 -0
- data/app/controllers/spree/admin/product_imports_controller.rb +35 -0
- data/app/jobs/import_products_job.rb +22 -0
- data/app/models/spree/product_import.rb +472 -0
- data/app/overrides/add_import_to_admin_sidebar_menu.rb +6 -0
- data/app/views/spree/admin/product_imports/index.html.erb +55 -0
- data/app/views/spree/admin/product_imports/show.html.erb +37 -0
- data/app/views/spree/admin/shared/_import_sidebar_menu.erb +5 -0
- data/app/views/spree/user_mailer/product_import_results.text.erb +21 -0
- data/lib/generators/import_products/install/install_generator.rb +30 -0
- data/lib/generators/import_products/install/templates/config/initializers/import_product_settings.rb +36 -0
- data/lib/import_products.rb +3 -0
- data/lib/import_products/engine.rb +22 -0
- data/lib/import_products/user_mailer_ext.rb +14 -0
- data/lib/tasks/import_products.rake +1 -0
- metadata +200 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2971bbecf8f07183d6d498c364e5b5adee68beb9
|
4
|
+
data.tar.gz: 6736b0073c7bfcfeead6b746ad905d6f393f12e8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3ac5dcbffc1b9ad1d1d1dbfb1f4cedf9e0be9e75e5d65b12a12e855068c9bf5fc70e78594876ddcd9947b9b97543eaea0c7b597e93b47820daed9eefc53f5d7b
|
7
|
+
data.tar.gz: bfc16a001c3232d8fbd2fc710551cc1443dc97f3964cd5cb8c82e4526625c1207542d26017c93968991d8fc70c95a06c58fe640ef0e99b6ec8fbdfc58ada97d8
|
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Redistribution and use in source and binary forms, with or without modification,
|
2
|
+
are permitted provided that the following conditions are met:
|
3
|
+
|
4
|
+
* Redistributions of source code must retain the above copyright notice,
|
5
|
+
this list of conditions and the following disclaimer.
|
6
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
7
|
+
this list of conditions and the following disclaimer in the documentation
|
8
|
+
and/or other materials provided with the distribution.
|
9
|
+
* Neither the name of the Rails Dog LLC nor the names of its
|
10
|
+
contributors may be used to endorse or promote products derived from this
|
11
|
+
software without specific prior written permission.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
14
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
15
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
16
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
17
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
18
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
19
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
20
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
21
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
22
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
23
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
Spree Import Products
|
2
|
+
==============
|
3
|
+
|
4
|
+
This extension adds product import functionality to Spree, with a bunch of features that give it similar functionality to Shopify's importer.
|
5
|
+
|
6
|
+
It's been built to be as simple as possible while still doing it's job, and almost the entire workflow of the script beyond creating products from a CSV file is configurable.
|
7
|
+
|
8
|
+
This extension adds a tab to the administration area of Spree, allowing a logged-in user to select and upload a CSV file containing product information. The upload is then placed on queue for processing. Once it has been processed, the user who initiated the job is notified by email that their import has completed.
|
9
|
+
|
10
|
+
|
11
|
+
FEATURES
|
12
|
+
==============
|
13
|
+
|
14
|
+
* A reasonably opinionated product import model should take the heavy lifting out of batch-importing products.
|
15
|
+
* Using things like `class_eval`, it can be extended or changed to do more, less, or something differently.
|
16
|
+
* Using delayed_job, the import is no longer processed when the user uploads the CSV file (**Note**: this requires the proper installation and configuration of delayed job).
|
17
|
+
* Columns are mapped dynamically by default. This means that if you have a SKU column in your CSV file, it will be automatically set as the `sku` attribute of the Product model.
|
18
|
+
* Multiple taxonomies are supported (By default, the importer looks for Brand and Taxonomy). Multiple taxonomy nesting is also supported. (See the 'Taxonomies' area below)
|
19
|
+
* Multiple images are supported (By default, the importer looks for Image Main, Image 2, Image 3 and Image 4). Images can be loaded either from disk, or from a publicly-accessible URL.
|
20
|
+
* Automatically creating variants is supported out-of-the-box - the importer compares the imported data with already-created products (By default, on the Product's `permalink` attribute), and creates a variant of that product if it exists already. Custom fields are also supported if dynamic column mapping is enabled - simply having 'Color', 'Size', 'Material' columns in your CSV file for example, will automatically set the relevant custom fields on the variant to the values from the CSV.
|
21
|
+
* It now uses the Ruby 1.9.2 standard CSV library (a.k.a FasterCSV).
|
22
|
+
|
23
|
+
|
24
|
+
DELAYED JOB
|
25
|
+
==============
|
26
|
+
This gem will require (or will install for you), [delayed_job](https://www.github.com/tobi/delayed_job).
|
27
|
+
Once the gem has installed and you have run migrations, you should also run `rails generator delayed_job` to create the tables that delayed_job requires.
|
28
|
+
|
29
|
+
Delayed Job also requires that you run 'workers' in the background to pop jobs off the queue and process them.
|
30
|
+
This setup may seem like extra work, but believe me, it pays off - with this method, users get an immediate confirmation that their import is on it's way, with a confirmation later on with full details - this is much better than the previous method where the actual processing was completed during the request, with no feedback reaching the user until after the import had finished.
|
31
|
+
|
32
|
+
Run `rake jobs:work` to start Delayed Job, and `rake jobs:clear` to clear all queued jobs. Also see delayed_job's Github page for info on Capistrano support.
|
33
|
+
|
34
|
+
For more information on Delayed Job, and for help getting a worker running, see the [Github Project Page](https://www.github.com/collectiveidea/delayed_job)
|
35
|
+
|
36
|
+
TAXONOMIES
|
37
|
+
==========
|
38
|
+
|
39
|
+
The columns of the CSV that contain taxonomies is configurable. Each of these columns can contain a number of formats that represent different hierarchies of taxonomies.
|
40
|
+
|
41
|
+
Examples
|
42
|
+
--------
|
43
|
+
* Basic taxonomy association for Category: `Furniture`
|
44
|
+
* Multiple taxonomy association for Category: `Furniture & Clearance`
|
45
|
+
* Hierarchy taxonomy association for Category: `Furniture > Dining Room > Tables`
|
46
|
+
* Multiple hierarchy taxonomy association for Category: `Furniture > Dining Room > Tables & Clearance > Hot this week`
|
47
|
+
|
48
|
+
CONFIGURATION
|
49
|
+
=============
|
50
|
+
|
51
|
+
All the configuration for this extension is inside the initializer generated when you run `rake import_products:install`. It's basically a big hash with manual column mappings (If you don't want to use dynamic column mapping), and a bunch of settings that control the workflow of the extension. Take a look at the initializer to see more details on each field.
|
52
|
+
|
53
|
+
In most cases, it's unlikely you will need to change defaults, but it's there is you need it.
|
54
|
+
|
55
|
+
|
56
|
+
TESTING
|
57
|
+
=======
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
rake test_app
|
61
|
+
cd spec/dummy
|
62
|
+
rake db:create
|
63
|
+
rake db:migrate db:test:prepare
|
64
|
+
rails generate delayed_job:active_record
|
65
|
+
rails generate import_products:install
|
66
|
+
```
|
67
|
+
|
68
|
+
INSTALLATION
|
69
|
+
==============
|
70
|
+
1. Add the gem to your Gemfile, and run bundle install.
|
71
|
+
`gem 'import_products', :git => 'git://github.com/joshmcarthur/spree-import-products.git'` then `bundle install`
|
72
|
+
|
73
|
+
2. rails generate import_products:install
|
74
|
+
|
75
|
+
3. Configure the extension to suit your application by changing config variables in `config/initializers/import_product_settings.rb`
|
76
|
+
|
77
|
+
4. Run application!
|
78
|
+
|
79
|
+
ATTRIBUTION
|
80
|
+
==============
|
81
|
+
The product import script was based on a simple import script written by Brian Quinn [here](https://gist.github.com/31710). I've extended it quite a bit and tweaked it to fit my needs.
|
82
|
+
|
83
|
+
Copyright (c) 2010 Josh McArthur, released under the MIT License
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Spree
|
2
|
+
module Admin
|
3
|
+
class ProductImportsController < BaseController
|
4
|
+
|
5
|
+
def index
|
6
|
+
@product_import = Spree::ProductImport.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@product_import = Spree::ProductImport.find(params[:id])
|
11
|
+
@products = @product_import.products
|
12
|
+
end
|
13
|
+
|
14
|
+
def create
|
15
|
+
@product_import = Spree::ProductImport.create(product_import_params)
|
16
|
+
ImportProductsJob.perform_later(@product_import)
|
17
|
+
flash[:notice] = t('product_import_processing')
|
18
|
+
redirect_to admin_product_imports_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
@product_import = Spree::ProductImport.find(params[:id])
|
23
|
+
if @product_import.destroy
|
24
|
+
flash[:notice] = t('delete_product_import_successful')
|
25
|
+
end
|
26
|
+
redirect_to admin_product_imports_path
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def product_import_params
|
31
|
+
params.require(:product_import).permit!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ImportProductsJob < ActiveJob::Base
|
2
|
+
queue_as :default
|
3
|
+
|
4
|
+
after_perform :notify_admin
|
5
|
+
|
6
|
+
rescue_from(StandardError) do |exception|
|
7
|
+
Spree::UserMailer.product_import_results(Spree::User.admin.first, exception.message).deliver_later
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform(products)
|
11
|
+
products.import_data!(Spree::ProductImport.settings[:transaction])
|
12
|
+
end
|
13
|
+
|
14
|
+
def notify_admin
|
15
|
+
# Spree::UserMailer.product_import_results(Spree::User.admin.first).deliver_later
|
16
|
+
puts "*********************************************************"
|
17
|
+
puts "*********************************************************"
|
18
|
+
puts "==================== Import Complete ===================="
|
19
|
+
puts "*********************************************************"
|
20
|
+
puts "*********************************************************"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,472 @@
|
|
1
|
+
# This model is the master routine for uploading products
|
2
|
+
# Requires Paperclip and CSV to upload the CSV file and read it nicely.
|
3
|
+
|
4
|
+
# Original Author:: Josh McArthur
|
5
|
+
# License:: MIT
|
6
|
+
module Spree
|
7
|
+
class ProductError < StandardError; end;
|
8
|
+
class ImportError < StandardError; end;
|
9
|
+
class SkuError < StandardError; end;
|
10
|
+
|
11
|
+
class ProductImport < ActiveRecord::Base
|
12
|
+
#attr_accessible :data_file, :data_file_file_name, :data_file_content_type, :data_file_file_size, :data_file_updated_at, :product_ids, :state, :failed_at, :completed_at
|
13
|
+
has_attached_file :data_file, :path => ":rails_root/lib/etc/product_data/data-files/:basename.:extension"
|
14
|
+
validates_attachment_presence :data_file
|
15
|
+
validates_attachment :data_file, :presence => true, content_type: { content_type: "text/csv" }
|
16
|
+
|
17
|
+
# after_destroy :destroy_products
|
18
|
+
|
19
|
+
serialize :product_ids, Array
|
20
|
+
cattr_accessor :settings
|
21
|
+
|
22
|
+
def products
|
23
|
+
Product.where :id => product_ids
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'csv'
|
27
|
+
require 'pp'
|
28
|
+
require 'open-uri'
|
29
|
+
|
30
|
+
# def destroy_products
|
31
|
+
# products.destroy_all
|
32
|
+
# end
|
33
|
+
|
34
|
+
state_machine :initial => :created do
|
35
|
+
|
36
|
+
event :start do
|
37
|
+
transition :to => :started, :from => :created
|
38
|
+
end
|
39
|
+
event :complete do
|
40
|
+
transition :to => :completed, :from => :started
|
41
|
+
end
|
42
|
+
event :failure do
|
43
|
+
transition :to => :failed, :from => :started
|
44
|
+
end
|
45
|
+
|
46
|
+
before_transition :to => [:failed] do |import|
|
47
|
+
import.product_ids = []
|
48
|
+
import.failed_at = Time.now
|
49
|
+
import.completed_at = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
before_transition :to => [:completed] do |import|
|
53
|
+
import.failed_at = nil
|
54
|
+
import.completed_at = Time.now
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def state_datetime
|
59
|
+
if failed?
|
60
|
+
failed_at
|
61
|
+
elsif completed?
|
62
|
+
completed_at
|
63
|
+
else
|
64
|
+
Time.now
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
## Data Importing:
|
69
|
+
# List Price maps to Master Price, Current MAP to Cost Price, Net 30 Cost unused
|
70
|
+
# Width, height, Depth all map directly to object
|
71
|
+
# Image main is created independtly, then each other image also created and associated with the product
|
72
|
+
# Meta keywords and description are created on the product model
|
73
|
+
|
74
|
+
def import_data!(_transaction=true)
|
75
|
+
start
|
76
|
+
if _transaction
|
77
|
+
transaction do
|
78
|
+
_import_data
|
79
|
+
end
|
80
|
+
else
|
81
|
+
_import_data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def _import_data
|
86
|
+
begin
|
87
|
+
@products_before_import = Spree::Product.all
|
88
|
+
@skus_of_products_before_import = @products_before_import.map(&:sku)
|
89
|
+
|
90
|
+
#rows = CSV.read(self.data_file.path, :encoding => 'windows-1251:utf-8')
|
91
|
+
rows = CSV.read(self.data_file.path, "r:ISO-8859-1")
|
92
|
+
|
93
|
+
if ProductImport.settings[:first_row_is_headings]
|
94
|
+
col = get_column_mappings(rows[0])
|
95
|
+
else
|
96
|
+
col = ProductImport.settings[:column_mappings]
|
97
|
+
end
|
98
|
+
|
99
|
+
rows[ProductImport.settings[:rows_to_skip]..-1].each do |row|
|
100
|
+
product_information = {}
|
101
|
+
|
102
|
+
#Automatically map 'mapped' fields to a collection of product information.
|
103
|
+
#NOTE: This code will deal better with the auto-mapping function - i.e. if there
|
104
|
+
#are named columns in the spreadsheet that correspond to product
|
105
|
+
# and variant field names.
|
106
|
+
|
107
|
+
col.each do |key, value|
|
108
|
+
#Trim whitespace off the beginning and end of row fields
|
109
|
+
row[value].try :strip!
|
110
|
+
product_information[key] = row[value]
|
111
|
+
end
|
112
|
+
|
113
|
+
#Manually set available_on if it is not already set
|
114
|
+
product_information[:available_on] = Date.today - 1.day if product_information[:available_on].nil?
|
115
|
+
#product_information[:price] = 0
|
116
|
+
|
117
|
+
sc = Spree::ShippingCategory.first
|
118
|
+
product_information[:shipping_category_id] = sc.id unless sc.nil?
|
119
|
+
|
120
|
+
log("#{pp product_information}")
|
121
|
+
|
122
|
+
variant_comparator_field = ProductImport.settings[:variant_comparator_field].try :to_sym
|
123
|
+
variant_comparator_column = col[variant_comparator_field]
|
124
|
+
|
125
|
+
if ProductImport.settings[:create_variants] and variant_comparator_column and
|
126
|
+
p = Product.where(variant_comparator_field => row[variant_comparator_column]).first
|
127
|
+
|
128
|
+
p.update_attribute(:deleted_at, nil) if p.deleted_at #Un-delete product if it is there
|
129
|
+
p.variants.each { |variant| variant.update_attribute(:deleted_at, nil) }
|
130
|
+
create_variant_for(p, :with => product_information)
|
131
|
+
else
|
132
|
+
next if @skus_of_products_before_import.include?(product_information[:sku])
|
133
|
+
next unless create_product_using(product_information)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if ProductImport.settings[:destroy_original_products]
|
138
|
+
@products_before_import.each { |p| p.destroy }
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
# Finished Importing!
|
144
|
+
complete
|
145
|
+
return [:notice, "Product data was successfully imported."]
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
|
151
|
+
# create_variant_for
|
152
|
+
# This method assumes that some form of checking has already been done to
|
153
|
+
# make sure that we do actually want to create a variant.
|
154
|
+
# It performs a similar task to a product, but it also must pick up on
|
155
|
+
# size/color options
|
156
|
+
def create_variant_for(product, options = {:with => {}})
|
157
|
+
return if options[:with].nil?
|
158
|
+
|
159
|
+
# Just update variant if exists
|
160
|
+
variant = Variant.find_by_sku(options[:with][:sku])
|
161
|
+
raise SkuError, "SKU #{variant.sku} should belongs to #{product.inspect} but was #{variant.product.inspect}" if variant && variant.product != product
|
162
|
+
if !variant
|
163
|
+
variant = product.variants.new
|
164
|
+
variant.id = options[:with][:id]
|
165
|
+
else
|
166
|
+
options[:with].delete(:id)
|
167
|
+
end
|
168
|
+
|
169
|
+
field = ProductImport.settings[:variant_comparator_field]
|
170
|
+
log "VARIANT:: #{variant.inspect} /// #{options.inspect } /// #{options[:with][field]} /// #{field}"
|
171
|
+
|
172
|
+
#Remap the options - oddly enough, Spree's product model has master_price and cost_price, while
|
173
|
+
#variant has price and cost_price.
|
174
|
+
|
175
|
+
options[:with][:price] = options[:with].delete(:price)
|
176
|
+
|
177
|
+
#First, set the primitive fields on the object (prices, etc.)
|
178
|
+
|
179
|
+
options[:with].each do |field, value|
|
180
|
+
variant.send("#{field}=", value) if variant.respond_to?("#{field}=")
|
181
|
+
applicable_option_type = OptionType.where("lower(presentation) = ? OR lower(name) = ?",field.to_s, field.to_s).first
|
182
|
+
if applicable_option_type.is_a?(OptionType)
|
183
|
+
product.option_types << applicable_option_type unless product.option_types.include?(applicable_option_type)
|
184
|
+
opt_value = applicable_option_type.option_values.where(["presentation = ? OR name = ?", value, value]).first
|
185
|
+
opt_value = applicable_option_type.option_values.create(:presentation => value, :name => value) unless opt_value
|
186
|
+
variant.option_values << opt_value unless variant.option_values.include?(opt_value)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
log "VARIANT PRICE #{variant.inspect} /// #{variant.price}"
|
191
|
+
|
192
|
+
if variant.valid?
|
193
|
+
variant.save
|
194
|
+
|
195
|
+
#Associate our new variant with any new taxonomies
|
196
|
+
ProductImport.settings[:taxonomy_fields].each do |field|
|
197
|
+
associate_product_with_taxon(variant.product, field.to_s, options[:with][field.to_sym])
|
198
|
+
end
|
199
|
+
|
200
|
+
#Associate our new variant with any stock item
|
201
|
+
source_location = Spree::StockLocation.find_by(default: true)
|
202
|
+
stock_item = variant.stock_items.where(stock_location_id: source_location.id).first
|
203
|
+
|
204
|
+
if options[:with][:on_hand].nil?
|
205
|
+
stock_item.set_count_on_hand(0)
|
206
|
+
else
|
207
|
+
stock_item.set_count_on_hand(options[:with][:on_hand])
|
208
|
+
end
|
209
|
+
|
210
|
+
#Finally, attach any images that have been specified
|
211
|
+
ProductImport.settings[:image_fields].each do |field|
|
212
|
+
find_and_attach_image_to(variant, options[:with][field.to_sym])
|
213
|
+
end
|
214
|
+
|
215
|
+
#Log a success message
|
216
|
+
log("Variant of SKU #{variant.sku} successfully imported.\n")
|
217
|
+
else
|
218
|
+
log("A variant could not be imported - here is the information we have:\n" +
|
219
|
+
"#{pp options[:with]}, #{variant.errors.full_messages.join(', ')}")
|
220
|
+
return false
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# create_product_using
|
226
|
+
# This method performs the meaty bit of the import - taking the parameters for the
|
227
|
+
# product we have gathered, and creating the product and related objects.
|
228
|
+
# It also logs throughout the method to try and give some indication of process.
|
229
|
+
def create_product_using(params_hash)
|
230
|
+
|
231
|
+
product = Product.new
|
232
|
+
properties_hash = Hash.new
|
233
|
+
|
234
|
+
# Array of special fields. Prevent adding them to properties.
|
235
|
+
special_fields = ProductImport.settings.values_at(
|
236
|
+
:image_fields,
|
237
|
+
:taxonomy_fields,
|
238
|
+
:store_field,
|
239
|
+
:variant_comparator_field
|
240
|
+
).flatten.map(&:to_s)
|
241
|
+
|
242
|
+
#The product is inclined to complain if we just dump all params
|
243
|
+
# into the product (including images and taxonomies).
|
244
|
+
# What this does is only assigns values to products if the product accepts that field.
|
245
|
+
params_hash.each do |field, value|
|
246
|
+
if product.respond_to?("#{field}=")
|
247
|
+
product.send("#{field}=", value)
|
248
|
+
elsif not special_fields.include?(field.to_s) and property = Property.where("lower(name) = ?", field).first
|
249
|
+
properties_hash[property] = value
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
after_product_built(product, params_hash)
|
254
|
+
|
255
|
+
#We can't continue without a valid product here
|
256
|
+
unless product.valid?
|
257
|
+
log(msg = "A product could not be imported - here is the information we have:\n" +
|
258
|
+
"#{pp params_hash}, #{product.errors.full_messages.join(', ')}")
|
259
|
+
raise ProductError, msg
|
260
|
+
end
|
261
|
+
|
262
|
+
#Just log which product we're processing
|
263
|
+
log(product.name)
|
264
|
+
|
265
|
+
#This should be caught by code in the main import code that checks whether to create
|
266
|
+
#variants or not. Since that check can be turned off, however, we should double check.
|
267
|
+
p = Spree::Variant.find_by_sku(product.sku)
|
268
|
+
if @skus_of_products_before_import.include? product.sku and p.deleted_at.nil?
|
269
|
+
log("#{product.name} is already in the system and active.\n")
|
270
|
+
else
|
271
|
+
if !p.nil? && !p.deleted_at.nil?
|
272
|
+
p.destroy
|
273
|
+
log("#{product.name} was removed from the system and will be replaced.\n")
|
274
|
+
end
|
275
|
+
|
276
|
+
#Save the object before creating asssociated objects
|
277
|
+
product.save and product_ids << product.id
|
278
|
+
log("Saved object before creating associated objects for: #{product.name}")
|
279
|
+
|
280
|
+
#Associate properties with product
|
281
|
+
properties_hash.each do |property, value|
|
282
|
+
product_property = Spree::ProductProperty.where(:product_id => product.id, :property_id => property.id).first_or_initialize
|
283
|
+
product_property.value = value
|
284
|
+
product_property.save!
|
285
|
+
end
|
286
|
+
|
287
|
+
#Associate our new product with any taxonomies that we need to worry about
|
288
|
+
ProductImport.settings[:taxonomy_fields].each do |field|
|
289
|
+
associate_product_with_taxon(product, field.to_s, params_hash[field.to_sym])
|
290
|
+
end
|
291
|
+
|
292
|
+
|
293
|
+
#Finally, attach any images that have been specified
|
294
|
+
ProductImport.settings[:image_fields].each do |field|
|
295
|
+
find_and_attach_image_to(product, params_hash[field.to_sym])
|
296
|
+
end
|
297
|
+
|
298
|
+
if ProductImport.settings[:multi_domain_importing] && product.respond_to?(:stores)
|
299
|
+
begin
|
300
|
+
store = Store.find(
|
301
|
+
:first,
|
302
|
+
:conditions => ["id = ? OR code = ?",
|
303
|
+
params_hash[ProductImport.settings[:store_field]],
|
304
|
+
params_hash[ProductImport.settings[:store_field]]
|
305
|
+
]
|
306
|
+
)
|
307
|
+
|
308
|
+
product.stores << store
|
309
|
+
rescue
|
310
|
+
log("#{product.name} could not be associated with a store. Ensure that Spree's multi_domain extension is installed and that fields are mapped to the CSV correctly.")
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
#Stock item
|
315
|
+
source_location = Spree::StockLocation.find_by(default: true)
|
316
|
+
stock_item = product.stock_items.where(stock_location_id: source_location.id).first
|
317
|
+
|
318
|
+
if params_hash[:on_hand].nil?
|
319
|
+
stock_item.set_count_on_hand(0)
|
320
|
+
else
|
321
|
+
stock_item.set_count_on_hand(params_hash[:on_hand])
|
322
|
+
end
|
323
|
+
|
324
|
+
log("#{product.name} successfully imported.\n")
|
325
|
+
end
|
326
|
+
return true
|
327
|
+
end
|
328
|
+
|
329
|
+
# get_column_mappings
|
330
|
+
# This method attempts to automatically map headings in the CSV files
|
331
|
+
# with fields in the product and variant models.
|
332
|
+
# If the headings of columns are going to be called something other than this,
|
333
|
+
# or if the files will not have headings, then the manual initializer
|
334
|
+
# mapping of columns must be used.
|
335
|
+
# Row is an array of headings for columns - SKU, Master Price, etc.)
|
336
|
+
# @return a hash of symbol heading => column index pairs
|
337
|
+
def get_column_mappings(row)
|
338
|
+
mappings = {}
|
339
|
+
row.each_with_index do |heading, index|
|
340
|
+
# Stop collecting headings, if heading is empty
|
341
|
+
if not heading.blank?
|
342
|
+
mappings[heading.downcase.gsub(/\A\s*/, '').chomp.gsub(/\s/, '_').to_sym] = index
|
343
|
+
else
|
344
|
+
break
|
345
|
+
end
|
346
|
+
end
|
347
|
+
mappings
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
### MISC HELPERS ####
|
352
|
+
|
353
|
+
# Log a message to a file - logs in standard Rails format to logfile set up in the import_products initializer
|
354
|
+
# and console.
|
355
|
+
# Message is string, severity symbol - either :info, :warn or :error
|
356
|
+
|
357
|
+
def log(message, severity = :info)
|
358
|
+
@rake_log ||= ActiveSupport::Logger.new(ProductImport.settings[:log_to])
|
359
|
+
message = "[#{Time.now.to_s(:db)}] [#{severity.to_s.capitalize}] #{message}\n"
|
360
|
+
@rake_log.send severity, message
|
361
|
+
puts message
|
362
|
+
end
|
363
|
+
|
364
|
+
|
365
|
+
### IMAGE HELPERS ###
|
366
|
+
|
367
|
+
# find_and_attach_image_to
|
368
|
+
# This method attaches images to products. The images may come
|
369
|
+
# from a local source (i.e. on disk), or they may be online (HTTP/HTTPS).
|
370
|
+
def find_and_attach_image_to(product_or_variant, filename)
|
371
|
+
return if filename.blank?
|
372
|
+
|
373
|
+
#The image can be fetched from an HTTP or local source - either method returns a Tempfile
|
374
|
+
file = filename =~ /\Ahttp[s]*:\/\// ? fetch_remote_image(filename) : fetch_local_image(filename)
|
375
|
+
|
376
|
+
#An image has an attachment (the image file) and some object which 'views' it
|
377
|
+
product_image = Spree::Image.new({:attachment => file,
|
378
|
+
:viewable_id => product_or_variant.id,
|
379
|
+
:viewable_type => "Spree::Variant",
|
380
|
+
:position => product_or_variant.images.length+1
|
381
|
+
})
|
382
|
+
|
383
|
+
log("#{product_image.viewable_id} : #{product_image.viewable_type} : #{product_image.position}")
|
384
|
+
|
385
|
+
product_or_variant.images << product_image if product_image.save
|
386
|
+
end
|
387
|
+
|
388
|
+
# This method is used when we have a set location on disk for
|
389
|
+
# images, and the file is accessible to the script.
|
390
|
+
# It is basically just a wrapper around basic File IO methods.
|
391
|
+
def fetch_local_image(filename)
|
392
|
+
filename = ProductImport.settings[:product_image_path] + filename
|
393
|
+
unless File.exists?(filename) && File.readable?(filename)
|
394
|
+
log("Image #{filename} was not found on the server, so this image was not imported.", :warn)
|
395
|
+
return nil
|
396
|
+
else
|
397
|
+
return File.open(filename, 'rb')
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
|
402
|
+
#This method can be used when the filename matches the format of a URL.
|
403
|
+
# It uses open-uri to fetch the file, returning a Tempfile object if it
|
404
|
+
# is successful.
|
405
|
+
# If it fails, it in the first instance logs the HTTP error (404, 500 etc)
|
406
|
+
# If it fails altogether, it logs it and exits the method.
|
407
|
+
def fetch_remote_image(filename)
|
408
|
+
begin
|
409
|
+
io = open(URI.parse(filename))
|
410
|
+
def io.original_filename; base_uri.path.split('/').last; end
|
411
|
+
return io
|
412
|
+
rescue OpenURI::HTTPError => error
|
413
|
+
log("Image #{filename} retrival returned #{error.message}, so this image was not imported")
|
414
|
+
rescue => error
|
415
|
+
log("Image #{filename} could not be downloaded, so was not imported. #{error.message}")
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
### TAXON HELPERS ###
|
420
|
+
|
421
|
+
# associate_product_with_taxon
|
422
|
+
# This method accepts three formats of taxon hierarchy strings which will
|
423
|
+
# associate the given products with taxons:
|
424
|
+
# 1. A string on it's own will will just find or create the taxon and
|
425
|
+
# add the product to it. e.g. taxonomy = "Category", taxon_hierarchy = "Tools" will
|
426
|
+
# add the product to the 'Tools' category.
|
427
|
+
# 2. A item > item > item structured string will read this like a tree - allowing
|
428
|
+
# a particular taxon to be picked out
|
429
|
+
# 3. An item > item & item > item will work as above, but will associate multiple
|
430
|
+
# taxons with that product. This form should also work with format 1.
|
431
|
+
def associate_product_with_taxon(product, taxonomy, taxon_hierarchy)
|
432
|
+
return if product.nil? || taxonomy.nil? || taxon_hierarchy.nil?
|
433
|
+
|
434
|
+
#Using find_or_create_by_name is more elegant, but our magical params code automatically downcases
|
435
|
+
# the taxonomy name, so unless we are using MySQL, this isn't going to work.
|
436
|
+
# taxonomy_name = taxonomy
|
437
|
+
# taxonomy = Taxonomy.find(:first, :conditions => ["lower(name) = ?", taxonomy])
|
438
|
+
# taxonomy = Taxonomy.create(:name => taxonomy_name.capitalize) if taxonomy.nil? && ProductImport.settings[:create_missing_taxonomies]
|
439
|
+
|
440
|
+
taxon_hierarchy.split(/\s*\|\s*/).each do |hierarchy|
|
441
|
+
hierarchy = hierarchy.split(/\s*>\s*/)
|
442
|
+
taxonomy = Spree::Taxonomy.where("lower(name) = ?", hierarchy.first.downcase).first
|
443
|
+
taxonomy = Taxonomy.create(:name => hierarchy.first.capitalize) if taxonomy.nil? && ProductImport.settings[:create_missing_taxonomies]
|
444
|
+
last_taxon = taxonomy.root
|
445
|
+
product.taxons << last_taxon unless product.taxons.include?(last_taxon)
|
446
|
+
|
447
|
+
hierarchy.shift
|
448
|
+
hierarchy.each do |taxon|
|
449
|
+
taxon = taxon.titleize
|
450
|
+
#last_taxon = last_taxon.children.find_or_create_by_name_and_taxonomy_id(taxon, taxonomy.id)
|
451
|
+
last_taxon = last_taxon.children.find_or_create_by(name: taxon, taxonomy_id: taxonomy.id)
|
452
|
+
product.taxons << last_taxon unless product.taxons.include?(last_taxon)
|
453
|
+
end
|
454
|
+
|
455
|
+
end
|
456
|
+
end
|
457
|
+
### END TAXON HELPERS ###
|
458
|
+
|
459
|
+
# May be implemented via decorator if useful:
|
460
|
+
#
|
461
|
+
# Spree::ProductImport.class_eval do
|
462
|
+
#
|
463
|
+
# private
|
464
|
+
#
|
465
|
+
# def after_product_built(product, params_hash)
|
466
|
+
# # so something with the product
|
467
|
+
# end
|
468
|
+
# end
|
469
|
+
def after_product_built(product, params_hash)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<% content_for :page_title do %>
|
2
|
+
<%= t('form.product_import.heading') %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<div class="row">
|
6
|
+
<div class="col-sm-6">
|
7
|
+
<%= form_for([:admin, @product_import], :method => :post, :html => { :multipart => true }) do |f| %>
|
8
|
+
<fieldset class="no-border-top">
|
9
|
+
<%= f.field_container :data_file, class: ['form-group'] do %>
|
10
|
+
<%= f.label :data_file, t('form.product_import.new.data_file') %>
|
11
|
+
<%= f.file_field :data_file %>
|
12
|
+
<%= f.error_message_on :data_file %>
|
13
|
+
<% end %>
|
14
|
+
<div data-hook="buttons" class="form-actions">
|
15
|
+
<%= button Spree.t('actions.create'), 'ok', 'submit', {class: 'btn-success'} %>
|
16
|
+
</div>
|
17
|
+
</fieldset>
|
18
|
+
<% end %>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
|
22
|
+
<table class="table">
|
23
|
+
<colgroup>
|
24
|
+
<col style="width: 15%">
|
25
|
+
<col style="width: 40%">
|
26
|
+
<col style="width: 10%">
|
27
|
+
<col style="width: 15%">
|
28
|
+
<col style="width: 10%">
|
29
|
+
<col style="width: 5%">
|
30
|
+
</colgroup>
|
31
|
+
<thead>
|
32
|
+
<tr>
|
33
|
+
<th>Date creation</th>
|
34
|
+
<th>CSV Name</th>
|
35
|
+
<th>Status</th>
|
36
|
+
<th>Date Status</th>
|
37
|
+
<th>Imported</th>
|
38
|
+
<th class="actions">Actions</th>
|
39
|
+
</tr>
|
40
|
+
</thead>
|
41
|
+
<tbody>
|
42
|
+
<% Spree::ProductImport.order("created_at DESC").all.each do |import| %>
|
43
|
+
<tr class="<%= cycle('odd', 'even') %>" id="<%= dom_id import %>">
|
44
|
+
<td><%= time_ago_in_words import.created_at -%> ago</td>
|
45
|
+
<td><%= link_to import.data_file_file_name, admin_product_import_path(import) -%></td>
|
46
|
+
<td><span class="label label-<%= import.state.downcase %>"><%= t(import.state, :scope => "product_import.state") -%></span></td>
|
47
|
+
<td><%= time_ago_in_words import.state_datetime -%> ago</td>
|
48
|
+
<td><%= import.product_ids.size -%></td>
|
49
|
+
<td class="actions text-center">
|
50
|
+
<%= link_to_delete import, :url => admin_product_import_path(import), :no_text => true -%>
|
51
|
+
</td>
|
52
|
+
</tr>
|
53
|
+
<% end %>
|
54
|
+
</tbody>
|
55
|
+
</table>
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<% content_for :page_title do %>
|
2
|
+
<%= t(:show_product_import) %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% content_for :page_actions do %>
|
6
|
+
<%= button_link_to t(:back_to_product_imports), admin_product_imports_path, :icon => 'icon-arrow-left' %>
|
7
|
+
<%= button_link_to t(:delete_product_import), admin_product_import_path(@product_import), {:icon => 'icon-remove', :method => :delete, :confirm => t("confirm_delete_product_import")} %>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<table class="table" id="listing_products">
|
11
|
+
<colgroup>
|
12
|
+
<col style="width: 20%">
|
13
|
+
<col style="width: 50%">
|
14
|
+
<col style="width: 20%">
|
15
|
+
<col style="width: 10%">
|
16
|
+
</colgroup>
|
17
|
+
<thead>
|
18
|
+
<tr data-hook="admin_products_index_headers">
|
19
|
+
<th><%= t(:sku) %></th>
|
20
|
+
<th><%= t(:name) %></th>
|
21
|
+
<th><%= t(:master_price) %></th>
|
22
|
+
<th class="actions" data-hook="admin_products_index_header_actions"></th>
|
23
|
+
</tr>
|
24
|
+
</thead>
|
25
|
+
<tbody>
|
26
|
+
<% @products.each do |product| %>
|
27
|
+
<tr class="<%= cycle('odd', 'even') %>" <%= "style='color: red;'" if product.deleted? %> id="<%= dom_id product %>" data-hook="admin_products_index_rows">
|
28
|
+
<td><%= product.sku rescue '' %></td>
|
29
|
+
<td><%= product.name rescue '' %></td>
|
30
|
+
<td><%= number_to_currency product.price rescue '' %></td>
|
31
|
+
<td class="actions" data-hook="admin_products_index_row_actions">
|
32
|
+
<%= link_to_edit_url edit_admin_product_path(product), :no_text => true unless product.deleted? %>
|
33
|
+
</td>
|
34
|
+
</tr>
|
35
|
+
<% end %>
|
36
|
+
</tbody>
|
37
|
+
</table>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Hello,
|
2
|
+
|
3
|
+
<% if @error_message %>
|
4
|
+
--
|
5
|
+
<%= @error_message %>
|
6
|
+
--
|
7
|
+
Something went wrong during the product import.
|
8
|
+
Common causes for this include:
|
9
|
+
- Badly formatted CSV
|
10
|
+
- Special characters within the file
|
11
|
+
- Incorrect data
|
12
|
+
- Application or database error
|
13
|
+
|
14
|
+
Please check your file and try again.
|
15
|
+
To help diagnose the problem, we have attached the log file for the imports.
|
16
|
+
<% else %>
|
17
|
+
This is a quick email to let you know that the product import you submitted has been completed.
|
18
|
+
You can now see the products you added visible in your store.
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ImportProducts
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
def self.source_paths
|
5
|
+
paths = self.superclass.source_paths
|
6
|
+
paths << File.expand_path('../templates', "../../#{__FILE__}")
|
7
|
+
paths << File.expand_path('../templates', "../#{__FILE__}")
|
8
|
+
paths << File.expand_path('../templates', __FILE__)
|
9
|
+
paths.flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_migrations
|
13
|
+
run 'bundle exec rake railties:install:migrations FROM=import_products'
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_files
|
17
|
+
template 'config/initializers/import_product_settings.rb', 'config/initializers/import_product_settings.rb'
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_migrations
|
21
|
+
res = ask "Would you like to run the migrations now? [Y/n]"
|
22
|
+
if res == "" || res.downcase == "y"
|
23
|
+
run 'bundle exec rake db:migrate'
|
24
|
+
else
|
25
|
+
puts "Skiping rake db:migrate, don't forget to run it!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/generators/import_products/install/templates/config/initializers/import_product_settings.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# This file is the thing you have to config to match your application
|
2
|
+
|
3
|
+
# This file is the thing you have to config to match your application
|
4
|
+
|
5
|
+
Spree::ProductImport.settings = {
|
6
|
+
:column_mappings => { #Change these for manual mapping of product fields to the CSV file
|
7
|
+
:sku => 0,
|
8
|
+
:name => 1,
|
9
|
+
:master_price => 2,
|
10
|
+
:cost_price => 3,
|
11
|
+
:weight => 4,
|
12
|
+
:height => 5,
|
13
|
+
:width => 6,
|
14
|
+
:depth => 7,
|
15
|
+
:image_main => 8,
|
16
|
+
:image_2 => 9,
|
17
|
+
:image_3 => 10,
|
18
|
+
:image_4 => 11,
|
19
|
+
:description => 12,
|
20
|
+
:category => 13
|
21
|
+
},
|
22
|
+
:create_missing_taxonomies => true,
|
23
|
+
:taxonomy_fields => [:category, :brand], #Fields that should automatically be parsed for taxons to associate
|
24
|
+
:image_fields => [:image_main, :image_2, :image_3, :image_4], #Image fields that should be parsed for image locations
|
25
|
+
:product_image_path => "#{Rails.root}/lib/etc/product_data/product-images/", #The location of images on disk
|
26
|
+
:rows_to_skip => 1, #If your CSV file will have headers, this field changes how many rows the reader will skip
|
27
|
+
:log_to => File.join(Rails.root, '/log/', "import_products_#{Rails.env}.log"), #Where to log to
|
28
|
+
:destroy_original_products => false, #Delete the products originally in the database after the import?
|
29
|
+
:first_row_is_headings => true, #Reads column names from first row if set to true.
|
30
|
+
:create_variants => true, #Compares products and creates a variant if that product already exists.
|
31
|
+
:variant_comparator_field => :permalink, #Which product field to detect duplicates on
|
32
|
+
:multi_domain_importing => true, #If Spree's multi_domain extension is installed, associates products with store
|
33
|
+
:store_field => :store_code, #Which field of the column mappings contains either the store id or store code?
|
34
|
+
:transaction => true #import product in a sql transaction so we can rollback when an exception is raised
|
35
|
+
}
|
36
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ImportProducts
|
2
|
+
class Engine < Rails::Engine
|
3
|
+
engine_name 'import_products'
|
4
|
+
|
5
|
+
config.autoload_paths += %W(#{config.root}/lib)
|
6
|
+
|
7
|
+
def self.activate
|
8
|
+
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
|
9
|
+
Rails.env.production? ? require(c) : load(c)
|
10
|
+
end
|
11
|
+
|
12
|
+
Dir.glob(File.join(File.dirname(__FILE__), "../../app/overrides/*.rb")) do |c|
|
13
|
+
Rails.application.config.cache_classes ? require(c) : load(c)
|
14
|
+
end
|
15
|
+
|
16
|
+
Spree::UserMailer.send(:include, ImportProducts::UserMailerExt)
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
config.to_prepare &method(:activate).to_proc
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ImportProducts
|
2
|
+
module UserMailerExt
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
def product_import_results(user, error_message = nil)
|
6
|
+
@user = user
|
7
|
+
@error_message = error_message
|
8
|
+
attachments["import_products.log"] = File.read(Spree::ProductImport.settings[:log_to]) if @error_message.nil?
|
9
|
+
mail(:to => @user.email, :from => 'fonemstr@gmail.com', :subject => "Spree: Import Products #{error_message.nil? ? "Success" : "Failure"}")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# add custom rake tasks here
|
metadata
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: import_products
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh McArthur
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-07-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: spree_core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.3.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.3.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: spree_auth_devise
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: spree_sample
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: ffaker
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.12.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.12.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: capybara
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: launchy
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 2.0.5
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 2.0.5
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: factory_girl
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: ruby-debug19
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description:
|
154
|
+
email: josh@3months.com
|
155
|
+
executables: []
|
156
|
+
extensions: []
|
157
|
+
extra_rdoc_files: []
|
158
|
+
files:
|
159
|
+
- LICENSE
|
160
|
+
- README.md
|
161
|
+
- app/controllers/spree/admin/product_imports_controller.rb
|
162
|
+
- app/jobs/import_products_job.rb
|
163
|
+
- app/models/spree/product_import.rb
|
164
|
+
- app/overrides/add_import_to_admin_sidebar_menu.rb
|
165
|
+
- app/views/spree/admin/product_imports/index.html.erb
|
166
|
+
- app/views/spree/admin/product_imports/show.html.erb
|
167
|
+
- app/views/spree/admin/shared/_import_sidebar_menu.erb
|
168
|
+
- app/views/spree/user_mailer/product_import_results.text.erb
|
169
|
+
- lib/generators/import_products/install/install_generator.rb
|
170
|
+
- lib/generators/import_products/install/templates/config/initializers/import_product_settings.rb
|
171
|
+
- lib/import_products.rb
|
172
|
+
- lib/import_products/engine.rb
|
173
|
+
- lib/import_products/user_mailer_ext.rb
|
174
|
+
- lib/tasks/import_products.rake
|
175
|
+
homepage: http://www.3months.com
|
176
|
+
licenses: []
|
177
|
+
metadata: {}
|
178
|
+
post_install_message:
|
179
|
+
rdoc_options: []
|
180
|
+
require_paths:
|
181
|
+
- lib
|
182
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: 1.8.7
|
187
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
requirements:
|
193
|
+
- none
|
194
|
+
rubyforge_project:
|
195
|
+
rubygems_version: 2.6.12
|
196
|
+
signing_key:
|
197
|
+
specification_version: 4
|
198
|
+
summary: spree_import_products ... imports products. From a CSV file via Spree's Admin
|
199
|
+
interface
|
200
|
+
test_files: []
|