stall 0.3.3 → 0.3.4

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/para/stall/inputs/variant-select.coffee +4 -3
  3. data/app/assets/javascripts/stall.coffee +3 -1
  4. data/app/assets/javascripts/stall/add-product-to-wish-list-button.coffee +99 -0
  5. data/app/assets/javascripts/stall/add-to-wish-list-button.coffee +17 -0
  6. data/app/assets/javascripts/stall/product-list-form.coffee +31 -0
  7. data/app/controllers/stall/cart_line_items_controller.rb +21 -0
  8. data/app/controllers/stall/curated_product_lists_controller.rb +6 -0
  9. data/app/controllers/stall/line_items_controller.rb +14 -7
  10. data/app/controllers/stall/products_breadcrumbs.rb +8 -1
  11. data/app/controllers/stall/products_controller.rb +12 -0
  12. data/app/controllers/stall/wish_list_line_items_controller.rb +52 -0
  13. data/app/controllers/stall/wish_lists_controller.rb +7 -0
  14. data/app/helpers/stall/add_to_cart_helper.rb +3 -2
  15. data/app/helpers/stall/add_to_wish_list_helper.rb +30 -0
  16. data/app/helpers/stall/products_helper.rb +13 -0
  17. data/app/models/stall/models/cart.rb +0 -6
  18. data/app/models/stall/models/customer.rb +9 -0
  19. data/app/models/stall/models/line_item.rb +13 -0
  20. data/app/models/stall/models/product.rb +1 -1
  21. data/app/models/stall/models/product_list.rb +4 -0
  22. data/app/models/stall/models/variant.rb +4 -0
  23. data/app/models/stall/models/wish_list.rb +18 -0
  24. data/app/models/wish_list.rb +3 -0
  25. data/app/services/stall/add_to_cart_service.rb +4 -47
  26. data/app/services/stall/add_to_product_list_service.rb +63 -0
  27. data/app/services/stall/add_to_wish_list_service.rb +17 -0
  28. data/app/services/stall/available_stocks_service.rb +1 -1
  29. data/app/services/stall/shipping_fee_calculator_service.rb +22 -9
  30. data/app/views/admin/products/_form.html.haml +5 -0
  31. data/app/views/checkout/steps/_informations.html.haml +1 -1
  32. data/app/views/stall/addresses/_fields.html.haml +4 -4
  33. data/app/views/stall/{line_items → cart_line_items}/_add_error.html.haml +0 -0
  34. data/app/views/stall/{line_items → cart_line_items}/_added.html.haml +2 -2
  35. data/app/views/stall/{line_items → cart_line_items}/_form.html.haml +1 -1
  36. data/app/views/stall/carts/_widget.html.haml +1 -1
  37. data/app/views/stall/carts/show.html.haml +4 -4
  38. data/app/views/stall/curated_product_lists/show.html.haml +1 -1
  39. data/app/views/stall/customers/_fields.html.haml +1 -1
  40. data/app/views/stall/customers/_sign_in.html.haml +2 -2
  41. data/app/views/stall/products/_list.html.haml +2 -0
  42. data/app/views/stall/products/_product.html.haml +2 -1
  43. data/app/views/stall/wish_list_line_items/_add_error.html.haml +17 -0
  44. data/app/views/stall/wish_list_line_items/_added.html.haml +19 -0
  45. data/app/views/stall/wish_list_line_items/_button.html.haml +3 -0
  46. data/app/views/stall/wish_list_line_items/_form.html.haml +12 -0
  47. data/app/views/stall/wish_lists/show.html.haml +21 -0
  48. data/config/locales/stall.fr.yml +26 -0
  49. data/db/migrate/20170425085606_add_weight_to_stall_products_and_variants.rb +6 -0
  50. data/db/migrate/20170426163450_add_vat_rate_to_stall_products.rb +5 -0
  51. data/db/migrate/20170522062334_change_variants_weight_default_to_nil.rb +11 -0
  52. data/lib/generators/stall/install/templates/initializer.rb +18 -0
  53. data/lib/stall.rb +1 -0
  54. data/lib/stall/addressable.rb +35 -4
  55. data/lib/stall/addresses/copy_source_to_target.rb +14 -10
  56. data/lib/stall/addresses/prefill_target_from_source.rb +10 -6
  57. data/lib/stall/config.rb +4 -0
  58. data/lib/stall/engine.rb +1 -0
  59. data/lib/stall/routes.rb +42 -19
  60. data/lib/stall/sellable.rb +1 -1
  61. data/lib/stall/shipping/calculator.rb +1 -1
  62. data/lib/stall/shipping/country_weight_table_calculator.rb +3 -2
  63. data/lib/stall/version.rb +1 -1
  64. data/lib/stall/wish_list_helper.rb +93 -0
  65. metadata +26 -6
  66. data/app/assets/javascripts/stall/cart-form.coffee +0 -28
