workarea-reviews 3.0.10 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc.json +35 -0
  3. data/.github/workflows/ci.yml +60 -0
  4. data/.gitignore +1 -2
  5. data/.rubocop.yml +3 -0
  6. data/.stylelintrc.json +8 -0
  7. data/CHANGELOG.md +5 -23
  8. data/Gemfile +4 -2
  9. data/Rakefile +4 -5
  10. data/app/controllers/workarea/admin/reports_controller.decorator +10 -0
  11. data/app/helpers/workarea/admin/reviews_helper.rb +6 -5
  12. data/app/helpers/workarea/storefront/reviews_helper.rb +1 -3
  13. data/app/helpers/workarea/storefront/reviews_schema_org_helper.rb +34 -0
  14. data/app/mailers/workarea/admin/status_report_mailer.decorator +2 -2
  15. data/app/models/workarea/insights/most_active_reviewers.rb +34 -0
  16. data/app/models/workarea/insights/most_reviewed_products.rb +33 -0
  17. data/app/models/workarea/insights/top_rated_products.rb +33 -0
  18. data/app/models/workarea/review.rb +5 -16
  19. data/app/queries/workarea/reports/reviews_by_product.rb +86 -0
  20. data/app/queries/workarea/reports/reviews_by_user.rb +71 -0
  21. data/app/seeds/{review_seeds.rb → workarea/review_seeds.rb} +0 -0
  22. data/app/view_models/workarea/admin/dashboards/reports_view_model.decorator +11 -0
  23. data/app/view_models/workarea/admin/reports/reviews_by_product_view_model.rb +27 -0
  24. data/app/views/workarea/admin/dashboards/_reviews_by_product_card.html.haml +17 -0
  25. data/app/views/workarea/admin/insights/_most_active_reviewers.html.haml +22 -0
  26. data/app/views/workarea/admin/insights/_most_reviewed_products.html.haml +22 -0
  27. data/app/views/workarea/admin/insights/_top_rated_products.html.haml +22 -0
  28. data/app/views/workarea/admin/reports/reviews_by_product.html.haml +44 -0
  29. data/app/views/workarea/storefront/products/_rating.html.haml +1 -2
  30. data/app/views/workarea/storefront/products/_reviews.html.haml +5 -5
  31. data/app/views/workarea/storefront/products/_reviews_aggregate.html.haml +2 -2
  32. data/app/views/workarea/storefront/review_mailer/review_request.html.haml +2 -0
  33. data/app/views/workarea/storefront/review_requests/show.html.haml +2 -1
  34. data/config/initializers/append_points.rb +5 -0
  35. data/config/initializers/configuration.rb +3 -1
  36. data/config/locales/en.yml +46 -0
  37. data/config/routes.rb +4 -0
  38. data/lib/workarea/reviews/engine.rb +4 -0
  39. data/lib/workarea/reviews/version.rb +1 -1
  40. data/package.json +9 -0
  41. data/test/dummy/config/initializers/session_store.rb +3 -1
  42. data/test/helpers/workarea/storefront/reviews_helper_test.rb +1 -0
  43. data/test/integration/workarea/admin/reviews_integration_test.rb +5 -1
  44. data/test/models/workarea/insights/most_active_reviewers_test.rb +32 -0
  45. data/test/models/workarea/insights/most_reviewed_products_test.rb +32 -0
  46. data/test/models/workarea/insights/top_rated_products_test.rb +49 -0
  47. data/test/models/workarea/review_test.rb +29 -18
  48. data/test/queries/workarea/reports/reviews_by_product_test.rb +104 -0
  49. data/test/queries/workarea/reports/reviews_by_user_test.rb +102 -0
  50. data/test/system/workarea/admin/reviews_by_product_system_test.rb +60 -0
  51. data/workarea-reviews.gemspec +1 -1
  52. data/yarn.lock +3265 -0
  53. metadata +31 -7
  54. data/app/controllers/workarea/admin/import_reviews_controller.rb +0 -35
  55. data/test/helpers/workarea/admin/reviews_helper_test.rb +0 -17
