workarea-wish_lists 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.eslintignore +2 -0
  4. data/.eslintrc +24 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. data/.gitignore +15 -0
  9. data/.scss-lint.yml +188 -0
  10. data/.yardopts +1 -0
  11. data/CHANGELOG.md +610 -0
  12. data/CODE_OF_CONDUCT.md +3 -0
  13. data/CONTRIBUTING.md +3 -0
  14. data/Gemfile +6 -0
  15. data/LICENSE +52 -0
  16. data/README.md +58 -0
  17. data/Rakefile +53 -0
  18. data/app/assets/javascripts/workarea/storefront/wish_lists/modules/wish_list_button.js +35 -0
  19. data/app/assets/javascripts/workarea/storefront/wish_lists/modules/wish_list_public_quantity_fields.js +42 -0
  20. data/app/assets/javascripts/workarea/storefront/wish_lists/templates/hidden_input.jst.ejs +1 -0
  21. data/app/assets/stylesheets/workarea/storefront/wish_lists/components/_wish_list_button.scss +6 -0
  22. data/app/assets/stylesheets/workarea/storefront/wish_lists/components/_wish_lists.scss +24 -0
  23. data/app/controllers/workarea/admin/users_controller.decorator +20 -0
  24. data/app/controllers/workarea/storefront/users/wish_lists_controller.rb +128 -0
  25. data/app/controllers/workarea/storefront/wish_lists_controller.rb +22 -0
  26. data/app/models/workarea/wish_list.rb +212 -0
  27. data/app/models/workarea/wish_list/item.rb +76 -0
  28. data/app/models/workarea/wish_list/pricing.rb +14 -0
  29. data/app/models/workarea/wish_list/pricing/calculators/totals_calculator.rb +26 -0
  30. data/app/models/workarea/wish_list/public_search.rb +34 -0
  31. data/app/models/workarea/wish_list/request.rb +36 -0
  32. data/app/seeds/workarea/wish_list_seeds.rb +24 -0
  33. data/app/services/workarea/set_wish_list_details.rb +49 -0
  34. data/app/services/workarea/wish_list_session.rb +53 -0
  35. data/app/view_models/workarea/admin/user_view_model.decorator +12 -0
  36. data/app/view_models/workarea/storefront/user_view_model.decorator +31 -0
  37. data/app/view_models/workarea/storefront/wish_list_item_view_model.rb +35 -0
  38. data/app/view_models/workarea/storefront/wish_list_view_model.rb +75 -0
  39. data/app/views/layouts/workarea/storefront/_wish_lists_link.html.haml +6 -0
  40. data/app/views/workarea/admin/users/_wish_list_card.html.haml +24 -0
  41. data/app/views/workarea/admin/users/_wish_list_item_summary.html.haml +8 -0
  42. data/app/views/workarea/admin/users/wish_list.html.haml +26 -0
  43. data/app/views/workarea/storefront/carts/_move_to_wish_list.html.haml +4 -0
  44. data/app/views/workarea/storefront/products/_add_to_wish_list.html.haml +4 -0
  45. data/app/views/workarea/storefront/style_guides/_wish_lists_product_list_docs.html.haml +158 -0
  46. data/app/views/workarea/storefront/users/accounts/_wish_list_summary.html.haml +15 -0
  47. data/app/views/workarea/storefront/users/wish_lists/show.html.haml +125 -0
  48. data/app/views/workarea/storefront/wish_lists/_wish_lists_link.html.haml +4 -0
  49. data/app/views/workarea/storefront/wish_lists/index.html.haml +32 -0
  50. data/app/views/workarea/storefront/wish_lists/show.html.haml +97 -0
  51. data/app/workers/workarea/mark_wish_list_items_purchased.rb +18 -0
  52. data/app/workers/workarea/update_wish_list_details.rb +19 -0
  53. data/bin/rails +18 -0
  54. data/config/initializers/append_points.rb +46 -0
  55. data/config/initializers/configuration.rb +10 -0
  56. data/config/locales/en.yml +60 -0
  57. data/config/routes.rb +23 -0
  58. data/lib/workarea/testing/factories/wish_lists.rb +22 -0
  59. data/lib/workarea/wish_lists.rb +13 -0
  60. data/lib/workarea/wish_lists/engine.rb +15 -0
  61. data/lib/workarea/wish_lists/version.rb +5 -0
  62. data/test/dummy/Rakefile +6 -0
  63. data/test/dummy/app/assets/config/manifest.js +4 -0
  64. data/test/dummy/app/assets/images/.keep +0 -0
  65. data/test/dummy/app/assets/javascripts/application.js +13 -0
  66. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  67. data/test/dummy/app/controllers/application_controller.rb +3 -0
  68. data/test/dummy/app/controllers/concerns/.keep +0 -0
  69. data/test/dummy/app/helpers/application_helper.rb +2 -0
  70. data/test/dummy/app/jobs/application_job.rb +2 -0
  71. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  72. data/test/dummy/app/models/concerns/.keep +0 -0
  73. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  74. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  75. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  76. data/test/dummy/bin/bundle +3 -0
  77. data/test/dummy/bin/rails +4 -0
  78. data/test/dummy/bin/rake +4 -0
  79. data/test/dummy/bin/setup +34 -0
  80. data/test/dummy/bin/update +29 -0
  81. data/test/dummy/config.ru +5 -0
  82. data/test/dummy/config/application.rb +24 -0
  83. data/test/dummy/config/boot.rb +5 -0
  84. data/test/dummy/config/cable.yml +9 -0
  85. data/test/dummy/config/environment.rb +5 -0
  86. data/test/dummy/config/environments/development.rb +54 -0
  87. data/test/dummy/config/environments/production.rb +86 -0
  88. data/test/dummy/config/environments/test.rb +43 -0
  89. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  90. data/test/dummy/config/initializers/assets.rb +11 -0
  91. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  92. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  93. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  94. data/test/dummy/config/initializers/inflections.rb +16 -0
  95. data/test/dummy/config/initializers/mime_types.rb +4 -0
  96. data/test/dummy/config/initializers/new_framework_defaults.rb +21 -0
  97. data/test/dummy/config/initializers/session_store.rb +3 -0
  98. data/test/dummy/config/initializers/workarea.rb +5 -0
  99. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  100. data/test/dummy/config/locales/en.yml +23 -0
  101. data/test/dummy/config/puma.rb +47 -0
  102. data/test/dummy/config/routes.rb +5 -0
  103. data/test/dummy/config/secrets.yml +22 -0
  104. data/test/dummy/config/spring.rb +6 -0
  105. data/test/dummy/db/seeds.rb +3 -0
  106. data/test/dummy/lib/assets/.keep +0 -0
  107. data/test/dummy/log/.keep +0 -0
  108. data/test/dummy/public/404.html +67 -0
  109. data/test/dummy/public/422.html +67 -0
  110. data/test/dummy/public/500.html +66 -0
  111. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  112. data/test/dummy/public/apple-touch-icon.png +0 -0
  113. data/test/dummy/public/favicon.ico +0 -0
  114. data/test/integration/workarea/storefront/wish_lists_integration_test.rb +211 -0
  115. data/test/models/workarea/wish_list/public_search_test.rb +82 -0
  116. data/test/models/workarea/wish_list_test.rb +158 -0
  117. data/test/services/workarea/set_wish_list_details_test.rb +62 -0
  118. data/test/services/workarea/wish_list_session_test.rb +47 -0
  119. data/test/system/workarea/admin/wish_lists_system_test.rb +46 -0
  120. data/test/system/workarea/storefront/wish_lists_system_test.rb +287 -0
  121. data/test/test_helper.rb +10 -0
  122. data/test/view_models/workarea/storefront/user_view_model_test.rb +46 -0
  123. data/test/view_models/workarea/storefront/wish_list_item_view_model_test.rb +47 -0
  124. data/test/view_models/workarea/storefront/wish_list_view_model_test.rb +74 -0
  125. data/test/workers/workarea/mark_wish_list_items_purchased_test.rb +21 -0
  126. data/workarea-wish_lists.gemspec +21 -0
  127. metadata +188 -0