@@ -0,0 +1,17 @@
1
+ .modal.danger.fade{ role: 'dialog', tabindex: '-1', data: { :'line-item' => @line_item.as_json(only: [:name, :quantity, :eot_price, :price, :vat_rate]) } }
2
+ .modal-dialog
3
+ .modal-content
4
+ .modal-header
5
+ %button.close{ type: 'button', :'aria-label' => 'Close', data: { dismiss: 'modal' } }
6
+ %span{ :'aria-hidden' => 'true' }
7
+ ×
8
+ %h4.modal-title
9
+ = t('stall.line_items.add_error.title')
10
+
11
+ .modal-body
12
+ %p.text-danger
13
+ = t('stall.line_items.add_error.description')
14
+
15
+ .modal-footer
16
+ %button.btn.btn-default{ type: 'button', data: { dismiss: 'modal' } }
17
+ = t('stall.shared.close')
@@ -0,0 +1,19 @@
1
+ .modal.fade{ role: 'dialog', tabindex: '-1', data: { :'wish-list' => { token: @product_list.token, :'total-quantity' => @product_list.total_quantity } } }
2
+ .modal-dialog
3
+ .modal-content
4
+ .modal-header
5
+ %button.close{ type: 'button', :'aria-label' => 'Close', data: { dismiss: 'modal' } }
6
+ %span{ :'aria-hidden' => 'true' }
7
+ ×
8
+ %h4.modal-title
9
+ = t('stall.wish_list_line_items.added.title')
10
+
11
+ .modal-body
12
+ = @line_item.name
13
+
14
+ .modal-footer
15
+ %button.btn.btn-default{ type: 'button', data: { dismiss: 'modal' } }
16
+ = t('stall.wish_list_line_items.added.continue_shopping')
17
+
18
+ = link_to wish_list_path(@product_list), class: 'btn btn-primary', rel: 'nofollow' do
19
+ = t('stall.wish_lists.actions.view_wish_list')
@@ -0,0 +1,3 @@
1
+ %button.btn.btn-default.add-to-wish-list-btn{ type: 'button', data: { :'add-to-wish-list' => true, title: title, url: url, included: included, :'product-id' => product.id, :'variant-id' => variant.try(:id), :'popover-content' => popover_content } }
2
+ = fa_icon (included ? 'heart' : 'heart-o'), data: { :'wish-list-icon' => true }
3
+ = fa_icon 'spinner spin', class: 'hidden', data: { :'wish-list-loading-spinner' => true }
@@ -0,0 +1,12 @@
1
+ = simple_form_for line_item, as: :line_item, url: wish_list_line_items_path(wish_list), remote: true, data: { :'add-to-wish-list-form' => true, :'error-messages' => { choose: t('stall.line_items.errors.choose'), quantity: t('stall.line_items.errors.quantity') } } do |form|
2
+ = form.hidden_field :sellable_type, value: 'Variant'
3
+ = form.input_field :sellable, as: :variant_select, product: product
4
+ = form.hidden_field :quantity, value: 1
5
+
6
+ %button.btn.btn-primary{ type: 'submit', autocomplete: 'off', data: { :'loading-text' => t('stall.shared.sending') } }
7
+ = fa_icon 'heart-o'
8
+ = t('stall.wish_list_line_items.form.add')
9
+
10
+ %button.btn.btn-danger{ type: 'button', data: { :'cancel-button' => true } }
11
+ = fa_icon 'times'
12
+ = t('stall.shared.cancel')
@@ -0,0 +1,21 @@
1
+ .wish-list-recap{ data: { :'product-list-recap' => true } }
2
+ = simple_form_for @wish_list, url: wish_list_path(@wish_list), as: :wish_list, remote: true, data: { :'product-list-form' => true } do |form|
3
+ %table.table.table-striped
4
+ %thead
5
+ %tr
6
+ %th= LineItem.human_attribute_name(:name)
7
+ %th= LineItem.human_attribute_name(:unit_price)
8
+ %th
9
+
10
+ %tbody
11
+ = form.fields_for :line_items do |line_item_fields|
12
+ %tr.line-item-row{ data: { :'line-item-id' => line_item_fields.object.id } }
13
+ %td= line_item_fields.object.name
14
+ %td= line_item_fields.object.unit_price
15
+ %td= link_to_remove_association '×'.html_safe, line_item_fields, wrapper_class: 'line-item-row'
16
+
17
+ .form-actions
18
+ %button.btn.btn-default{ type: 'submit', data: { :'product-list-update-button' => true } }
19
+ = t('stall.wish_lists.recap.update')
20
+
21
+
@@ -5,6 +5,7 @@ fr:
5
5
  filters:
