solidus_reviews 1.0.0 → 1.1.0

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 (100) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +322 -0
  3. data/.travis.yml +27 -14
  4. data/Gemfile +23 -8
  5. data/LICENSE.md +2 -2
  6. data/README.md +9 -11
  7. data/Rakefile +2 -0
  8. data/app/controllers/spree/admin/feedback_reviews_controller.rb +3 -1
  9. data/app/controllers/spree/admin/review_settings_controller.rb +3 -1
  10. data/app/controllers/spree/admin/reviews_controller.rb +9 -6
  11. data/app/controllers/spree/feedback_reviews_controller.rb +25 -19
  12. data/app/controllers/spree/products_controller_decorator.rb +2 -0
  13. data/app/controllers/spree/reviews_controller.rb +11 -7
  14. data/app/helpers/spree/api/api_helpers_decorator.rb +12 -0
  15. data/app/helpers/spree/reviews_helper.rb +15 -5
  16. data/app/models/spree/feedback_review.rb +5 -4
  17. data/app/models/spree/product_decorator.rb +5 -4
  18. data/app/models/spree/review.rb +28 -10
  19. data/app/models/spree/reviews_ability.rb +17 -3
  20. data/app/models/spree/reviews_configuration.rb +16 -8
  21. data/app/models/spree/user_decorator.rb +3 -0
  22. data/app/overrides/add_reviews_after_product_properties.rb +7 -5
  23. data/app/overrides/add_reviews_tab_to_admin.rb +12 -4
  24. data/app/overrides/add_reviews_to_admin_configuration_sidebar.rb +13 -5
  25. data/app/views/spree/admin/feedback_reviews/index.html.erb +9 -9
  26. data/app/views/spree/admin/review_settings/edit.html.erb +64 -35
  27. data/app/views/spree/admin/reviews/_form.html.erb +26 -5
  28. data/app/views/spree/admin/reviews/edit.html.erb +2 -2
  29. data/app/views/spree/admin/reviews/index.html.erb +100 -88
  30. data/app/views/spree/api/reviews/_review.json.jbuilder +8 -0
  31. data/app/views/spree/api/reviews/index.json.jbuilder +6 -0
  32. data/app/views/spree/api/reviews/show.json.jbuilder +3 -0
  33. data/app/views/spree/feedback_reviews/_form.html.erb +5 -5
  34. data/app/views/spree/feedback_reviews/_summary.html.erb +3 -3
  35. data/app/views/spree/feedback_reviews/create.js.erb +2 -2
  36. data/app/views/spree/reviews/_form.html.erb +15 -10
  37. data/app/views/spree/reviews/new.html.erb +2 -2
  38. data/app/views/spree/shared/_rating.html.erb +3 -3
  39. data/app/views/spree/shared/_review.html.erb +29 -19
  40. data/app/views/spree/shared/_review_summary.html.erb +3 -3
  41. data/app/views/spree/shared/_reviews.html.erb +6 -6
  42. data/app/views/spree/shared/_shortrating.html.erb +2 -2
  43. data/bin/rails +4 -2
  44. data/config/initializers/constants.rb +2 -0
  45. data/config/initializers/load_preferences.rb +2 -0
  46. data/config/locales/de-CH.yml +14 -1
  47. data/config/locales/de.yml +14 -1
  48. data/config/locales/en-GB.yml +14 -2
  49. data/config/locales/en.yml +14 -1
  50. data/config/locales/es.yml +14 -2
  51. data/config/locales/fr.yml +14 -2
  52. data/config/locales/it.yml +79 -0
  53. data/config/locales/pl.yml +14 -2
  54. data/config/locales/pt-BR.yml +14 -2
  55. data/config/locales/pt.yml +14 -2
  56. data/config/locales/ro.yml +15 -3
  57. data/config/locales/ru.yml +14 -2
  58. data/config/locales/sv.yml +14 -1
  59. data/config/locales/tr.yml +14 -2
  60. data/config/locales/uk.yml +14 -1
  61. data/config/locales/zh-CN.yml +14 -1
  62. data/config/locales/zh-TW.yml +14 -1
  63. data/config/routes.rb +18 -1
  64. data/db/migrate/20081020220724_create_reviews.rb +4 -2
  65. data/db/migrate/20101222083309_create_feedback_reviews.rb +5 -3
  66. data/db/migrate/20110406083603_add_rating_to_products.rb +7 -5
  67. data/db/migrate/20110606150524_add_user_to_reviews.rb +4 -2
  68. data/db/migrate/20110806093221_add_ip_address_to_reviews.rb +3 -1
  69. data/db/migrate/20120110172331_namespace_tables.rb +3 -1
  70. data/db/migrate/20120123141326_recalculate_ratings.rb +6 -5
  71. data/db/migrate/20120712182514_add_locale_to_reviews.rb +4 -2
  72. data/db/migrate/20120712182627_add_locale_to_feedback_reviews.rb +4 -2
  73. data/db/migrate/20140703200946_add_show_identifier_to_reviews.rb +3 -1
  74. data/db/migrate/20190613165528_add_verified_purchaser_to_reviews.rb +5 -0
  75. data/lib/controllers/spree/api/reviews_controller.rb +111 -0
  76. data/lib/generators/solidus_reviews/install/install_generator.rb +5 -4
  77. data/lib/solidus_reviews.rb +3 -1
  78. data/lib/solidus_reviews/factories.rb +3 -1
  79. data/lib/solidus_reviews/factories/feedback_review_factory.rb +6 -5
  80. data/lib/solidus_reviews/factories/review_factory.rb +19 -9
  81. data/lib/spree_reviews/engine.rb +7 -0
  82. data/solidus_reviews.gemspec +20 -18
  83. data/spec/controllers/admin/feedback_reviews_controller_spec.rb +9 -7
  84. data/spec/controllers/admin/review_settings_controller_spec.rb +18 -16
  85. data/spec/controllers/admin/reviews_controller_spec.rb +18 -16
  86. data/spec/controllers/feedback_reviews_controller_spec.rb +29 -25
  87. data/spec/controllers/products_controller_spec.rb +4 -2
  88. data/spec/controllers/reviews_controller_spec.rb +67 -40
  89. data/spec/controllers/spree/api/reviews_controller_spec.rb +233 -0
  90. data/spec/features/admin_spec.rb +3 -0
  91. data/spec/features/reviews_spec.rb +27 -11
  92. data/spec/helpers/review_helper_spec.rb +3 -2
  93. data/spec/models/feedback_review_spec.rb +19 -17
  94. data/spec/models/product_spec.rb +41 -19
  95. data/spec/models/review_spec.rb +100 -42
  96. data/spec/models/reviews_ability_spec.rb +10 -8
  97. data/spec/models/reviews_configuration_spec.rb +28 -19
  98. data/spec/spec_helper.rb +21 -35
  99. data/vendor/assets/javascripts/jquery.rating.js +389 -376
  100. metadata +108 -59
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler'
2
4
  Bundler::GemHelper.install_tasks