@@ -0,0 +1,71 @@
1
+ module Workarea
2
+ module Reports
3
+ class ReviewsByUser
4
+ include Report
5
+
6
+ self.reporting_class = Review
7
+ self.sort_fields = %w(reviews verified average_rating activity_score)
8
+
9
+ def aggregation
10
+ [filter_results, project_used_fields, group_by_email, project_fields]
11
+ end
12
+
13
+ def filter_results
14
+ {
15
+ '$match' => {
16
+ 'created_at' => { '$gte' => starts_at, '$lte' => ends_at },
17
+ **approval_query
18
+ }
19
+ }
20
+ end
21
+
22
+ def project_used_fields
23
+ {
24
+ '$project' => {
25
+ 'email' => 1,
26
+ 'user_id' => 1,
27
+ 'rating' => 1,
28
+ 'verified' => 1
29
+ }
30
+ }
31
+ end
32
+
33
+ def group_by_email
34
+ {
35
+ '$group' => {
36
+ '_id' => '$email',
37
+ 'user_id' => { '$max' => '$user_id' },
38
+ 'reviews' => { '$sum' => 1 },
39
+ 'verified' => { '$sum' => { '$cond' => { 'if' => '$verified', 'then' => 1, 'else' => 0 } } },
40
+ 'rating_tally' => { '$sum' => '$rating' }
41
+ }
42
+ }
43
+ end
44
+
45
+ def project_fields
46
+ {
47
+ '$project' => {
48
+ 'email' => 1,
49
+ 'user_id' => 1,
50
+ 'reviews' => 1,
51
+ 'verified' => 1,
52
+ 'average_rating' => { '$divide' => ['$rating_tally', '$reviews'] },
53
+ 'activity_score' => { '$sum' => ['$reviews', { '$multiply' => ['$verified', 0.75] }] }
54
+ }
55
+ }
56
+ end
57
+
58
+ private
59
+
60
+ def approval_query
61
+ if params[:results_filter] == 'approved'
62
+ { approved: true }
63
+ elsif params[:results_filter] == 'unapproved'
64
+ { approved: false }
65
+ else
66
+ {}
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,11 @@
1
+ module Workarea
2
+ decorate Admin::Dashboards::ReportsViewModel, with: :reviews do
3
+ def reviews_by_product
4
+ @reviews_by_product ||=
5
+ Admin::Reports::ReviewsByProductViewModel.wrap(
6
+ Workarea::Reports::ReviewsByProduct.new(options),
7
+ options
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ module Workarea
2
+ module Admin
3
+ module Reports
4
+ class ReviewsByProductViewModel < ApplicationViewModel
5
+ def results
6
+ @results ||= model.results.map do |result|
7
+ product = products.detect { |p| p.id == result['_id'] }
8
+ OpenStruct.new(
9
+ { product: product }
10
+ .merge(result)
11
+ .merge(
12
+ average_rating: result['average_rating'].round(2),
13
+ weighted_average_rating: result['weighted_average_rating'].round(2)
14
+ )
15
+ )
16
+ end
17
+ end
18
+
19
+ def products
20
+ @products ||= Catalog::Product.any_in(
21
+ id: model.results.map { |r| r['_id'] }
22
+ ).to_a
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ .grid__cell
2
+ .card{ class: card_classes(:reviews_by_product_report, local_assigns[:active]) }
3
+ = link_to reviews_by_product_report_path, class: 'card__header' do
4
+ %span.card__header-text= t('workarea.admin.reports.reviews_by_product.title')
5
+ = inline_svg 'workarea/admin/icons/insights.svg', class: 'card__icon'
6
+
7
+ .card__body
8
+ .card__centered-content
9
+ %table
10
+ %tbody
11
+ - @dashboard.reviews_by_product.results.take(4).each do |result|
12
+ %tr
13
+ %td= result.product.present? ? result.product.name : result._id
14
+ %td.align-right= number_with_delimiter result.reviews
15
+
16
+ = link_to reviews_by_product_report_path, class: 'card__button' do
17
+ %span.button.button--small= t('workarea.admin.dashboards.reports.view_full_report')
@@ -0,0 +1,22 @@
1
+ .insight
2
+ .insight__date
3
+ %span.insight__period= insight.reporting_on.strftime('%B %Y')
4
+ .insight__heading= t('workarea.admin.insights.most_active_reviewers.title')
5
+ .insight__body
6
+ %p.insight__note= t('workarea.admin.insights.most_active_reviewers.info')
7
+ %table
8
+ %thead
9
+ %tr
10
+ %th.align-center= t('workarea.admin.insights.most_active_reviewers.user')
11
+ %th.align-center= t('workarea.admin.insights.most_active_reviewers.reviews')
12
+ %th.align-center= t('workarea.admin.insights.most_active_reviewers.verified')
13
+ %tbody
14
+ - insight.results.each do |result|
15
+ %tr
16
+ %td.align-center
17
+ - if result.user.present?
18
+ = link_to result.user.name, user_path(result.user)
19
+ - else
20
+ = result.email
21
+ %td.align-center= number_with_delimiter result.reviews
22
+ %td.align-center= number_with_delimiter result.verified
@@ -0,0 +1,22 @@
1
+ .insight
2
+ .insight__date
3
+ %span.insight__period= insight.reporting_on.strftime('%B %Y')
4
+ .insight__heading= t('workarea.admin.insights.most_reviewed_products.title')
5
+ .insight__body
6
+ %p.insight__note= t('workarea.admin.insights.most_reviewed_products.info')
7
+ .grid.grid--large.grid--center
8
+ - insight.results.each do |result|
9
+ .grid__cell.grid__cell--50.grid__cell--25-at-wide.align-center
10
+ .insight__product
11
+ - if result.product.blank?
12
+ = image_tag(product_image_url(Workarea::Catalog::ProductPlaceholderImage.cached, :small), alt: result.product_id, class: 'insight__product-image')
13
+ - else
14
+ = link_to catalog_product_path(result.product) do
15
+ = image_tag(product_image_url(result.product.primary_image, :small), alt: result.product.name, class: 'insight__product-image')
16
+ .insight__product-name
17
+ - if result.product.blank?
18
+ = result.product_id
19
+ - else
20
+ = link_to result.product.name, catalog_product_path(result.product)
21
+ .insight__product-info
22
+ %strong= t('workarea.admin.insights.most_reviewed_products.reviews', count: result.reviews)
@@ -0,0 +1,22 @@
1
+ .insight
2
+ .insight__date
3
+ %span.insight__period= insight.reporting_on.strftime('%B %Y')
4
+ .insight__heading= t('workarea.admin.insights.top_rated_products.title')
5
+ .insight__body
6
+ %p.insight__note= t('workarea.admin.insights.top_rated_products.info')
7
+ .grid.grid--large.grid--center
8
+ - insight.results.each do |result|
9
+ .grid__cell.grid__cell--50.grid__cell--25-at-wide.align-center
10
+ .insight__product
11
+ - if result.product.blank?
12
+ = image_tag(product_image_url(Workarea::Catalog::ProductPlaceholderImage.cached, :small), alt: result.product_id, class: 'insight__product-image')
13
+ - else
14
+ = link_to catalog_product_path(result.product) do
15
+ = image_tag(product_image_url(result.product.primary_image, :small), alt: result.product.name, class: 'insight__product-image')
16
+ .insight__product-name
17
+ - if result.product.blank?
18
+ = result.product_id
19
+ - else
20
+ = link_to result.product.name, catalog_product_path(result.product)
21
+ .insight__product-info
22
+ %strong= t('workarea.admin.insights.top_rated_products.rating', rating: number_with_delimiter(result.weighted_average_rating.round(2)))
@@ -0,0 +1,44 @@
1
+ - @page_title = t('workarea.admin.reports.reviews_by_product.title')
2
+
3
+ .view
4
+ .view__header
5
+ .view__heading
6
+ = link_to "↑ #{t('workarea.admin.reports.all_reports')}", reports_dashboards_path
7
+ %h1.heading.heading--no-margin= t('workarea.admin.reports.reviews_by_product.title')
8
+ %p= t('workarea.admin.reports.reference_link_html', path: reference_report_path)
9
+
10
+ .view__container.view__container--narrow
11
+ .browsing-controls.browsing-controls--with-divider.browsing-controls--center.browsing-controls--filters-displayed
12
+ = form_tag reviews_by_product_report_path, method: 'get', class: 'browsing-controls__form' do
13
+ = render 'workarea/admin/shared/date_selector', starts_at: @report.starts_at, ends_at: @report.ends_at
14
+
15
+ .browsing-controls__filter
16
+ .property.property--inline
17
+ = label_tag 'results_filter', t('workarea.admin.reports.reviews_by_product.results_filter'), class: 'property__name'
18
+ = select_tag 'results_filter', options_for_select(reviews_report_filter_options, params[:results_filter]), data: { form_submitting_control: '' }
19
+
20
+
21
+ .browsing-controls__count
22
+ = render_reports_results_message(@report)
23
+ = render 'workarea/admin/reports/export', report: @report
24
+
25
+ %table
26
+ %thead
27
+ %tr
28
+ %th= t('workarea.admin.fields.product')
29
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.reviews'), report: @report, sort_by: 'reviews'
30
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.verified_reviews'), report: @report, sort_by: 'verified'
31
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.average_rating'), report: @report, sort_by: 'average_rating'
32
+ %th.align-center= link_to_reports_sorting t('workarea.admin.fields.weighted_average_rating'), report: @report, sort_by: 'weighted_average_rating'
33
+ %tbody
34
+ - @report.results.each do |result|
35
+ %tr
36
+ %td
37
+ - if result.product.present?
38
+ = link_to result.product.name, catalog_product_path(result.product)
39
+ - else
40
+ = result._id
41
+ %td.align-center= number_with_delimiter result.reviews
42
+ %td.align-center= number_with_delimiter result.verified
43
+ %td.align-center= number_with_delimiter result.average_rating
44
+ %td.align-center= number_with_delimiter result.weighted_average_rating
@@ -1,4 +1,4 @@
1
- %p.rating{ title: "#{rating.round(2)} #{t('workarea.storefront.reviews.out_of')} #{t('workarea.storefront.reviews.stars', count: 5)}", itemprop: itemprop, itemscope: true, itemtype: itemtype }
1
+ %p.rating{ title: "#{rating.round(2)} #{t('workarea.storefront.reviews.out_of')} #{t('workarea.storefront.reviews.stars', count: 5)}" }
2
2
  - full_star_count.times do
3
3
  = inline_svg 'workarea/storefront/icons/star.svg', class: 'rating__star'
4
4
  - if half_star_size > 0
@@ -6,5 +6,4 @@
6
6
  - empty_star_count.times do
7
7
  = inline_svg 'workarea/storefront/icons/empty_star.svg', class: 'rating__star'
8
8
  %span.rating__text.visually-hidden
9
- %meta{ itemprop: 'ratingValue', content: rating.round(2) }
10
9
  #{rating.round(2)} #{t('workarea.storefront.reviews.out_of')} #{t('workarea.storefront.reviews.stars', count: 5)}
@@ -10,14 +10,14 @@
10
10
 
11
11
  %ol.reviews__review-group
12
12
  - product.reviews.each do |review|
13
- %li.reviews__review{ itemprop: 'review', itemscope: true, itemtype: 'http://schema.org/Review', data: { product_review_section_entry: { rating: review.rating.to_f, createdAt: review.created_at.to_i }.to_json } }
13
+ %li.reviews__review{ data: { product_review_section_entry: { rating: review.rating.to_f, createdAt: review.created_at.to_i }.to_json } }
14
14
  = rating_stars(review.rating)
15
15
 
16
- %h3.reviews__review-title{ itemprop: 'name' }= review.title
17
- %p.reviews__review-body{ itemprop: 'reviewBody' }= review.body
16
+ %h3.reviews__review-title= review.title
17
+ %p.reviews__review-body= review.body
18
18
  .reviews__review-meta
19
- %p.reviews__review-author{ itemprop: 'author', itemscope: true, itemtype: 'http://schema.org/Person' }
20
- %span{ itemprop: 'name' }= review.user_info
19
+ %p.reviews__review-author
20
+ %span= review.user_info
21
21
  - if review.verified?
22
22
  %p.reviews__review-verified= t('workarea.storefront.reviews.verified_purchaser')
23
23
  %p.reviews__review-date= local_time(review.created_at, format: :long, itemprop: 'datePublished')
@@ -1,5 +1,5 @@
1
1
  - if product.has_reviews?
2
- .reviews-aggregate{ itemprop: 'aggregateRating', itemscope: true, itemtype: 'http://schema.org/AggregateRating' }
2
+ .reviews-aggregate
3
3
  = link_to "#{product_path(product, product.browse_link_options)}#reviews", data: { scroll_to_button: '' }, class: 'reviews-aggregate__rating-link' do
4
4
  = rating_stars(product.average_rating, aggregate: true)
5
5
 
@@ -7,7 +7,7 @@
7
7
  %span.reviews-aggregate__count
8
8
  %span.reviews-aggregate__read= t('workarea.storefront.reviews.read_reviews')
9
9
  = surround '(', ')' do
10
- %span{ itemprop: 'reviewCount' }= product.total_reviews
10
+ %span= product.total_reviews
11
11
 
12
12
  = link_to t('workarea.storefront.reviews.write_a_review'), new_product_review_path(product), class: 'reviews-aggregate__write-action', data: { dialog_button: '' }
13
13
  - if display_purchase_requirement_message
@@ -1,3 +1,5 @@
1
+ = render_schema_org(email_action_schema(review_request_url(@request.token), t('workarea.storefront.email.review_request.email_action.name'), t('workarea.storefront.email.review_request.email_action.description')))
2
+
1
3
  - content_for :preheader_text do
2
4
  = @content
3
5
 
@@ -5,7 +5,8 @@
5
5
 
6
6
  .grid
7
7
  .grid__cell.grid__cell--25
8
- .product-summary{ itemscope: true, itemtype: 'http://schema.org/Product' }
8
+ .product-summary
9
+ = render_schema_org(product_schema(@product))
9
10
  = render 'workarea/storefront/products/summary', product: @product
10
11
 
11
12
  .grid__cell.grid__cell--75
@@ -67,4 +67,9 @@ module Workarea
67
67
  'admin.marketing_dashboard_navigation',
68
68
  'workarea/admin/reviews/dashboard_navigation'
69
69
  )
