workarea-reviews 3.0.8

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 (166) 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 +16 -0
  9. data/.scss-lint.yml +188 -0
  10. data/.yardopts +1 -0
  11. data/CHANGELOG.md +756 -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 +107 -0
  17. data/Rakefile +54 -0
  18. data/app/assets/images/workarea/admin/icons/star.svg +1 -0
  19. data/app/assets/images/workarea/storefront/icons/empty_star.svg +1 -0
  20. data/app/assets/images/workarea/storefront/icons/half_star.svg +1 -0
  21. data/app/assets/images/workarea/storefront/icons/star.svg +1 -0
  22. data/app/assets/javascripts/workarea/storefront/reviews/modules/product_review_ajax_submit.js +39 -0
  23. data/app/assets/javascripts/workarea/storefront/reviews/modules/product_reviews_sort_menus.js +82 -0
  24. data/app/assets/javascripts/workarea/storefront/reviews/modules/rating_buttons.js +103 -0
  25. data/app/assets/javascripts/workarea/storefront/reviews/templates/sort_by_property.jst.ejs +11 -0
  26. data/app/assets/stylesheets/workarea/storefront/reviews/components/_rating.scss +28 -0
  27. data/app/assets/stylesheets/workarea/storefront/reviews/components/_reviews.scss +59 -0
  28. data/app/assets/stylesheets/workarea/storefront/reviews/components/_reviews_aggregate.scss +38 -0
  29. data/app/assets/stylesheets/workarea/storefront/reviews/components/_write_review.scss +63 -0
  30. data/app/controllers/workarea/admin/catalog_products_controller.decorator +9 -0
  31. data/app/controllers/workarea/admin/import_reviews_controller.rb +35 -0
  32. data/app/controllers/workarea/admin/reviews_controller.rb +51 -0
  33. data/app/controllers/workarea/storefront/review_requests_controller.rb +50 -0
  34. data/app/controllers/workarea/storefront/reviews_controller.rb +53 -0
  35. data/app/helpers/workarea/admin/reviews_helper.rb +12 -0
  36. data/app/helpers/workarea/storefront/reviews_helper.rb +24 -0
  37. data/app/mailers/workarea/admin/status_report_mailer.decorator +8 -0
  38. data/app/mailers/workarea/storefront/review_mailer.rb +23 -0
  39. data/app/models/workarea/catalog/product.decorator +15 -0
  40. data/app/models/workarea/fulfillment.decorator +11 -0
  41. data/app/models/workarea/review/request.rb +62 -0
  42. data/app/models/workarea/review.rb +135 -0
  43. data/app/models/workarea/search/admin/review.rb +72 -0
  44. data/app/models/workarea/search/storefront/product.decorator +9 -0
  45. data/app/models/workarea/sort.decorator +17 -0
  46. data/app/queries/workarea/review_request_params.rb +47 -0
  47. data/app/queries/workarea/review_summary.rb +11 -0
  48. data/app/queries/workarea/search/admin_reviews.rb +40 -0
  49. data/app/queries/workarea/search/category_browse.decorator +9 -0
  50. data/app/queries/workarea/search/product_search.decorator +9 -0
  51. data/app/seeds/reviews.rb +38 -0
  52. data/app/services/workarea/create_review.rb +99 -0
  53. data/app/view_models/workarea/admin/product_view_model.decorator +7 -0
  54. data/app/view_models/workarea/admin/review_view_model.rb +23 -0
  55. data/app/view_models/workarea/admin/reviews_search_view_model.rb +46 -0
  56. data/app/view_models/workarea/storefront/product_view_model.decorator +13 -0
  57. data/app/view_models/workarea/storefront/review_view_model.rb +14 -0
  58. data/app/views/workarea/admin/activities/_review_create.html.haml +12 -0
  59. data/app/views/workarea/admin/activities/_review_destroy.html.haml +10 -0
  60. data/app/views/workarea/admin/activities/_review_update.html.haml +10 -0
  61. data/app/views/workarea/admin/reviews/_aux_navigation.html.haml +5 -0
  62. data/app/views/workarea/admin/reviews/_catalog_products_aux_link.html.haml +5 -0
  63. data/app/views/workarea/admin/reviews/_dashboard_navigation.html.haml +1 -0
  64. data/app/views/workarea/admin/reviews/_menu.html.haml +3 -0
  65. data/app/views/workarea/admin/reviews/_summary.html.haml +12 -0
  66. data/app/views/workarea/admin/reviews/edit.html.haml +77 -0
  67. data/app/views/workarea/admin/reviews/index.html.haml +74 -0
  68. data/app/views/workarea/admin/status_report_mailer/_reviews.html.haml +5 -0
  69. data/app/views/workarea/storefront/products/_rating.html.haml +10 -0
  70. data/app/views/workarea/storefront/products/_reviews.html.haml +23 -0
  71. data/app/views/workarea/storefront/products/_reviews_aggregate.html.haml +22 -0
  72. data/app/views/workarea/storefront/products/_reviews_summary.html.haml +2 -0
  73. data/app/views/workarea/storefront/review_mailer/review_request.html.haml +22 -0
  74. data/app/views/workarea/storefront/review_requests/show.html.haml +44 -0
  75. data/app/views/workarea/storefront/reviews/new.html.haml +68 -0
  76. data/app/views/workarea/storefront/style_guides/_reviews_product_summary_docs.html.haml +13 -0
  77. data/app/views/workarea/storefront/style_guides/components/_reviews.html.haml +17 -0
  78. data/app/workers/workarea/create_review_requests.rb +33 -0
  79. data/app/workers/workarea/schedule_review_requests.rb +44 -0
  80. data/app/workers/workarea/send_review_requests.rb +12 -0
  81. data/app/workers/workarea/update_product_review_data.rb +26 -0
  82. data/bin/rails +18 -0
  83. data/config/initializers/append_points.rb +70 -0
  84. data/config/initializers/assets.rb +1 -0
  85. data/config/initializers/configuration.rb +29 -0
  86. data/config/initializers/rack_attack.rb +11 -0
  87. data/config/initializers/recaptcha.rb +19 -0
  88. data/config/initializers/scheduled_jobs.rb +6 -0
  89. data/config/locales/en.yml +104 -0
  90. data/config/routes.rb +21 -0
  91. data/lib/tasks/verify.rake +16 -0
  92. data/lib/workarea/mailer_previews/storefront/review_mailer_preview.rb +10 -0
  93. data/lib/workarea/reviews/engine.rb +12 -0
  94. data/lib/workarea/reviews/version.rb +5 -0
  95. data/lib/workarea/reviews.rb +13 -0
  96. data/test/dummy/Rakefile +6 -0
  97. data/test/dummy/app/assets/config/manifest.js +4 -0
  98. data/test/dummy/app/assets/images/.keep +0 -0
  99. data/test/dummy/app/assets/javascripts/application.js +13 -0
  100. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  101. data/test/dummy/app/controllers/application_controller.rb +3 -0
  102. data/test/dummy/app/controllers/concerns/.keep +0 -0
  103. data/test/dummy/app/helpers/application_helper.rb +2 -0
  104. data/test/dummy/app/jobs/application_job.rb +2 -0
  105. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  106. data/test/dummy/app/models/concerns/.keep +0 -0
  107. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  108. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  109. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  110. data/test/dummy/bin/bundle +3 -0
  111. data/test/dummy/bin/rails +4 -0
  112. data/test/dummy/bin/rake +4 -0
  113. data/test/dummy/bin/setup +34 -0
  114. data/test/dummy/bin/update +29 -0
  115. data/test/dummy/config/application.rb +25 -0
  116. data/test/dummy/config/boot.rb +5 -0
  117. data/test/dummy/config/cable.yml +9 -0
  118. data/test/dummy/config/environment.rb +5 -0
  119. data/test/dummy/config/environments/development.rb +55 -0
  120. data/test/dummy/config/environments/production.rb +86 -0
  121. data/test/dummy/config/environments/test.rb +43 -0
  122. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  123. data/test/dummy/config/initializers/assets.rb +11 -0
  124. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  125. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  126. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  127. data/test/dummy/config/initializers/inflections.rb +16 -0
  128. data/test/dummy/config/initializers/mime_types.rb +4 -0
  129. data/test/dummy/config/initializers/new_framework_defaults.rb +21 -0
  130. data/test/dummy/config/initializers/session_store.rb +3 -0
  131. data/test/dummy/config/initializers/workarea.rb +5 -0
  132. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  133. data/test/dummy/config/locales/en.yml +23 -0
  134. data/test/dummy/config/puma.rb +47 -0
  135. data/test/dummy/config/routes.rb +5 -0
  136. data/test/dummy/config/secrets.yml +22 -0
  137. data/test/dummy/config/spring.rb +6 -0
  138. data/test/dummy/config.ru +5 -0
  139. data/test/dummy/db/seeds.rb +2 -0
  140. data/test/dummy/lib/assets/.keep +0 -0
  141. data/test/dummy/log/.keep +0 -0
  142. data/test/factories/reviews.rb +32 -0
  143. data/test/fixtures/reviews.csv +5 -0
  144. data/test/helpers/workarea/admin/reviews_helper_test.rb +17 -0
  145. data/test/helpers/workarea/storefront/reviews_helper_test.rb +19 -0
  146. data/test/integration/workarea/admin/reviews_integration_test.rb +33 -0
  147. data/test/integration/workarea/storefront/review_requests_integration_test.rb +63 -0
  148. data/test/integration/workarea/storefront/reviews_integration_test.rb +82 -0
  149. data/test/javascripts/fixtures/product_review_ajax_submit.html.haml +4 -0
  150. data/test/javascripts/product_review_ajax_submit_spec.js +28 -0
  151. data/test/javascripts/spec_helper.js +23 -0
  152. data/test/models/workarea/review/request_test.rb +18 -0
  153. data/test/models/workarea/review_test.rb +94 -0
  154. data/test/models/workarea/search/storefront/product_test.decorator +14 -0
  155. data/test/queries/workarea/review_request_params_test.rb +64 -0
  156. data/test/services/workarea/create_review_test.rb +131 -0
  157. data/test/system/workarea/admin/review_system_test.rb +96 -0
  158. data/test/system/workarea/storefront/review_request_system_test.rb +34 -0
  159. data/test/system/workarea/storefront/review_system_test.rb +113 -0
  160. data/test/teaspoon_env.rb +10 -0
  161. data/test/test_helper.rb +11 -0
  162. data/test/workers/workarea/create_review_requests_test.rb +38 -0
  163. data/test/workers/workarea/schedule_review_requests_test.rb +115 -0
  164. data/test/workers/workarea/send_review_requests_test.rb +30 -0
  165. data/workarea-reviews.gemspec +22 -0
  166. metadata +241 -0