3
5
 
@@ -1,4 +1,6 @@
1
- class Spree::Admin::FeedbackReviewsController < Spree::Admin::ResourceController
1
+ # frozen_string_literal: true
2
+
3
+ class Spree::Admin::FeedbackReviewsController < Spree::Admin::ResourceController
2
4
  belongs_to 'spree/review'
3
5
  def index
4
6
  @collection = parent.feedback_reviews
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::Admin::ReviewSettingsController < Spree::Admin::BaseController
2
- before_filter :process_unset_checkboxes, only: [:update]
4
+ before_action :process_unset_checkboxes, only: [:update]
3
5
 
4
6
  def update
5
7
  Spree::Reviews::Config.set(params[:preferences])
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::Admin::ReviewsController < Spree::Admin::ResourceController
2
4
  helper Spree::ReviewsHelper
3
5
 
@@ -6,20 +8,21 @@ class Spree::Admin::ReviewsController < Spree::Admin::ResourceController
6
8
  end
7
9
 
8
10
  def approve
9
- r = Spree::Review.find(params[:id])
11
+ review = Spree::Review.find(params[:id])
10
12
 
11
- if r.update_attribute(:approved, true)
12
- flash[:notice] = Spree.t("info_approve_review")
13
+ if review.update_attribute(:approved, true)
14
+ flash[:success] = I18n.t('spree.info_approve_review')
13
15
  else