70
+
71
+ Workarea.append_partials(
72
+ 'admin.reports_dashboard',
73
+ 'workarea/admin/dashboards/reviews_by_product_card'
74
+ )
70
75
  end
@@ -1,5 +1,5 @@
1
1
  Workarea.configure do |config|
2
- config.seeds << 'Workarea::ReviewSeeds'
2
+ config.seeds.insert('Workarea::InsightsSeeds', 'Workarea::ReviewSeeds')
3
3
 
4
4
  config.jump_to_navigation.merge!('Product Reviews' => :reviews_path)
5
5
 
@@ -11,6 +11,8 @@ Workarea.configure do |config|
11
11
 
12
12
  config.data_file_ignored_fields << %w(total_reviews average_rating)
13
13
 
14
+ config.insights_model_classes << 'Workarea::User'
15
+
14
16
  # The amount of time before a Review::Request will auto expire
15
17
  # and be removed from the collection
16
18
  config.review_request_ttl = 6.months
@@ -15,12 +15,55 @@ en:
15
15
  marketing:
16
16
  reviews: Product reviews
17
17
  fields:
18
+ average_rating: Average Rating
18
19
  body: Body
19
20
  title: Title
20
21
  rating: Rating
22
+ reviews: Reviews
21
23
  approved: Approved
