shoppe 1.0.5 → 1.0.6
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.
- checksums.yaml +4 -4
- data/README.md +55 -0
- data/app/assets/stylesheets/shoppe/application.scss +16 -0
- data/app/controllers/shoppe/product_categories_controller.rb +2 -2
- data/app/controllers/shoppe/products_controller.rb +2 -2
- data/app/helpers/shoppe/product_category_helper.rb +36 -0
- data/app/models/shoppe/payment.rb +14 -8
- data/app/models/shoppe/product.rb +25 -9
- data/app/models/shoppe/product_categorization.rb +14 -0
- data/app/models/shoppe/product_category.rb +49 -4
- data/app/views/shoppe/product_categories/_form.html.haml +29 -4
- data/app/views/shoppe/product_categories/index.html.haml +3 -2
- data/app/views/shoppe/products/_form.html.haml +2 -2
- data/config/locales/de.yml +650 -0
- data/config/locales/en.yml +10 -0
- data/config/locales/pl.yml +9 -0
- data/config/locales/pt-BR.yml +9 -0
- data/db/migrate/20141026181717_allow_multiple_shoppe_products_per_shoppe_product_category.rb +46 -0
- data/db/migrate/20141026181718_add_nested_to_product_categories.rb +30 -0
- data/db/schema.rb +24 -10
- data/lib/shoppe/version.rb +1 -1
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 654732ba8cacdcd7bbca27157c736b6e580057b5
|
4
|
+
data.tar.gz: 79ca38d5d11640b2eae2cf4a6312c4ea700345c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 01b12f0ef8412ead707dcdb3181af156b5112de357f001e9315dc1844b1017a1d167d8b147b26e043b0a04960443d42c2202f917f70f8bd754e04ea187434cd9
|
7
|
+
data.tar.gz: de74b94ebf50aa7d0609909b04e6004655f91dd86fd530b54b41095092485bff219182ff44eb55cf1b23cc2e36f2322ff28b46430d80bf4453230daad07e04c3
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Shoppe
|
2
|
+
|
3
|
+
Shoppe is an Rails-based e-commerce platform which allows you to easily introduce a
|
4
|
+
catalogue-based store into your Rails 4 applications.
|
5
|
+
|
6
|
+