14
- flash[:error] = Spree.t("error_approve_review")
16
+ flash[:error] = I18n.t('spree.error_approve_review')
15
17
  end
18
+
16
19
  redirect_to admin_reviews_path
17
20
  end
18
21
 
19
22
  def edit
20
23
  if @review.product.nil?
21
- flash[:error] = Spree.t("error_no_product")
22
- redirect_to admin_reviews_path and return
24
+ flash[:error] = I18n.t('spree.error_no_product')
25
+ redirect_to admin_reviews_path
23
26
  end
24
27
  end
25
28
 
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::FeedbackReviewsController < Spree::StoreController
2
4
  helper Spree::BaseHelper
3
5
 
4
- before_filter :sanitize_rating, only: [:create]
5
- before_filter :load_review, only: [:create]
6
+ before_action :sanitize_rating, only: [:create]
7
+ before_action :load_review, only: [:create]
6
8
 
7
9
  def create
8
-
9
10
  if @review.present?
10
11
  @feedback_review = @review.feedback_reviews.new(feedback_review_params)
11
12
  @feedback_review.user = spree_current_user
@@ -15,27 +16,32 @@ class Spree::FeedbackReviewsController < Spree::StoreController
15
16
  end
16
17
 
17
18
  respond_to do |format|
18
- format.html { redirect_to :back }
19
- format.js { render :action => :create }
19
+ format.html {
20
+ if SolidusSupport.solidus_gem_version < Gem::Version.new('2.0')
21
+ redirect_to(:back)
22
+ else
23
+ redirect_back(fallback_location: root_path)
24
+ end
25
+ }
26
+ format.js { render action: :create }
20
27
  end
21
-
22
28
  end
23
29
 
24
30
  protected
25
- def load_review
26
- @review ||= Spree::Review.find_by_id!(params[:review_id])
27
- end
28
31
 
29
- def permitted_feedback_review_attributes
30
- [:rating, :comment]
31
- end
32
+ def load_review
33
+ @review ||= Spree::Review.find_by_id!(params[:review_id])
34
+ end
32
35
 
33
- def feedback_review_params
34
- params.require(:feedback_review).permit(permitted_feedback_review_attributes)
35
- end
36
+ def permitted_feedback_review_attributes
37
+ [:rating, :comment]
38
+ end
36
39
 
37
- def sanitize_rating
38
- params[:feedback_review][:rating].to_s.sub!(/\s*[^0-9]*\z/,'') unless (params[:feedback_review] && params[:feedback_review][:rating].blank?)
39
- end
40
- end
40
+ def feedback_review_params
41
+ params.require(:feedback_review).permit(permitted_feedback_review_attributes)
42
+ end
41
43
 
44
+ def sanitize_rating
45
+ params[:feedback_review][:rating].to_s.sub!(/\s*[^0-9]*\z/, '') unless params[:feedback_review] && params[:feedback_review][:rating].blank?
46
+ end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Spree::ProductsController.class_eval do
2
4
  helper Spree::ReviewsHelper
3
5
 
@@ -1,30 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::ReviewsController < Spree::StoreController
2
4
  helper Spree::BaseHelper
3
- before_filter :load_product, :only => [:index, :new, :create]
4
- rescue_from ActiveRecord::RecordNotFound, :with => :render_404
5
+ before_action :load_product, only: [:index, :new, :create]
5
6
 
6
7
  def index
7
8
  @approved_reviews = Spree::Review.approved.where(product: @product)
8
9
  end
9
10
 
10
11
  def new
11
- @review = Spree::Review.new(:product => @product)
12
+ @review = Spree::Review.new(product: @product)
12
13
  authorize! :create, @review