22
24
  auto_approve: Approve All Reviews
23
25
  verified: Verified Purchaser
26
+ verified_reviews: Verified
27
+ weighted_average_rating: Weighted Average Rating
28
+ insights:
29
+ most_active_reviewers:
30
+ info: Your customers that are submitting the most reviews.
31
+ reviews: Reviews
32
+ title: Most Active Reviewers
33
+ user: User
34
+ verified: Verified Reviews
35
+ most_reviewed_products:
36
+ title: Most Reviewed Products
37
+ info: Your products that have received the most reviews.
38
+ reviews:
39
+ one: '%{count} Review'
40
+ other: '%{count} Reviews'
41
+ top_rated_products:
42
+ title: Top Rated Products
43
+ info: Your products with the highest weighted average rating.
44
+ rating: '%{rating} Rating'
45
+ reports:
46
+ reference:
47
+ terms:
48
+ average_rating:
49
+ name: Average Rating
50
+ description: The sum of all review ratings divided by the total number of reviews.
51
+ reviews:
52
+ name: Reviews
53
+ description: The total number of reviews.
54
+ verified:
55
+ name: Verified
56
+ description: The total number of reviews made by customers who have purchased the product.
57
+ weighted_average_rating:
58
+ name: Weighted Average Rating
59
+ description: A base rating of 3.0, adjusted up or down by each review rating.
60
+ reviews_by_product:
61
+ filters:
62
+ all: All Reviews
63
+ approved: Approved Reviews
64
+ unapproved: Unapproved Reviews
65
+ results_filter: Results Filter
66
+ title: Reviews By Product
24
67
  reviews:
