stall 0.3.3 → 0.3.4

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