13
14
  end
14
15
 
15
16
  # save if all ok
16
17
  def create
17
- params[:review][:rating].sub!(/\s*[^0-9]*\z/,'') unless params[:review][:rating].blank?
18
+ params[:review][:rating].sub!(/\s*[^0-9]*\z/, '') unless params[:review][:rating].blank?
18
19
 
19
20
  @review = Spree::Review.new(review_params)
20
21
  @review.product = @product
21
22
  @review.user = spree_current_user if spree_user_signed_in?
22
23
  @review.ip_address = request.remote_ip
23
24
  @review.locale = I18n.locale.to_s if Spree::Reviews::Config[:track_locale]
25
+ # Handle images
26
+ params[:review][:images]&.each do |image|
27
+ @review.images.new(attachment: image)
28
+ end
24
29
 
25
30
  authorize! :create, @review
26
31
  if @review.save
27
- flash[:notice] = Spree.t('review_successfully_submitted')
32
+ flash[:notice] = I18n.t('spree.review_successfully_submitted')
28
33
  redirect_to spree.product_path(@product)
29
34
  else
30
35
  render :new
@@ -38,11 +43,10 @@ class Spree::ReviewsController < Spree::StoreController
38
43
  end
39
44
 
40
45
  def permitted_review_attributes
41
- [:rating, :title, :review, :name, :show_identifier]
46
+ [:rating, :title, :review, :name, :show_identifier, :images]
42
47
  end
43
48
 
44
49
  def review_params
45
50
  params.require(:review).permit(permitted_review_attributes)
46
51
  end
47
-
48
52
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Api::ApiHelpers.module_eval do
4
+ @@review_attributes = [
5
+ :id, :product_id, :name, :location, :rating, :title, :review, :approved,
6
+ :created_at, :updated_at, :user_id, :ip_address, :locale, :show_identifier
7
+ ]
8
+
9
+ def review_attributes
10
+ @@review_attributes
11
+ end
12
+ end
@@ -1,17 +1,27 @@
1
- module Spree::ReviewsHelper
1
+ # frozen_string_literal: true
2
2
 
3
+ module Spree::ReviewsHelper
3
4
  def star(the_class)
4
- content_tag(:span, " &#10030; ".html_safe, :class => the_class)
5
+ content_tag(:span, " &#10030; ".html_safe, class: the_class)
5
6
  end
6
7
 
7
8
  def mk_stars(m)
8
- (1..5).collect {|n| n <= m ? star("lit") : star("unlit") }.join
9
+ (1..5).collect { |n| n <= m ? star("lit") : star("unlit") }.join
9
10
  end
10
11
 
11
12
  def txt_stars(n, show_out_of = true)
12
- res = Spree.t('star', :count => n)
13
- res += " #{Spree.t('out_of_5')}" if show_out_of
13
+ res = I18n.t('spree.star', count: n)
14
+ res += " #{I18n.t('spree.out_of_5')}" if show_out_of
14
15
  res
15
16
  end
16
17
 
18
+ def display_verified_purchaser?(review)
19
+ Spree::Reviews::Config[:show_verified_purchaser] && review.user &&
20
+ Spree::LineItem.joins(:order, :variant)
21
+ .where.not(spree_orders: { completed_at: nil })
22
+ .find_by(
23
+ spree_variants: { product_id: review.product_id },
24
+ spree_orders: { user_id: review.user_id }
25
+ ).present?
26
+ end
17
27
  end
@@ -1,17 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::FeedbackReview < ActiveRecord::Base
2
- belongs_to :user, :class_name => Spree.user_class.to_s
4
+ belongs_to :user, class_name: Spree.user_class.to_s
3
5
 
4
6
  belongs_to :review, dependent: :destroy
5
7
  validates :review, presence: true
6
8
 
7
9
  validates :rating, numericality: { only_integer: true,
8
- greater_than_or_equal_to: 1,
10
+ greater_than_or_equal_to: 1,
9
11
  less_than_or_equal_to: 5,
10
- message: Spree.t('you_must_enter_value_for_rating') }
12
+ message: :you_must_enter_value_for_rating }
11
13
 