6
6
  paid_after: Payé après le
7
7
  paid_before: Payé avant le
8
+
8
9
  variants_matrix:
9
10
  enabled: "Publier ?"
10
11
  price: "Prix"
@@ -31,6 +32,10 @@ fr:
31
32
  sent: "Notification envoyée !"
32
33
  sent_description: "Le client a bien été notifié de l'expédition de sa commande. "
33
34
 
35
+ products:
36
+ weight_hint: "Indiquez le poids du produit <b>en grammes</b>"
37
+ vat_rate_hint: "Taux de TVA en %. <b>%{rate} si vide.</b>"
38
+
34
39
  forms:
35
40
  tabs:
36
41
  cart:
@@ -45,15 +50,18 @@ fr:
45
50
  images: "Images"
46
51
  descriptions: "Descriptions"
47
52
  variants: "Déclinaisons"
53
+ shipping: "Livraison"
48
54
 
49
55
  components:
50
56
  section:
51
57
  products: "Catalogue"
58
+ selling: "Vente"
52
59
  component:
53
60
  products: "Produits"
54
61
  product_categories: "Catégories de produits"
55
62
  properties: "Propriétés de produit"
56
63
  manufacturers: "Fabricants"
64
+ carts: "Commandes / Paniers"
57
65
 
58
66
  stall:
59
67
  shared:
@@ -99,6 +107,10 @@ fr:
99
107
  validate: "Passer la commande"
100
108
  paid_at: "Payée le %{at}"
101
109
 
110
+ wish_lists:
111
+ actions:
112
+ view_wish_list: "Voir ma liste"
113
+
102
114
  line_items:
103
115
  form:
104
116
  add_to_cart: "Ajouter au panier"
@@ -112,6 +124,14 @@ fr:
112
124
  choose: "Merci de choisir un produit afin de l'ajouter au panier"
113
125
  quantity: "Merci de choisir une quantité à ajouter au panier"
114
126
 
127
+ wish_list_line_items:
128
+ form:
129
+ add: "Ajouter à ma Wish list"
130
+ remove: "Retirer de ma Wish list"
131
+ added:
132
+ title: "Votre produit a bien été ajouté à votre Wish list"
133
+ continue_shopping: "Continuer ma visite"
134
+
115
135
  checkout:
116
136
  shared:
117
137
  not_checkoutable: "Le panier a expiré, celui-ci a probablement déjà été payé."
@@ -349,6 +369,12 @@ fr:
349
369
  manufacturer: "Fabricant"
350
370
  suggested_products: "Produits suggérés"
351
371
  price: "Prix"
372
+ vat_rate: "Taux de TVA"
373
+ weight: "Poids du produit"
374
+ variant:
375
+ price: "Prix"
376
+ stock: "Quantité en stock"
377
+ weight: "Poids"
352
378
  product_category:
