shoppe 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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