12
14
  scope :most_recent_first, -> { order("spree_feedback_reviews.created_at DESC") }
13
15
  default_scope { most_recent_first }
14
16
 
15
17
  scope :localized, lambda { |lc| where('spree_feedback_reviews.locale = ?', lc) }
16
-
17
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Add access to reviews/ratings to the product model
2
4
  Spree::Product.class_eval do
3
5
  has_many :reviews
@@ -7,15 +9,14 @@ Spree::Product.class_eval do
7
9
  end
8
10
 
9
11
  def recalculate_rating
10
- reviews_count = self.reviews.reload.approved.count
12
+ reviews_count = reviews.reload.default_approval_filter.count
11
13
 
12
14
  self.reviews_count = reviews_count
13
15
  if reviews_count > 0
14
- self.avg_rating = self.reviews.approved.sum(:rating).to_f / reviews_count
16
+ self.avg_rating = '%.1f' % (reviews.default_approval_filter.sum(:rating).to_f / reviews_count)
15
17
  else
16
18
  self.avg_rating = 0
17
19
  end
18
- self.save
20
+ save
19
21
  end
20
-
21
22
  end
@@ -1,22 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::Review < ActiveRecord::Base
2
4
  belongs_to :product, touch: true
3
- belongs_to :user, :class_name => Spree.user_class.to_s
5
+ belongs_to :user, class_name: Spree.user_class.to_s
4
6
  has_many :feedback_reviews
7
+ has_many :images, -> { order(:position) }, as: :viewable,
8
+ dependent: :destroy, class_name: "Spree::Image"
5
9
 
6
- after_save :recalculate_product_rating, :if => :approved?
10
+ before_create :verify_purchaser
11
+ after_save :recalculate_product_rating, if: :approved?
7
12
  after_destroy :recalculate_product_rating
8
13
 
9
- validates :name, presence: true
10
- validates :review, presence: true
11
-
12
14
  validates :rating, numericality: { only_integer: true,
13
- greater_than_or_equal_to: 1,
15
+ greater_than_or_equal_to: 1,
14
16
  less_than_or_equal_to: 5,
15
- message: Spree.t('you_must_enter_value_for_rating') }
16
-
17
+ message: :you_must_enter_value_for_rating }
17
18
 
18
19
  default_scope { order("spree_reviews.created_at DESC") }
19
-
20
+
20
21
  scope :localized, ->(lc) { where('spree_reviews.locale = ?', lc) }
21
22
  scope :most_recent_first, -> { order('spree_reviews.created_at DESC') }
22
23
  scope :oldest_first, -> { reorder('spree_reviews.created_at ASC') }
@@ -31,6 +32,23 @@ class Spree::Review < ActiveRecord::Base
31
32
  end
32
33
 
33
34
  def recalculate_product_rating
34
- self.product.recalculate_rating if product.present?
35
+ product.recalculate_rating if product.present?
36
+ end
37
+
38
+ def email
39
+ user.try!(:email)
40
+ end
41
+
42
+ def verify_purchaser
43
+ return unless user_id && product_id
44
+
45
+ verified_purchase = Spree::LineItem.joins(:order, :variant)
46
+ .where.not(spree_orders: { completed_at: nil })
47
+ .find_by(
48
+ spree_variants: { product_id: product_id },
49
+ spree_orders: { user_id: user_id }
50
+ ).present?
51
+
52
+ self.verified_purchaser = verified_purchase
35
53
  end
36
54
  end
@@ -1,14 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::ReviewsAbility
2
4
  include CanCan::Ability
3
5
 
4
- def initialize user
6
+ def initialize(user)
5
7
  review_ability_class = self.class
6
- can :create, Spree::Review do |review|
8
+
9
+ can :create, Spree::Review do |_review|
7
10
  review_ability_class.allow_anonymous_reviews? || !user.email.blank?
8
11
  end