353
379
  name: "Nom"
354
380
  slug: "Fragment d'URL"
@@ -0,0 +1,6 @@
1
+ class AddWeightToStallProductsAndVariants < ActiveRecord::Migration
2
+ def change
3
+ add_column :stall_products, :weight, :integer, default: 0
4
+ add_column :stall_variants, :weight, :integer, default: 0
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddVatRateToStallProducts < ActiveRecord::Migration
2
+ def change
3
+ add_column :stall_products, :vat_rate, :decimal, precision: 4, scale: 2
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class ChangeVariantsWeightDefaultToNil < ActiveRecord::Migration
2
+ def up
3
+ change_column_default :stall_variants, :weight, nil
4
+
5
+ Variant.where(weight: 0).find_each { |v| v.update(weight: nil) }
6
+ end
7
+
8
+ def down
9
+ change_column_default :stall_variants, :weight, 0
10
+ end
11
+ end
@@ -33,6 +33,24 @@ Stall.configure do |config|
33
33
  #
34
34
  # config.prices_precision = 2
35
35
 
36
+ # Override the controllers used in stall's default routing.
37
+ #
38
+ # This allows to extend or replace default controller behaviors by providing
39
+ # a route-compatible controller name with the right key
40
+ #
41
+ # Please read the routes source to see which controllers your can override
42
+ # with this config :
43
+ #
44
+ # https://github.com/rails-stall/stall/blob/master/lib/stall/routes.rb
45
+ #
46
+ # Note : If you define controllers here that inherit from stall controllers,
47
+ # to extend their behavior, please fill the `config.default_layout`
48
+ # config to avoid layout issues, or define the `#set_stall_layout`
49
+ # in your controllers, returing the layout name to render, or set
50
+ # the `.layout` macro in your controller.
51
+ #
52
+ # config.controllers = { products: 'products' }
53
+
36
54
  # User omniauth providers that will be allowed for customers to authenticate
37
55
  # with.
38
56
  #
@@ -50,6 +50,7 @@ module Stall
50
50
  autoload :Routes
51
51
  autoload :CartHelper
52
52
  autoload :ArchivedPaidCartHelper
53
+ autoload :WishListHelper
53
54
  autoload :Config
54
55
  autoload :Utils
55
56
 
@@ -22,23 +22,54 @@ module Stall
22
22
  end
23
23
 
24
24
  # Allow billing address to fall back to shipping address when not filled
25
- def billing_address
26
- association(:billing_address).load_target ||
25
+ def billing_address(force_actual_address: false)
26
+ if (billing_address = association(:billing_address).load_target)
27
+ billing_address
28
+ elsif !@_force_actual_address_association && !force_actual_address
27
29
  association(:shipping_address).load_target
30
+ end
28
31
  end
29
32
 
30
33
  def billing_address?
31
34
  billing_address.try(:persisted?) && billing_address.billing?
32
35
  end
33
36
 
37
+ def billing_address_attributes=(attributes)
38
+ with_actual_address_associations do
39
+ assign_nested_attributes_for_one_to_one_association(:billing_address, attributes)
40
+ end
41
+ end
42
+
34
43
  # Allow shipping address to fall back to billing address when not filled
35
- def shipping_address
36
- association(:shipping_address).load_target ||
44
+ def shipping_address(force_actual_address: false)
45
+ if (shipping_address = association(:shipping_address).load_target)
46
+ shipping_address
47
+ elsif !@_force_actual_address_association && !force_actual_address
37
48
  association(:billing_address).load_target
49
+ end
38
50
  end
39
51
 
40
52
  def shipping_address?
41
53
  shipping_address.try(:persisted?) && shipping_address.shipping?
42
54
  end