@@ -0,0 +1,22 @@
1
+ module Workarea
2
+ module Storefront
3
+ class WishListsController < Storefront::ApplicationController
4
+ def index
5
+ if params[:wish_list_query].present?
6
+ @wish_lists = WishList
7
+ .search(params[:wish_list_query], params[:location])
8
+ .page(params[:page] || 1)
9
+ end
10
+ end
11
+
12
+ def show
13
+ wish_list = WishList.find_by_token(params[:id])
14
+ WishList::Pricing.perform(wish_list)
15
+ @wish_list = WishListViewModel.new(
16
+ wish_list,
17
+ view_model_options
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,212 @@
1
+ module Workarea
2
+ class WishList
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+ include UrlToken
6
+
7
+ class InvalidToken < StandardError; end
8
+
9
+ field :user_id, type: String
10
+ field :email, type: String
11
+ field :first_name, type: String
12
+ field :last_name, type: String
13
+ field :name, type: String
14
+ field :location, type: String
15
+ field :privacy, type: String, default: 'public'
16
+
17
+ index({ user_id: 1 }, { unique: true })
18
+ index(first_name: 1)
19
+ index(last_name: 1)
20
+ index(name: 1)
21
+ index(email: 1)
22
+
23
+ index(token: 1, privacy: 1)
24
+
25
+ embeds_many :items, class_name: 'Workarea::WishList::Item'
26
+
27
+ validates :privacy,
28
+ presence: true,
29
+ inclusion: { in: %w(public shared private) }
30
+
31
+ scope :named, -> { where(:name.exists => true, :name.ne => '') }
32
+
33
+ before_create :set_user_details
34
+
35
+ # Gets a wish list for a user.
36
+ #
37
+ # @param [String] user_id
38
+ #
39
+ # @return [WishList]
40
+ #
41
+ def self.for_user(user_id)
42
+ existing = Workarea::WishList.where(user_id: user_id).first
43
+ existing || Workarea::WishList.create!(user_id: user_id)
44
+ end
45
+
46
+ # Find wish lists for public viewing.
47
+ # Only returns wish lists with public privacy setting.
48
+ #
49
+ # @param [String] query
50
+ # name or email
51
+ # @param [String] location
52
+ # city, state, postal code
53
+ #
54
+ # @return [Array]
55
+ #
56
+ def self.search(query, location = '')
57
+ Workarea::WishList::PublicSearch.new(
58
+ query,
59
+ location
60
+ ).results
61
+ end
62
+
63
+ # Find a wish list by the share token
64
+ #
65
+ # @param [String] token
66
+ #
67
+ # @return [WishList]
68
+ #
69
+ def self.find_by_token(token)
70
+ raise(InvalidToken, token) if token.blank?
71
+
72
+ Workarea::WishList.where(
73
+ token: token,
74
+ :privacy.ne => 'private'
75
+ ).first || raise(InvalidToken, token)
76
+ end
77
+
78
+ # Find a list of product ids in a user's wish list
79
+ #
80
+ # @param [String, BSON::ObjectId] user_id
81
+ #
82
+ # @return [Array]
83
+ #
84
+ def self.find_products(user_id)
85
+ wish_list = Workarea::WishList.where(user_id: user_id).first
86
+ wish_list ? wish_list.items.map(&:product_id) : []
87
+ end
88
+
89
+ %w(public shared private).each do |privacy_level|
90
+ define_method "#{privacy_level}?" do
91
+ privacy == privacy_level
92
+ end
93
+ end
94
+
95
+ # How many valid units are in the wish list
96
+ #
97
+ # @return [Integer]
98
+ #
99
+ def quantity
100
+ items.select(&:valid?).sum(&:quantity)
101
+ end
102
+
103
+ # Get unpurchased items
104
+ #
105
+ # @return [Array]
106
+ #
107
+ def unpurchased_items
108
+ items.reject(&:purchased?)
109
+ end
110
+
111
+ # Get items from this list that have been purchased
112
+ #
113
+ # @return [Array]
114
+ #
115
+ def purchased_items
116
+ items.select(&:purchased?)
117
+ end
118
+
119
+ # Add a wish list item
120
+ #
121
+ # @param [String] product_id
122
+ # @param [String] sku
123
+ # @param [Integer] quantity
124
+ #
125
+ # @return [Boolean]
126
+ # whether the item was saved
127
+ #
128
+ def add_item(product_id, sku, quantity = 1, details = {}, customizations = {})
129
+ if existing_item = items.where(sku: sku, customizations: customizations).first
130
+ change_quantity(sku, existing_item.quantity + quantity)
131
+ else
132
+ items.build(
133
+ product_id: product_id,
134
+ sku: sku,
135
+ quantity: quantity,
136
+ details: details,
137
+ customizations: customizations
138
+ )
139
+ end
140
+
141
+ save
142
+ end
143
+
144
+ # Update a wish list item's quantity
145
+ #
146
+ # @param [String] item_id
147
+ # @param [Integer] quantity
148
+ #
149
+ # @return [Boolean]
150
+ # whether the item was saved
151
+ #
152
+
153
+ def update_item_quantity(item_id, quantity)
154
+ item = items.where(id: item_id).first
155
+ return false if item.nil?
156
+ item.update_attributes(quantity: quantity)
157
+ end
158
+
159
+ # Mark the items as being purchased
160
+ #
161
+ # @param [Array] skus
162
+ #
163
+ # @return [self]
164
+ #
165
+ def mark_item_purchased(sku, quantity)
166
+ item = items.detect { |i| i.sku == sku }
167
+ if item
168
+ item.received = item.received + quantity
169
+ item.purchased = true
170
+ end
171
+ save!
172
+ end
173
+
174
+ # Remove a SKU from the wish list
175
+ #
176
+ # @param [String] sku
177
+ #
178
+ # @return [self]
179
+ #
180
+ def remove_item(sku)
181
+ items.where(sku: sku).first.delete
182
+ save
183
+ end
184
+
185
+ def price_adjustments
186
+ PriceAdjustmentSet.new(
187
+ (items.map(&:price_adjustments).flatten || [])
188
+ )
189
+ end
190
+
191
+ def reset_pricing!
192
+ items.each(&:reset_pricing!)
193
+ self
194
+ end
195
+
196
+ def segment_ids
197
+ []
198
+ end
199
+
200
+ private
201
+
202
+ def change_quantity(sku, quantity)
203
+ item = items.where(sku: sku).first
204
+ item.quantity = quantity
205
+ end
206
+
207
+ def set_user_details
208
+ return unless user_id.present?
209
+ SetWishListDetails.new(self).perform
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,76 @@
1
+ module Workarea
2
+ class WishList::Item
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps
5
+
6
+ field :product_id, type: String
7
+ field :quantity, type: Integer, default: 1
8
+ field :received, type: Integer, default: 0
9
+ field :sku, type: String
10
+ field :product_details, type: Hash, default: {}
11
+ field :sku_details, type: Hash, default: {}
12
+ field :customizations, type: Hash, default: {}
13
+ field :purchased, type: Boolean, default: false
14
+ field :discountable, type: Boolean, default: true
15
+ field :contributes_to_shipping, type: Boolean, default: true
16
+ field :product_attributes, type: Hash, default: {}
17
+ field :total_price, type: Money, default: 0
18
+
19
+ embedded_in :wish_list, inverse_of: :items
20
+
21
+ embeds_many :price_adjustments,
22
+ class_name: 'Workarea::PriceAdjustment',
23
+ extend: Workarea::PriceAdjustmentExtension
24
+
25
+ validates :product_id, presence: true
26
+ validates :sku, presence: true
27
+ validates :quantity, presence: true,
28
+ numericality: {
29
+ greater_than_or_equal_to: 1,
30
+ only_integer: true
31
+ }
32
+
33
+ def details=(details)
34
+ write_attributes(
35
+ details.slice(
36
+ :product_details,
37
+ :sku_details,
38
+ :product_attributes
39
+ )
40
+ )
41
+
42
+ self
43
+ end
44
+
45
+ # Adds a price adjustment to the item. Does not persist.
46
+ #
47
+ # @return [self]
48
+ #
49
+ def adjust_pricing(options = {})
50
+ price_adjustments.build(options)
51
+ end
52
+
53
+ # Clears out all pricing for this item.
54
+ #
55
+ def reset_pricing!
56
+ price_adjustments.delete_all
57
+ end
58
+
59
+ # The base price per-unit for this item.
60
+ #
61
+ # @return [Money]
62
+ #
63
+ def unit_price
64
+ price_adjustments.first.unit.to_m
65
+ end
66
+
67
+ # Whether this item is on sale (as of the last time the
68
+ # order was priced).
69
+ #
70
+ # @return [Boolean]
71
+ #
72
+ def on_sale?
73
+ !!price_adjustments.first.data['on_sale']
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,14 @@
1
+ module Workarea
2
+ module WishList::Pricing
3
+ # Build price adjustments and set order total prices.
4
+ #
5
+ # @param [Workarea::WishList] wish_list
6
+ # @return [self]
7
+ #
8
+ def self.perform(wish_list)
9
+ wish_list.reset_pricing!
10
+ WishList::Request.new(wish_list).run
11
+ self
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ module Workarea
2
+ class WishList::Pricing::Calculators::TotalsCalculator
3
+ attr_reader :wish_list, :request
4
+
5
+ def initialize(wish_list, request = nil)
6
+ @wish_list = wish_list
7
+ @request = request
8
+ end
9
+
10
+ def adjust
11
+ set_item_totals
12
+ end
13
+
14
+ private
15
+
16
+ def price_adjustments
17
+ @price_adjustments ||= @wish_list.price_adjustments # Memoize for perf
18
+ end
19
+
20
+ def set_item_totals
21
+ @wish_list.items.each do |item|
22
+ item.total_price = item.price_adjustments.adjusting('item').sum
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Workarea
2
+ class WishList::PublicSearch
3
+ attr_reader :query, :location
4
+
5
+ def initialize(query, location = '')
6
+ @query = query
7
+ @location = location
8
+ end
9
+
10
+ def results
11
+ regex = /^#{Regexp.quote(query)}/i
12
+ criteria = [
13
+ { name: regex },
14
+ { first_name: regex },
15
+ { last_name: regex },
16
+ { email: query } # we don't want partial matching on email
17
+ ]
18
+
19
+ if location.present?
20
+ loc_regex = /.*#{Regexp.quote(location)}.*/i
21
+ Workarea::WishList.where(
22
+ privacy: 'public',
23
+ location: loc_regex,
24
+ '$or' => criteria
25
+ )
26
+ else
27
+ Workarea::WishList.where(
28
+ privacy: 'public',
29
+ '$or' => criteria
30
+ )
31
+ end.named
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ module Workarea
2
+ module Pricing
3
+ class WishList::Request
4
+ delegate :items, to: :order
5
+
6
+ def initialize(wish_list)
7
+ @wish_list = wish_list
8
+ end
9
+
10
+ def pricing
11
+ @pricing ||= Pricing::Collection.new(all_skus)
12
+ end
13
+
14
+ def order
15
+ @wish_list
16
+ end
17
+
18
+ def run
19
+ Workarea.config.wish_list_pricing_calculators.each do |klass|
20
+ klass.constantize.new(self).adjust
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def all_skus
27
+ skus = [
28
+ @wish_list.items.map(&:sku),
29
+ @wish_list.items.map { |i| i.customizations['pricing_sku'] }
30
+ ]
31
+
32
+ skus.flatten.reject(&:blank?).uniq
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module Workarea
2
+ class WishListSeeds
3
+ def perform
4
+ puts 'Adding wish lists...'
5
+
6
+ 5.times do
7
+ user = Workarea::User.sample
8
+ wish_list = Workarea::WishList.for_user(user.id)
9
+ Workarea::Catalog::Product.limit(5).each do |product|
10
+ sku = product.skus.sample
11
+ quantity = rand(3) + 1
12
+
13
+ next unless sku
14
+ wish_list.add_item(product.id, sku, quantity)
15
+
16
+ if rand(2).zero?
17
+ purchased_quantity = rand(quantity) + 1
18
+ wish_list.mark_item_purchased(sku, purchased_quantity)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end