25
68
  approved: Approved
26
69
  catalog_products_cards:
@@ -66,6 +109,9 @@ en:
66
109
  review_request:
67
110
  subject: Review your recent purchase of %{product}
68
111
  link: Write a review
112
+ email_action:
113
+ name: Write Review
114
+ description: Please review your recent purchase
69
115
  fields:
70
116
  username: Username
71
117
  review_requests:
@@ -6,6 +6,10 @@ Workarea::Admin::Engine.routes.draw do
6
6
  get :reviews
7
7
  end
8
8
  end
9
+
10
+ resource :report, only: [] do
11
+ get :reviews_by_product
12
+ end
9
13
  end
10
14
  end
11
15
 
@@ -5,7 +5,11 @@ module Workarea
5
5
  isolate_namespace Workarea::Reviews
6
6
 
7
7
  config.to_prepare do
8
+ Admin::ApplicationController.helper(Admin::ReviewsHelper)
8
9
  Storefront::ApplicationController.helper(Storefront::ReviewsHelper)
10
+ Workarea::Storefront::ApplicationController.helper(
11
+ Workarea::Storefront::ReviewsSchemaOrgHelper
12
+ )
9
13
  end
10
14
  end
11
15
  end
@@ -1,5 +1,5 @@
1
1
  module Workarea
2
2
  module Reviews
3
- VERSION = '3.0.10'.freeze
3
+ VERSION = '3.1.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,9 @@
1
+ {
2
+ "devDependencies": {
3
+ "eslint": "~5.16.0",
4
+ "eslint-config-recommended": "~4.0.0",
5
+ "stylelint": "~10.1.0",
6
+ "stylelint-config-recommended-scss": "~3.3.0",
7
+ "stylelint-scss": "~3.9.2"
8
+ }
9
+ }