55
+
56
+ def shipping_address_attributes=(attributes)
57
+ with_actual_address_associations do
58
+ assign_nested_attributes_for_one_to_one_association(:shipping_address, attributes)
59
+ end
60
+ end
61
+
62
+ # Allow forcing actual address associations to be retrieved, thus disabling
63
+ # addresses fallback for the duration of the block.
64
+ #
65
+ # This is used for nested attributes assignation which calls the
66
+ # associations reader directly, which happen to merge addresses when
67
+ # both billing and shipping addresses are assigned at the same time.
68
+ #
69
+ def with_actual_address_associations
70
+ @_force_actual_address_association = true
71
+ yield
72
+ @_force_actual_address_association = false
73
+ end
43
74
  end
44
75
  end
@@ -17,17 +17,21 @@ module Stall
17
17
  # Update or create target address with source attributes
18
18
  #
19
19
  def copy_address(type)
20
- address = if target.send(:"#{ type }_address?")
21
- target.send(:"#{ type }_address")
22
- else
23
- target.send(:"build_#{ type }_address")
24
- end
20
+ source.with_actual_address_associations do
21
+ target.with_actual_address_associations do
22
+ address = if target.send(:"#{ type }_address?")
23
+ target.send(:"#{ type }_address")
24
+ else
25
+ target.send(:"build_#{ type }_address")
26
+ end
25
27
 
26
- if source.send(:"#{ type }_address?")
27
- attributes = duplicate_attributes(source.send(:"#{ type }_address"))
28
- address.assign_attributes(attributes)
29
- else
30
- address.try(:mark_for_destruction)
28
+ if source.send(:"#{ type }_address?")
29
+ attributes = duplicate_attributes(source.send(:"#{ type }_address"))
30
+ address.assign_attributes(attributes)
31
+ else
32
+ address.try(:mark_for_destruction)
33
+ end
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -9,13 +9,17 @@ module Stall
9
9
  private
10
10
 
11
11
  def prefill_address(type)
12
- source_address = source.send("#{ type }_address")
12
+ source.with_actual_address_associations do
13
+ target.with_actual_address_associations do
14
+ source_address = source.send("#{ type }_address")
13
15
 
14
- if source_address && !target.send("#{ type }_address")
15
- attributes = duplicate_attributes(source_address)
16
- target.send("build_#{ type }_address", attributes)
17
- elsif !target.send("#{ type }_address")
18
- target.send("build_#{ type }_address")
16
+ if source_address && !target.send("#{ type }_address?")
17
+ attributes = duplicate_attributes(source_address)
18
+ target.send("build_#{ type }_address", attributes)
19
+ elsif !target.send("#{ type }_address?")
20
+ target.send("build_#{ type }_address")
21
+ end
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -21,6 +21,10 @@ module Stall
21
21
  # Default prices precision for rounding
22
22
  param :prices_precision, 2
23
23
 
24
+ # Controller overrides to allow extending or replacing default controllers
25
+ # behaviors
26
+ param :controllers, {}.with_indifferent_access
27
+
24
28
  # User omniauth providers
25
29
  param :devise_for_user_config, { controllers: { omniauth_callbacks: 'stall/omniauth_callbacks' } }
26
30
 
@@ -21,6 +21,7 @@ module Stall
21
21
  config.to_prepare do
22
22
  ::ApplicationController.send(:include, Stall::CartHelper)
23
23
  ::ApplicationController.send(:include, Stall::ArchivedPaidCartHelper)
24
+ ::ApplicationController.send(:include, Stall::WishListHelper)
24
25
  end
25
26
 
26
27
  initializer 'stall.ensure_shipping_method_for_all_calculators' do
@@ -7,6 +7,8 @@ module Stall
7
7
  end
8
8
 
9
9
  def draw(mount_location)
10
+ routes = self
11
+
10
12
  router.instance_eval do
11
13
  devise_for :users, Stall.config.devise_for_user_config
12
14
 
@@ -14,33 +16,43 @@ module Stall
14
16
  get '/users/omniauth/:provider/redirect' => 'stall/omniauth_callbacks#redirect', as: :user_omniauth_redirect
15
17
  end
16
18
 
17
- scope mount_location, module: :stall do
18
- resources :products, only: [:index], as: :products
19
+ scope mount_location do
20
+ resources :products, only: [:index], as: :products, controller: routes.controller_for(:products)
19
21
 
