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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b79095cb897e80d6693d4cb9f1458f1581d17c2
4
- data.tar.gz: c44a89fb521ab3051320be885c2255e954265b27
3
+ metadata.gz: 654732ba8cacdcd7bbca27157c736b6e580057b5
4
+ data.tar.gz: 79ca38d5d11640b2eae2cf4a6312c4ea700345c9
5
5
  SHA512:
6
- metadata.gz: 3fcb2055f19c5bdbb15b97a924f6fe6d312da850aa8ebbe3ddc94ef43e9c71f81be613e9f6c4ddff1322f224e5ac1325aec4d73e3db1a0b3d3a2e890b70c2ed1
7
- data.tar.gz: f3ee60d2f4eb75d0ecc05786cc35772400c1a8783ff37e824d50a6066b41af889884aa466e69d4088e6dc7baa34029fdf5527c352b89b7657209e013c96f7bc7
6
+ metadata.gz: 01b12f0ef8412ead707dcdb3181af156b5112de357f001e9315dc1844b1017a1d167d8b147b26e043b0a04960443d42c2202f917f70f8bd754e04ea187434cd9
7
+ data.tar.gz: de74b94ebf50aa7d0609909b04e6004655f91dd86fd530b54b41095092485bff219182ff44eb55cf1b23cc2e36f2322ff28b46430d80bf4453230daad07e04c3
@@ -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
+ ![GemVersion](https://badge.fury.io/rb/shoppe.png)
7
+ [![Code Climate](https://codeclimate.com/github/tryshoppe/core/badges/gpa.svg)](https://codeclimate.com/github/tryshoppe/core)
8
+ [![Build Status](https://travis-ci.org/tryshoppe/shoppe.svg?branch=master)](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
- @product_categories = Shoppe::ProductCategory.ordered.all
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, :product_category, :variants).order(:name).group_by(&:product_category).sort_by { |cat,pro| cat.name }
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(:product_category_id, :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])
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} &#8627; #{child.name} (#{t('shoppe.product_category.nesting.current_category')})"
19
+ else
20
+ s << "#{nested_product_category_spacing_adjusted_for_depth child, relative_depth} &#8627; #{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} &#8627; #{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
- amount = BigDecimal(amount)
54
- if refundable_amount >= amount
55
- transaction do
56
- self.class.create(:parent => self, :order_id => self.order_id, :amount => 0-amount, :method => self.method, :reference => reference)
57
- self.update_attribute(:amount_refunded, self.amount_refunded + amount)
58
- true
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 category
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
- belongs_to :product_category, :class_name => 'Shoppe::ProductCategory'
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.validates :product_category_id, :presence => true
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.product_category_id = begin
148
+ product.product_categories << begin
138
149
  if Shoppe::ProductCategory.find_by(name: row["category_name"]).present?
139
- # Find and set the category
140
- Shoppe::ProductCategory.find_by(name: row["category_name"]).id
150
+ Shoppe::ProductCategory.find_by(name: row["category_name"])
141
151
  else
142
- # Create the category
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 :products, :dependent => :restrict_with_exception, :class_name => 'Shoppe::Product'
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 => true, :permalink => true
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 { self.permalink = self.name.parameterize if self.permalink.blank? && self.name.is_a?(String) }
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
- %dl
12
+ %dl.half
13
+ %dt &nbsp;
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 @product_categories.empty?
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 @product_categories
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 :product_category_id, t('shoppe.products.product_category')
6
- %dd= f.collection_select :product_category_id, Shoppe::ProductCategory.ordered, :id, :name, {:prompt => false}, {:class => 'chosen'}
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