@@ -0,0 +1,9 @@
1
+ module Workarea
2
+ decorate Admin::CatalogProductsController, with: :reviews do
3
+ decorated { helper Admin::ReviewsHelper }
4
+
5
+ def reviews
6
+ @reviews = Review.find_for_product(@product.id, true)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ module Workarea
2
+ module Admin
3
+ class ImportReviewsController < Admin::ApplicationController
4
+ required_permissions :marketing
5
+
6
+ def new
7
+ @import = Import::Review.new
8
+ end
9
+
10
+ def create
11
+ @import = Import::Review.new(import_params)
12
+
13
+ if @import.save
14
+ ProcessImport.perform_async(@import.to_global_id)
15
+
16
+ flash[:success] = t('workarea.admin.import_reviews.flash_messages.processing')
17
+ redirect_to reviews_path
18
+ else
19
+ render :new, status: :unprocessable_entity
20
+ end
21
+ end
22
+
23
+ def sample
24
+ send_file Reviews::Engine.root.join('public/workarea/import_samples/reviews.csv')
25
+ end
26
+
27
+ private
28
+
29
+ def import_params
30
+ params.fetch(:import_review, {})
31
+ .merge(created_by_id: current_user.id)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ module Workarea
2
+ class Admin::ReviewsController < Admin::ApplicationController
3
+ required_permissions :marketing
4
+ before_action :find_review, except: :index
5
+
6
+ def index
7
+ search = Search::AdminReviews.new(params.merge(autocomplete: request.xhr?))
8
+ @search = Admin::ReviewsSearchViewModel.new(search, view_model_options)
9
+ end
10
+
11
+ def show
12
+ redirect_to edit_review_path(@review)
13
+ end
14
+
15
+ def edit; end
16
+
17
+ def update
18
+ if @review.update_attributes(review_params)
19
+ flash[:success] = t('workarea.admin.reviews.flash_messages.updated')
20
+ redirect_to reviews_path
21
+ else
22
+ render :edit
23
+ end
24
+ end
25
+
26
+ def destroy
27
+ @review.destroy
28
+ flash[:success] = t('workarea.admin.reviews.flash_messages.destroyed')
29
+ if params[:product].present?
30
+ redirect_to reviews_catalog_product_path(params[:product])
31
+ else
32
+ redirect_to reviews_path
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def find_review
39
+ if params[:id].present?
40
+ review = Review.find(params[:id])
41
+ @review = Admin::ReviewViewModel.wrap(review)
42
+ @user = User.find(review.user_id) rescue nil
43
+ end
44
+ end
45
+
46
+ def review_params
47
+ return {} if params[:review].blank?
48
+ params[:review].permit(:body, :approved, :rating, :title)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ReviewRequestsController < Storefront::ApplicationController
4
+ before_action :validate_request
5
+ before_action :find_product, :set_review
6
+
7
+ def show; end
8
+
9
+ def complete
10
+ if verify_recaptcha(model: @review, env: Rails.env) && @review.save
11
+ @request.complete!
12
+ Review::Request.cancel_for_orders!(@request.order_id)
13
+
14
+ flash[:success] = t('workarea.storefront.reviews.flash_messages.created')
15
+ redirect_to product_path(@product)
16
+ else
17
+ flash[:error] = t('workarea.storefront.reviews.flash_messages.failure')
18
+ render :show
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def validate_request
25
+ @request = Review::Request.find_by_token(params[:id])
26
+
27
+ if @request.nil? || @request.completed?
28
+ flash[:error] = t('workarea.storefront.review_requests.flash_messages.already_submitted')
29
+ redirect_to(root_path) && (return)
30
+ end
31
+ end
32
+
33
+ def find_product
34
+ model = Catalog::Product.find(@request.product_id)
35
+ @product = ProductViewModel.wrap(model, view_model_options)
36
+ end
37
+
38
+ def set_review
39
+ @review = ReviewViewModel.wrap(Review.new(review_params))
40
+ end
41
+
42
+ def review_params
43
+ ReviewRequestParams.new(
44
+ @request,
45
+ params[:review]&.permit(:rating, :title, :body)&.to_h || {}
46
+ ).to_h
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ReviewsController < Storefront::ApplicationController
4
+ before_action :find_product
5
+ before_action :set_review
6
+
7
+ def new; end
8
+
9
+ def create
10
+ if review_can_be_saved? && @create_review.save
11
+ redirect_to product_path(params[:product_id])
12
+ flash[:success] = t('workarea.storefront.reviews.flash_messages.created')
13
+ else
14
+ render :new
15
+ flash[:error] = t('workarea.storefront.reviews.flash_messages.failure')
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def set_review
22
+ @create_review = CreateReview.for(
23
+ product: @product,
24
+ user: current_user,
25
+ params: review_params.to_h
26
+ )
27
+
28
+ @review = ReviewViewModel.wrap(@create_review.review)
29
+ end
30
+
31
+ def find_product
32
+ @product ||=
33
+ begin
34
+ model = Catalog::Product.find_by(slug: params[:product_id])
35
+ raise InvalidDisplay unless model.active? || current_user.try(:admin?)
36
+ ProductViewModel.wrap(model)
37
+ end
38
+ end
39
+
40
+ def review_params
41
+ return {} unless params[:review].present?
42
+
43
+ params[:review].permit(
44
+ :product_id, :rating, :title, :body, :first_name, :last_name, :email
45
+ )
46
+ end
47
+
48
+ def review_can_be_saved?
49
+ verify_recaptcha(model: @create_review, env: Rails.env)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,12 @@
1
+ module Workarea
2
+ module Admin
3
+ module ReviewsHelper
4
+ def reviewer_info(model)
5
+ return model.user_info unless model.user_id.present?
6
+
7
+ link_to model.user_info,
8
+ edit_user_path(model.user_id)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Workarea
2
+ module Storefront
3
+ module ReviewsHelper
4
+ def rating_stars(rating, options = {})
5
+ full_star_count = rating.floor
6
+ empty_star_count = 5 - rating.ceil
7
+ half_star_size = (rating % 1).round(2) * 100
8
+ half_star_width = 20 + (half_star_size - 0) * (80.0 - 20) / (100.0 - 0)
9
+ itemprop = options[:aggregate] ? 'aggregateRating' : 'reviewRating'
10
+ itemtype = options[:aggregate] ? 'http://schema.org/AggregateRating' : 'http://schema.org/Rating'
11
+
12
+ render 'workarea/storefront/products/rating', rating: rating, full_star_count: full_star_count, empty_star_count: empty_star_count, half_star_width: half_star_width, half_star_size: half_star_size, itemprop: itemprop, itemtype: itemtype
13
+ end
14
+
15
+ def display_purchase_requirement_message
16
+ if current_user && Workarea.config.require_purchase_to_post_review && current_user.total_spent.to_f.zero?
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ module Workarea
2
+ decorate Admin::StatusReportMailer, with: :reviews do
3
+ def report(email, date)
4
+ @reviews_summary = ReviewSummary.new
5
+ super
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ module Workarea
2
+ module Storefront
3
+ class ReviewMailer < Storefront::ApplicationMailer
4
+ include TransactionalMailer
5
+
6
+ def review_request(request_id)
7
+ @request = Review::Request.find(request_id)
8
+ @content = Content::Email.find_content('review_request')
9
+ @product = Storefront::ProductViewModel.wrap(
10
+ Catalog::Product.find(@request.product_id)
11
+ )
12
+
13
+ mail(
14
+ to: @request.email,
15
+ subject: t(
16
+ 'workarea.storefront.email.review_request.subject',
17
+ product: @product.name
18
+ )
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module Workarea
2
+ decorate Catalog::Product, with: :reviews do
3
+ decorated do
4
+ # @!attribute total_reviews
5
+ # @return [Integer] the total number of approved reviews
6
+ #
7
+ field :total_reviews, type: Integer, default: 0
8
+
9
+ # @!attribute average_rating
10
+ # @return [Float] the average rating of approved reviews
11
+ #
12
+ field :average_rating, type: Float
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ decorate Fulfillment, with: :reviews do
3
+ def ship_items(tracking_number, shipped_items)
4
+ super.tap do |result|
5
+ if result && Workarea.config.send_transactional_emails
6
+ ScheduleReviewRequests.perform_async(id, shipped_items)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,62 @@
1
+ module Workarea
2
+ class Review
3
+ class Request
4
+ include ApplicationDocument
5
+ include UrlToken
6
+
7
+ field :order_id, type: String
8
+ field :user_id, type: String
9
+ field :product_id, type: String
10
+
11
+ field :email, type: String
12
+
13
+ field :send_after, type: DateTime
14
+ field :sent_at, type: DateTime
15
+ field :completed_at, type: DateTime
16
+ field :canceled_at, type: DateTime
17
+
18
+ index(order_id: 1)
19
+ index(send_after: 1, sent_at: 1, canceled_at: 1)
20
+
21
+ index(
22
+ { created_at: 1 },
23
+ { expire_after_seconds: Workarea.config.review_request_ttl }
24
+ )
25
+
26
+ scope :by_order, ->(order_id) { where(order_id: order_id) }
27
+ scope :ready_to_send, ->(from = Time.current) do
28
+ where(:send_after.lte => from, :sent_at => nil, :canceled_at => nil)
29
+ end
30
+
31
+ def self.cancel_for_orders!(order_ids)
32
+ self.in(order_id: Array.wrap(order_ids))
33
+ .where(completed_at: nil)
34
+ .update_all(canceled_at: Time.current)
35
+ end
36
+
37
+ def scheduled?
38
+ send_after.present?
39
+ end
40
+
41
+ def sent?
42
+ sent_at.present?
43
+ end
44
+
45
+ def completed?
46
+ completed_at.present? || canceled?
47
+ end
48
+
49
+ def canceled?
50
+ canceled_at.present?
51
+ end
52
+
53
+ def complete!
54
+ update_attributes(completed_at: Time.current)
55
+ end
56
+
57
+ def send!
58
+ update_attributes(sent_at: Time.current)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,135 @@
1
+ module Workarea
2
+ class Review
3
+ include ApplicationDocument
4
+
5
+ field :user_id, type: String
6
+ field :user_info, type: String
7
+ field :first_name, type: String
8
+ field :last_name, type: String
9
+ field :email, type: String
10
+ field :product_id, type: String
11
+ field :rating, type: Integer
12
+ field :title, type: String
13
+ field :body, type: String
14
+ field :approved, type: Boolean, default: false
15
+ field :verified, type: Boolean, default: false
16
+
17
+ index(product_id: 1, approved: 1)
18
+ index(approved: 1)
19
+ index(created_at: 1)
20
+
21
+ validates :product_id, presence: true
22
+ validates :user_info, presence: true
23
+ validates :body, presence: true
24
+ validates :rating, presence: true,
25
+ numericality: {
26
+ greater_than_or_equal_to: 1,
27
+ less_than_or_equal_to: 5,
28
+ only_integer: true
29
+ }
30
+
31
+ validate :user_must_have_spent_money_to_create_review
32
+
33
+ before_save :ensure_title
34
+
35
+ scope :by_product, ->(id) { where(product_id: id) }
36
+ scope :approved, -> { where(approved: true) }
37
+ scope :pending, -> { where(approved: false) }
38
+
39
+ def self.sorts
40
+ [ Workarea::Sort.pending,
41
+ Workarea::Sort.newest,
42
+ Workarea::Sort.modified,
43
+ Workarea::Sort.title,
44
+ Workarea::Sort.rating ]
45
+ end
46
+
47
+ # Get {Mongoid::Criteria} for the reviews for a certain product.
48
+ # Optionally allow including unapproved reviews (for the admin).
49
+ #
50
+ # @param [String] product_id
51
+ # @param [optional, Boolean] allow_unapproved
52
+ # @return [Mongoid::Criteria]
53
+ #
54
+ def self.find_for_product(product_id, allow_unapproved = false)
55
+ criteria = by_product(product_id)
56
+ allow_unapproved ? criteria : criteria.approved
57
+ end
58
+
59
+ # Lookup aggregate stats on product reviews for a product.
60
+ #
61
+ # @param [String] product_id
62
+ # @return [Hash]
63
+ #
64
+ def self.find_single_aggregates(product_id)
65
+ stats = by_product(product_id).approved.aggregates(:rating)
66
+ { count: stats['count'] || 0, average: stats['avg'] || 0 }
67
+ end
68
+
69
+ # Lookup aggregate review stats on multiple products.
70
+ # Used for show stats on browse/detail.
71
+ #
72
+ # @param [Array<String>] product_ids
73
+ # @return [Hash] results Keys are product IDs, values are stats Hash
74
+ #
75
+ def self.find_aggregates(*product_ids)
76
+ product_ids = Array(product_ids).flatten
77
+
78
+ if product_ids.one?
79
+ find_single_aggregates(product_ids.first)
80
+ else
81
+ product_ids.inject({}) do |memo, product_id|
82
+ memo[product_id] = find_single_aggregates(product_id)
83
+ memo
84
+ end
85
+ end
86
+ end
87
+
88
+ # Find sort score for a product. Used in the search index for sorting by
89
+ # best reviewed.
90
+ #
91
+ # Balances a small number of ratings with the uncertainty of only having a few ratings.
92
+ # See: http://masanjin.net/blog/how-to-rank-products-based-on-user-input
93
+ #
94
+ # @param [String] product_id
95
+ # @return [Float]
96
+ #
97
+ def self.find_sorting_score(product_id)
98
+ reviews = find_for_product(product_id)
99
+
100
+ votes = [ reviews.select { |r| r.rating == 1 }.length,
101
+ reviews.select { |r| r.rating == 2 }.length,
102
+ reviews.select { |r| r.rating == 3 }.length,
103
+ reviews.select { |r| r.rating == 4 }.length,
104
+ reviews.select { |r| r.rating == 5 }.length ]
105
+
106
+ prior = [2, 2, 2, 2, 2]
107
+
108
+ posterior = votes.zip(prior).map { |a, b| a + b }
109
+ sum = posterior.inject { |a, b| a + b }
110
+
111
+ posterior.
112
+ map.with_index { |v, i| (i + 1) * v }.
113
+ inject { |a, b| a + b }.
114
+ to_f / sum
115
+ end
116
+
117
+ def anonymous?
118
+ !user_id.present?
119
+ end
120
+
121
+ def ensure_title
122
+ self.title = body.truncate(50) if title.blank?
123
+ end
124
+
125
+ def user_must_have_spent_money_to_create_review
126
+ if Workarea.config.require_purchase_to_post_review
127
+ user = User.find(user_id)
128
+ if user.total_spent.to_f.zero?
129
+ err_msg = I18n.t('workarea.storefront.reviews.total_spent_validation')
130
+ errors.add(:user, err_msg)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,72 @@
1
+ module Workarea
2
+ module Search
3
+ class Admin
4
+ class Review < Search::Admin
5
+ def type
6
+ 'review'
7
+ end
8
+
9
+ def status
10
+ 'active'
11
+ end
12
+
13
+ def facets
14
+ super.merge(
15
+ state: state,
16
+ rating: model.rating,
17
+ verification: verification
18
+ )
19
+ end
20
+
21
+ def jump_to_text
22
+ nil
23
+ end
24
+
25
+ def name
26
+ nil
27
+ end
28
+
29
+ def jump_to_position
30
+ 6
31
+ end
32
+
33
+ def search_text
34
+ [model.product_id, model.user_id]
35
+ end
36
+
37
+ def review_text
38
+ [model.title, model.body, product&.name].compact
39
+ end
40
+
41
+ def state
42
+ if model.approved?
43
+ I18n.t('workarea.admin.reviews.approved')
44
+ else
45
+ I18n.t('workarea.admin.reviews.pending')
46
+ end
47
+ end
48
+
49
+ def verification
50
+ if model.verified?
51
+ I18n.t('workarea.admin.reviews.verified')
52
+ else
53
+ I18n.t('workarea.admin.reviews.unverified')
54
+ end
55
+ end
56
+
57
+ def as_document
58
+ super.merge(
59
+ review_text: review_text,
60
+ rating: model.rating
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def product
67
+ @product ||= Catalog::Product.find(model.product_id) rescue nil
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ module Workarea
2
+ decorate Search::Storefront::Product, with: :reviews do
3
+ def sorts
4
+ super.merge(
5
+ rating: Review.find_sorting_score(model.id)
6
+ )
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Workarea
2
+ decorate Sort, with: :reviews do
3
+ class_methods do
4
+ def top_rated
5
+ new(I18n.t('workarea.sorts.top_rated'), :top_rated, :'sorts.rating', :desc)
6
+ end
7
+
8
+ def highest_rating
9
+ new(I18n.t('workarea.sorts.highest_rating'), :highest_rating, :rating, :desc)
10
+ end
11
+
12
+ def lowest_rating
13
+ new(I18n.t('workarea.sorts.lowest_rating'), :lowest_rating, :rating, :asc)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ module Workarea
2
+ class ReviewRequestParams
3
+ attr_reader :request, :params
4
+
5
+ def initialize(request, params)
6
+ @request = request
7
+ @params = params
8
+ end
9
+
10
+ def to_h
11
+ params.merge(
12
+ product_id: @request.product_id,
13
+ user_id: @request.user_id,
14
+ email: @request.email,
15
+ user_info: user_info,
16
+ first_name: first_name,
17
+ last_name: last_name,
18
+ verified: true
19
+ )
20
+ end
21
+
22
+ private
23
+
24
+ def user_info
25
+ return user.public_info if user&.public_info.present?
26
+
27
+ [first_name.first, last_name.first].join
28
+ end
29
+
30
+ def first_name
31
+ user&.first_name || billing_address.first_name
32
+ end
33
+
34
+ def last_name
35
+ user&.last_name || billing_address.last_name
36
+ end
37
+
38
+ def user
39
+ return nil unless @request.user_id.present?
40
+ @user ||= User.find(@request.user_id)
41
+ end
42
+
43
+ def billing_address
44
+ @billing_address ||= Payment.find(@request.order_id).address
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ class ReviewSummary
3
+ def pending_reviews_count
4
+ @pending_reviews_count ||= Review.pending.count
5
+ end
6
+
7
+ def recent_reviews_count
8
+ @recent_reviews_count ||= Review.where(:created_at.gte => 1.day.ago).count
9
+ end
10
+ end
11
+ end