9
- can :create, Spree::FeedbackReview do |review|
12
+
13
+ can :create, Spree::FeedbackReview do |_review|
10
14
  review_ability_class.allow_anonymous_reviews? || !user.email.blank?
11
15
  end
16
+
17
+ # You can read your own reviews, and everyone can read approved ones
18
+ can :read, Spree::Review do |review|
19
+ review.user == user || review.approved?
20
+ end
21
+
22
+ # You can only change your own review
23
+ can [:update, :destroy], Spree::Review do |review|
24
+ review.user == user
25
+ end
12
26
  end
13
27
 
14
28
  def self.allow_anonymous_reviews?
@@ -1,26 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Spree::ReviewsConfiguration < Spree::Preferences::Configuration
2
4
  def self.boolean_preferences
3
- %w(include_unapproved_reviews feedback_rating show_email require_login track_locale)
5
+ %w(display_unapproved_reviews include_unapproved_reviews feedback_rating show_email require_login track_locale)
4
6
  end
5
7
 
6
8
  # include non-approved reviews in (public) listings
7
- preference :include_unapproved_reviews, :boolean, :default => false
9
+ preference :include_unapproved_reviews, :boolean, default: false
10
+
11
+ # displays non-approved reviews in (public) listings
12
+ preference :display_unapproved_reviews, :boolean, default: false
8
13
 
9
14
  # control how many reviews are shown in summaries etc.
10
- preference :preview_size, :integer, :default => 3
15
+ preference :preview_size, :integer, default: 3
11
16
 
12
17
  # show a reviewer's email address
13
- preference :show_email, :boolean, :default => false
18
+ preference :show_email, :boolean, default: false
19
+
20
+ # show if a reviewer actually purchased the product
21
+ preference :show_verified_purchaser, :boolean, default: false
14
22
 
15
23
  # show helpfullness rating form elements
16
- preference :feedback_rating, :boolean, :default => false
24
+ preference :feedback_rating, :boolean, default: false
17
25
 
18
26
  # require login to post reviews
19
- preference :require_login, :boolean, :default => true
27
+ preference :require_login, :boolean, default: true
20
28
 
21
29
  # whether to keep track of the reviewer's locale
22
- preference :track_locale, :boolean, :default => false
30
+ preference :track_locale, :boolean, default: false
23
31
 
24
32
  # render checkbox for a user to approve to show their identifier (name or email) on their review
25
- preference :render_show_identifier_checkbox, :boolean, :default => false
33
+ preference :render_show_identifier_checkbox, :boolean, default: false
26
34
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Spree.user_class.has_many :reviews, class_name: 'Spree::Review'
@@ -1,5 +1,7 @@
1
- Deface::Override.new(:virtual_path => "spree/products/show",
2
- :name => "converted_product_properties_767643482",
3
- :insert_after => "[data-hook='product_properties']",
4
- :partial => "spree/shared/reviews",
5
- :disabled => false)
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(virtual_path: "spree/products/show",
4
+ name: "converted_product_properties_767643482",
5
+ insert_after: "[data-hook='product_properties']",
6
+ partial: "spree/shared/reviews",
7
+ disabled: false)
@@ -1,4 +1,12 @@
1
- Deface::Override.new(:virtual_path => "spree/admin/shared/_product_sub_menu",
2
- :name => "reviews_admin_tab",
3
- :insert_bottom => "[data-hook='admin_product_sub_tabs']",
4
- :text => "<%= tab(:reviews, :label => 'review_management') %>")
1
+ # frozen_string_literal: true
2
+
3
+ Spree::Backend::Config.configure do |config|
4
+ config.menu_items.detect { |menu_item|
5
+ menu_item.label == :products
6
+ }.sections << :reviews
7
+ end
8
+
9
+ Deface::Override.new(virtual_path: "spree/admin/shared/_product_sub_menu",
10
+ name: "reviews_admin_tab",
11
+ insert_bottom: "[data-hook='admin_product_sub_tabs']",
12
+ text: "<%= tab(:reviews, label: 'review_management') %>")