|
7
|
+
[](https://codeclimate.com/github/tryshoppe/core)
|
8
|
+
[](https://travis-ci.org/tryshoppe/shoppe)
|
9
|
+
|
10
|
+
* [Check out the website](http://tryshoppe.com)
|
11
|
+
* [View the demo site](http://demo.tryshoppe.com)
|
12
|
+
* [Check out the demo site source](http://github.com/tryshoppe/example-store)
|
13
|
+
* [Read the release notes](https://github.com/tryshoppe/core/blob/master/CHANGELOG.md)
|
14
|
+
* [Read API documentation](http://api.tryshoppe.com)
|
15
|
+
|
16
|
+
## Features
|
17
|
+
|
18
|
+
* An attractive & easy to use admin interface with integrated authentication
|
19
|
+
* Full product/catalogue management
|
20
|
+
* Stock control
|
21
|
+
* Tax management
|
22
|
+
* Flexible & customisable order flow
|
23
|
+
* Delivery/shipping control, management & weight-based calculation
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
Shoppe provides the core framework for the store and you're responsible for creating
|
28
|
+
the storefront which your customers will use to purchase products. In addition to
|
29
|
+
creating the UI for the frontend, you are also responsible for integrating with whatever
|
30
|
+
payment gateway takes your fancy.
|
31
|
+
|
32
|
+
### Installing into a new Rails application
|
33
|
+
|
34
|
+
To get up and running with Shoppe in a new Rails application is simple. Just follow the
|
35
|
+
instructions below and you'll be up and running in minutes.
|
36
|
+
|
37
|
+
rails new my_store
|
38
|
+
cd my_store
|
39
|
+
echo "gem 'shoppe', '~> 1.0'" >> Gemfile
|
40
|
+
bundle
|
41
|
+
rails generate shoppe:setup
|
42
|
+
rails generate nifty:attachments:migration
|
43
|
+
rails generate nifty:key_value_store:migration
|
44
|
+
rake db:migrate shoppe:setup
|
45
|
+
rails server
|
46
|
+
|
47
|
+
## Contribution
|
48
|
+
|
49
|
+
If you'd like to help with this project, please get in touch with me. The best place is on
|
50
|
+
Twitter (@adamcooke) or by e-mail to adam@atechmedia.com.
|
51
|
+
|
52
|
+
## License
|
53
|
+
|
54
|
+
Shoppe is licenced under the MIT license. Full details can be found in the MIT-LICENSE
|
55
|
+
file in the root of the repository.
|
@@ -154,6 +154,11 @@ header.main {
|
|
154
154
|
.splitContainer { margin:0 25px; margin-bottom:15px; height:50px;}
|
155
155
|
.splitContainer:last-child { margin-bottom:0;}
|
156
156
|
|
157
|
+
dl.cleared {
|
158
|
+
margin-top:15px;
|
159
|
+
clear:both;
|
160
|
+
}
|
161
|
+
|
157
162
|
dl {
|
158
163
|
margin:0 25px;
|
159
164
|
margin-bottom:15px;
|
@@ -262,6 +267,17 @@ header.main {
|
|
262
267
|
width:auto;
|
263
268
|
}
|
264
269
|
}
|
270
|
+
|
271
|
+
//
|
272
|
+
// category's children table
|
273
|
+
//
|
274
|
+
table.categoryChildren {
|
275
|
+
@extend table.importExample;
|
276
|
+
a { color:#000; }
|
277
|
+
tr:nth-last-child(2) td {
|
278
|
+
border: 1px solid #ddd;
|
279
|
+
}
|
280
|
+
}
|
265
281
|
}
|
266
282
|
p.submit {margin:0; background:#fff; border:1px solid #dce2eb; padding:25px; }
|
267
283
|
p.submit .button {
|
@@ -5,7 +5,7 @@ module Shoppe
|
|
5
5
|
before_filter { params[:id] && @product_category = Shoppe::ProductCategory.find(params[:id]) }
|
6
6
|
|
7
7
|
def index
|
8
|
-
@
|
8
|
+
@product_categories_without_parent = Shoppe::ProductCategory.without_parent.ordered
|
9
9
|
end
|
10
10
|
|
11
11
|
def new
|
@@ -40,7 +40,7 @@ module Shoppe
|
|
40
40
|
private
|
41
41
|
|
42
42
|
def safe_params
|
43
|
-
params[:product_category].permit(:name, :permalink, :description, :image_file)
|
43
|
+
params[:product_category].permit(:name, :permalink, :description, :image_file, :parent_id, :permalink_includes_ancestors)
|
44
44
|
end
|
45
45
|
|
46
46
|
end
|
@@ -5,7 +5,7 @@ module Shoppe
|
|
5
5
|
before_filter { params[:id] && @product = Shoppe::Product.root.find(params[:id]) }
|
6
6
|
|
7
7
|
def index
|
8
|
-
@products = Shoppe::Product.root.includes(:stock_level_adjustments, :default_image, :
|
8
|
+
@products = Shoppe::Product.root.includes(:stock_level_adjustments, :default_image, :product_categories, :variants).order(:name).group_by(&:product_category).sort_by { |cat,pro| cat.name }
|
9
9
|
end
|
10
10
|
|
11
11
|
def new
|
@@ -51,7 +51,7 @@ module Shoppe
|
|
51
51
|
private
|
52
52
|
|
53
53
|
def safe_params
|
54
|
-
params[:product].permit(:
|
54
|
+
params[:product].permit(:name, :sku, :permalink, :description, :short_description, :weight, :price, :cost_price, :tax_rate_id, :stock_control, :default_image_file, :data_sheet_file, :active, :featured, :in_the_box, :product_attributes_array => [:key, :value, :searchable, :public], :product_category_ids => [])
|
55
55
|
end
|
56
56
|
|
57
57
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Shoppe
|
2
|
+
module ProductCategoryHelper
|
3
|
+
|
4
|
+
def nested_product_category_spacing_adjusted_for_depth(category, relative_depth)
|
5
|
+
depth = category.depth - relative_depth
|
6
|
+
spacing = depth < 2 ? 0.8 : 1.5
|
7
|
+
("<span style='display:inline-block;width:#{spacing}em;'></span>"*category.depth).html_safe
|
8
|
+
end
|
9
|
+
|
10
|
+
def nested_product_category_rows(category, current_category = nil, link_to_current = true, relative_depth = 0)
|
11
|
+
if category.present? && category.children.count > 0
|
12
|
+
String.new.tap do |s|
|
13
|
+
category.children.ordered.each do |child|
|
14
|
+
s << "<tr>"
|
15
|
+
s << "<td>"
|
16
|
+
if child == current_category
|
17
|
+
if link_to_current == false
|
18
|
+
s << "#{nested_product_category_spacing_adjusted_for_depth child, relative_depth} ↳ #{child.name} (#{t('shoppe.product_category.nesting.current_category')})"
|
19
|
+
else
|
20
|
+
s << "#{nested_product_category_spacing_adjusted_for_depth child, relative_depth} ↳ #{link_to(child.name, [:edit, child]).html_safe} (#{t('shoppe.product_category.nesting.current_category')})"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
s << "#{nested_product_category_spacing_adjusted_for_depth child, relative_depth} ↳ #{link_to(child.name, [:edit, child]).html_safe}"
|
24
|
+
end
|
25
|
+
s << "</td>"
|
26
|
+
s << "</tr>"
|
27
|
+
s << nested_product_category_rows(child, current_category, link_to_current, relative_depth)
|
28
|
+
end
|
29
|
+
end.html_safe
|
30
|
+
else
|
31
|
+
""
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -1,6 +1,10 @@
|
|
1
1
|
module Shoppe
|
2
2
|
class Payment < ActiveRecord::Base
|
3
3
|
|
4
|
+
# Additional callbacks
|
5
|
+
extend ActiveModel::Callbacks
|
6
|
+
define_model_callbacks :refund
|
7
|
+
|
4
8
|
# The associated order
|
5
9
|
#
|
6
10
|
# @return [Shoppe::Order]
|
@@ -50,15 +54,17 @@ module Shoppe
|
|
50
54
|
# @param amount [String] the amount which should be refunded
|
51
55
|
# @return [Boolean]
|
52
56
|
def refund!(amount)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
57
|
+
run_callbacks :refund do
|
58
|
+
amount = BigDecimal(amount)
|
59
|
+
if refundable_amount >= amount
|
60
|
+
transaction do
|
61
|
+
self.class.create(:parent => self, :order_id => self.order_id, :amount => 0-amount, :method => self.method, :reference => reference)
|
62
|
+
self.update_attribute(:amount_refunded, self.amount_refunded + amount)
|
63
|
+
true
|
64
|
+
end
|
65
|
+
else
|
66
|
+
raise Shoppe::Errors::RefundFailed, :message => I18n.t('.refund_failed', refundable_amount: refundable_amount)
|
59
67
|
end
|
60
|
-
else
|
61
|
-
raise Shoppe::Errors::RefundFailed, :message => I18n.t('.refund_failed', refundable_amount: refundable_amount)
|
62
68
|
end
|
63
69
|
end
|
64
70
|
|
@@ -13,10 +13,14 @@ module Shoppe
|
|
13
13
|
attachment :default_image
|
14
14
|
attachment :data_sheet
|
15
15
|
|
16
|
-
# The product's
|
16
|
+
# The product's categorizations
|
17
|
+
#
|
18
|
+
# @return [Shoppe::ProductCategorization]
|
19
|
+
has_many :product_categorizations, dependent: :destroy, class_name: 'Shoppe::ProductCategorization', inverse_of: :product
|
20
|
+
# The product's categories
|
17
21
|
#
|
18
22
|
# @return [Shoppe::ProductCategory]
|
19
|
-
|
23
|
+
has_many :product_categories, class_name: 'Shoppe::ProductCategory', through: :product_categorizations
|
20
24
|
|
21
25
|
# The product's tax rate
|
22
26
|
#
|
@@ -34,7 +38,7 @@ module Shoppe
|
|
34
38
|
|
35
39
|
# Validations
|
36
40
|
with_options :if => Proc.new { |p| p.parent.nil? } do |product|
|
37
|
-
product.
|
41
|
+
product.validate :has_at_least_one_product_category
|
38
42
|
product.validates :description, :presence => true
|
39
43
|
product.validates :short_description, :presence => true
|
40
44
|
end
|
@@ -94,6 +98,13 @@ module Shoppe
|
|
94
98
|
self.stock_level_adjustments.sum(:adjustment)
|
95
99
|
end
|
96
100
|
|
101
|
+
# Return the first product category
|
102
|
+
#
|
103
|
+
# @return [Shoppe::ProductCategory]
|
104
|
+
def product_category
|
105
|
+
self.product_categories.first rescue nil
|
106
|
+
end
|
107
|
+
|
97
108
|
# Search for products which include the given attributes and return an active record
|
98
109
|
# scope of these products. Chainable with other scopes and with_attributes methods.
|
99
110
|
# For example:
|
@@ -134,19 +145,16 @@ module Shoppe
|
|
134
145
|
product.weight = row["weight"]
|
135
146
|
product.price = row["price"].nil? ? 0 : row["price"]
|
136
147
|
|
137
|
-
product.
|
148
|
+
product.product_categories << begin
|
138
149
|
if Shoppe::ProductCategory.find_by(name: row["category_name"]).present?
|
139
|
-
|
140
|
-
Shoppe::ProductCategory.find_by(name: row["category_name"]).id
|
150
|
+
Shoppe::ProductCategory.find_by(name: row["category_name"])
|
141
151
|
else
|
142
|
-
|
143
|
-
Shoppe::ProductCategory.create(name: row["category_name"]).id
|
152
|
+
Shoppe::ProductCategory.create(name: row["category_name"])
|
144
153
|
end
|
145
154
|
end
|
146
155
|
|
147
156
|
product.save!
|
148
157
|
|
149
|
-
# Create quantities
|
150
158
|
qty = row["qty"].to_i
|
151
159
|
if qty > 0
|
152
160
|
product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
|
@@ -165,5 +173,13 @@ module Shoppe
|
|
165
173
|
end
|
166
174
|
end
|
167
175
|
|
176
|
+
private
|
177
|
+
|
178
|
+
# Validates
|
179
|
+
|
180
|
+
def has_at_least_one_product_category
|
181
|
+
errors.add(:base, 'must add at least one product category') if self.product_categories.blank?
|
182
|
+
end
|
183
|
+
|
168
184
|
end
|
169
185
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Shoppe
|
2
|
+
class ProductCategorization < ActiveRecord::Base
|
3
|
+
|
4
|
+
self.table_name = 'shoppe_product_categorizations'
|
5
|
+
|
6
|
+
# Links back
|
7
|
+
belongs_to :product, class_name: 'Shoppe::Product'
|
8
|
+
belongs_to :product_category, class_name: 'Shoppe::ProductCategory'
|
9
|
+
|
10
|
+
# Validations
|
11
|
+
validates_presence_of :product, :product_category
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -1,23 +1,68 @@
|
|
1
|
+
require 'awesome_nested_set'
|
2
|
+
|
1
3
|
module Shoppe
|
2
4
|
class ProductCategory < ActiveRecord::Base
|
3
5
|
|
6
|
+
# Allow the nesting of product categories
|
7
|
+
# :restrict_with_exception relies on a fix to the awesome_nested_set gem
|
8
|
+
# which has been referenced in the Gemfile as we can't add a dependency
|
9
|
+
# to a branch in the .gemspec
|
10
|
+
acts_as_nested_set dependent: :restrict_with_exception,
|
11
|
+
after_move: :set_ancestral_permalink
|
12
|
+
|
4
13
|
self.table_name = 'shoppe_product_categories'
|
5
|
-
|
14
|
+
|
6
15
|
# Categories have an image attachment
|
7
16
|
attachment :image
|
8
17
|
|
9
18
|
# All products within this category
|
10
|
-
has_many :
|
19
|
+
has_many :product_categorizations, dependent: :restrict_with_exception, class_name: 'Shoppe::ProductCategorization', inverse_of: :product_category
|
20
|
+
has_many :products, class_name: 'Shoppe::Product', through: :product_categorizations
|
11
21
|
|
12
22
|
# Validations
|
13
23
|
validates :name, :presence => true
|
14
|
-
validates :permalink, :presence => true, :uniqueness =>
|
24
|
+
validates :permalink, :presence => true, :uniqueness => {scope: :parent_id}, :permalink => true
|
15
25
|
|
16
26
|
# All categories ordered by their name ascending
|
17
27
|
scope :ordered, -> { order(:name) }
|
18
28
|
|
29
|
+
# Root (no parent) product categories only
|
30
|
+
scope :without_parent, -> { where(parent_id: nil) }
|
31
|
+
|
32
|
+
# No descendents
|
33
|
+
scope :except_descendants, ->(record) { where.not(id: (Array.new(record.descendants) << record).flatten) }
|
34
|
+
|
19
35
|
# Set the permalink on callback
|
20
|
-
before_validation
|
36
|
+
before_validation :set_permalink, :set_ancestral_permalink
|
37
|
+
after_save :set_child_permalinks
|
38
|
+
|
39
|
+
def combined_permalink
|
40
|
+
if self.permalink_includes_ancestors && self.ancestral_permalink.present?
|
41
|
+
"#{self.ancestral_permalink}/#{self.permalink}"
|
42
|
+
else
|
43
|
+
self.permalink
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def set_permalink
|
50
|
+
self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String)
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_ancestral_permalink
|
54
|
+
permalinks = Array.new
|
55
|
+
self.ancestors.each do |category|
|
56
|
+
permalinks << category.permalink
|
57
|
+
end
|
58
|
+
self.ancestral_permalink = permalinks.join '/'
|
59
|
+
end
|
21
60
|
|
61
|
+
def set_child_permalinks
|
62
|
+
self.children.each do |category|
|
63
|
+
category.save! # save forces children to update their ancestral_permalink
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
22
67
|
end
|
23
68
|
end
|
@@ -1,17 +1,42 @@
|
|
1
1
|
= form_for @product_category do |f|
|
2
2
|
= f.error_messages
|
3
|
+
|
3
4
|
= field_set_tag t('shoppe.product_category.category_details') do
|
5
|
+
%dl
|
6
|
+
%dt= f.label :name, t('shoppe.product_category.name')
|
7
|
+
%dd= f.text_field :name, :class => 'focus text'
|
4
8
|
.splitContainer
|
5
|
-
%dl.half
|
6
|
-
%dt= f.label :name, t('shoppe.product_category.name')
|
7
|
-
%dd= f.text_field :name, :class => 'focus text'
|
8
9
|
%dl.half
|
9
10
|
%dt= f.label :permalink, t('shoppe.product_category.permalink')
|
10
11
|
%dd= f.text_field :permalink, :class => 'text'
|
11
|
-
|
12
|
+
%dl.half
|
13
|
+
%dt
|
14
|
+
%dd.checkbox
|
15
|
+
= f.check_box :permalink_includes_ancestors
|
16
|
+
= f.label :permalink_includes_ancestors, t('shoppe.product_category.permalink_includes_ancestors')
|
17
|
+
%dl.cleared
|
12
18
|
%dt= f.label :description, t('shoppe.product_category.description')
|
13
19
|
%dd= f.text_area :description, :class => 'text'
|
14
20
|
|
21
|
+
= field_set_tag t('shoppe.product_category.nesting.category_nesting') do
|
22
|
+
%dl
|
23
|
+
%dt= f.label :parent_id, t('shoppe.product_category.nesting.category_parent')
|
24
|
+
%dd= f.collection_select :parent_id, Shoppe::ProductCategory.except_descendants(@product_category).ordered, :id, :name, {:include_blank => t('shoppe.product_category.nesting.blank_option')}, {:class => 'chosen'}
|
25
|
+
%dl
|
26
|
+
%dt= f.label :child_ids, t('shoppe.product_category.nesting.hierarchy')
|
27
|
+
%dd
|
28
|
+
%table.categoryChildren
|
29
|
+
%tbody
|
30
|
+
- if @product_category.children.count == 0
|
31
|
+
%tr
|
32
|
+
%td
|
33
|
+
= t('shoppe.product_category.nesting.no_children')
|
34
|
+
- else
|
35
|
+
%tr
|
36
|
+
%td
|
37
|
+
= "#{@product_category.name} (#{t('shoppe.product_category.nesting.current_category')})"
|
38
|
+
= nested_product_category_rows(@product_category, current_category = @product_category, link_to_current = false, relative_depth = @product_category.depth)
|
39
|
+
|
15
40
|
= field_set_tag t('shoppe.product_category.attachments') do
|
16
41
|
%dl
|
17
42
|
%dt= f.label :image_file, t('shoppe.product_category.image')
|
@@ -10,10 +10,11 @@
|
|
10
10
|
%tr
|
11
11
|
%th= t('shoppe.product_category.name')
|
12
12
|
%tbody
|
13
|
-
- if @
|
13
|
+
- if @product_categories_without_parent.empty?
|
14
14
|
%tr.empty
|
15
15
|
%td= t('shoppe.product_category.no_categories')
|
16
16
|
- else
|
17
|
-
- for cat in @
|
17
|
+
- for cat in @product_categories_without_parent
|
18
18
|
%tr
|
19
19
|
%td= link_to cat.name, [:edit, cat]
|
20
|
+
= nested_product_category_rows(cat)
|
@@ -2,8 +2,8 @@
|
|
2
2
|
= f.error_messages
|
3
3
|
= field_set_tag t('shoppe.products.product_information') do
|
4
4
|
%dl
|
5
|
-
%dt= f.label :
|
6
|
-
%dd= f.collection_select :
|
5
|
+
%dt= f.label :product_categories, t('shoppe.product_category.product_categories')
|
6
|
+
%dd= f.collection_select :product_category_ids, Shoppe::ProductCategory.ordered, :id, :name, {:prompt => false}, {:class => 'chosen', :multiple => true, :data => {:placeholder => t('shoppe.product_category.choose_product_category') }}
|
7
7
|
|
8
8
|
.splitContainer
|
9
9
|
%dl.third
|