20
22
  constraints ProductExistsConstraint.new do
21
- resources :products, path: '/', only: [:show]
23
+ resources :products, path: '/', only: [:show], controller: routes.controller_for(:products)
24
+ end
25
+
26
+ constraints ProductCategoryExistsConstraint.new do
27
+ resources :product_categories, path: '/', only: [:show], controller: routes.controller_for(:product_categories)
22
28
  end
23
29
 
24
30
  constraints CuratedProductListExistsConstraint.new do
25
- resources :curated_product_lists, path: '/', only: [:show]
31
+ resources :curated_product_lists, path: '/', only: [:show], controller: routes.controller_for(:curated_product_lists) do
32
+ resources :products, only: [:show], path: '/', controller: routes.controller_for(:products)
33
+ end
26
34
  end
27
35
 
28
- constraints ProductCategoryExistsConstraint.new do
29
- resources :product_categories, path: '/', only: [:show]
36
+ constraints ManufacturerExistsConstraint.new do
37
+ resources :manufacturers, path: '/', only: [:show], controller: routes.controller_for(:manufacturers) do
38
+ resources :products, only: [:show], path: '/', controller: routes.controller_for(:products)
39
+ end
30
40
  end
31
41
 
32
- resources :manufacturers, only: [:show]
42
+ resources :carts, controller: routes.controller_for(:carts) do
43
+ resources :line_items, only: [:create], controller: routes.controller_for(:cart_line_items)
44
+ resource :credit, only: [:update, :destroy], controller: routes.controller_for(:cart_credits)
45
+ end
33
46
 
34
- resources :carts do
35
- resources :line_items
36
- resource :credit, controller: 'cart_credits', only: [:update, :destroy]
47
+ resources :wish_lists, only: [:show], controller: routes.controller_for(:wish_lists) do
48
+ resources :line_items, only: [:create, :destroy], controller: routes.controller_for(:wish_list_line_items)
37
49
  end
38
50
 
39
- get 'checkout/:cart_key' => 'checkouts#show', as: :checkout
51
+ get 'checkout/:cart_key' => "#{routes.controller_for(:checkouts)}#show", as: :checkout
40
52
 
41
- scope 'checkout', module: 'checkout', as: :checkout do
53
+ scope 'checkout', as: :checkout do
42
54
  scope '(:cart_key)' do
43
- resource :step, only: [:show, :update] do
55
+ resource :step, only: [:show, :update], controller: routes.controller_for(:'checkout/steps') do
44
56
  post '/', action: :update, as: :update
45
57
  get '/process', action: :update, as: :process
46
58
  # Allow external URLs process steps, allowing some payment
@@ -51,9 +63,8 @@ module Stall
51
63
  end
52
64
  end
53
65
 
54
-
55
66
  scope '/:gateway' do
56
- resource :payment, only: [] do
67
+ resource :payment, only: [], controller: routes.controller_for(:payments) do
57
68
  member do
58
69
  match 'notify', action: 'notify', via: [:get, :post]
59
70
  end
@@ -63,10 +74,8 @@ module Stall
63
74
  end
64
75
  end
65
76
 
66
- class CuratedProductListExistsConstraint
67
- def matches?(request)
68
- CuratedProductList.exists?(slug: request.params[:id])
69
- end
77
+ def controller_for(key)
78
+ Stall.config.controllers[key] || "stall/#{key}"
70
79
  end
71
80
 
72
81
  class ProductExistsConstraint
@@ -80,5 +89,19 @@ module Stall
80
89
  ProductCategory.exists?(slug: request.params[:id])
81
90
  end
82
91
  end
92
+
93
+ class CuratedProductListExistsConstraint
94
+ def matches?(request)
95
+ id = request.params[:curated_product_list_id] || request.params[:id]
96
+ CuratedProductList.exists?(slug: id)
97
+ end
98
+ end
99
+
100
+ class ManufacturerExistsConstraint
101
+ def matches?(request)
102
+ id = request.params[:manufacturer_id] || request.params[:id]
103
+ Manufacturer.exists?(slug: id)
104
+ end
105
+ end
83
106
  end
84
107
  end