spree_product_reviews 1.0.0 → 1.2.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/spree/admin/product_reviews_controller.rb +15 -7
  3. data/app/controllers/spree/admin/review_settings_controller.rb +23 -0
  4. data/app/controllers/spree/admin/reviews_controller.rb +18 -0
  5. data/app/controllers/spree/product_reviews_controller.rb +40 -9
  6. data/app/models/spree_product_reviews/spree/store_decorator.rb +19 -0
  7. data/app/views/spree/admin/product_reviews/_product_review.html.erb +60 -52
  8. data/app/views/spree/admin/product_reviews/_table.html.erb +8 -2
  9. data/app/views/spree/admin/product_reviews/index.html.erb +83 -10
  10. data/app/views/spree/admin/review_settings/edit.html.erb +259 -0
  11. data/app/views/spree/admin/reviews/index.html.erb +16 -0
  12. data/app/views/spree/page_sections/_product_reviews.html.erb +9 -6
  13. data/app/views/spree/product_reviews/_form.html.erb +1 -3
  14. data/app/views/spree/product_reviews/new.html.erb +40 -0
  15. data/app/views/{spree → themes/default/spree}/products/_json_ld.html.erb +1 -1
  16. data/config/initializers/spree_product_reviews.rb +15 -7
  17. data/config/locales/en.yml +38 -6
  18. data/config/routes.rb +11 -3
  19. data/db/migrate/20250501173852_add_reviews.rb +1 -1
  20. data/db/migrate/20250501174531_add_review_info_to_products.rb +1 -1
  21. data/db/migrate/20260105145731_add_spam_to_spree_product_reviews.rb +5 -0
  22. data/lib/generators/spree_product_reviews/install/install_generator.rb +2 -3
  23. data/lib/spree_product_reviews/configuration.rb +5 -10
  24. data/lib/spree_product_reviews/engine.rb +1 -6
  25. data/lib/spree_product_reviews/version.rb +1 -1
  26. metadata +11 -8
  27. data/app/views/themes/default/spree/page_sections/_product_details.html.erb +0 -89
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 743b927cdaf050b3014719af7d93fd72298516fe5906b9c7ee78aa11ac0aa9b1
4
- data.tar.gz: f5343a9a1f75dc591b02edd6b0d88b208da346a1d5ea8932f67165d6e64432af
3
+ metadata.gz: ec19cfa05a7b060251012fac3924e5566796626a04480bde5e7bd11ea7d5ef63
4
+ data.tar.gz: c82cccb478d74e2d54067bce06124df092c782a378c2c85da18fc4938c39a8d6
5
5
  SHA512:
6
- metadata.gz: 9809e4fd93893bc2172f1a848b075832182bf4079cf16580d4ae0e2552a119f7c5a20d9085f6760414f940db38165c9a9533c1368fe8f4c19bf58ddaa447d106
7
- data.tar.gz: e8fae77c66014f097f38d40d6bd7382215814bbc5cfba008e9df280a5b1e8e30ff50a7a6867cec47bdf86ffd37ca03d29a100deadd16e0b6c7f13bac8879d1e1
6
+ metadata.gz: 1e2a18db7f6b7fed14cd097c54f6da904e99a1108f3f21adc0e2974bdbe9d9d8bce08a7a5e7b3df7f65939f854cba015ffda8431eddab9507d84a307412dceee
7
+ data.tar.gz: db313f9911b65241c29d9759112eb3c4b77c6981f59f377d530d540644b4b3e0c2994a5ffda249970fddbac8d39c994f9760de5471478eec0ae1c3f0cfd334ca
@@ -6,7 +6,7 @@ module Spree
6
6
  before_action :find_product_review, only: [:approve, :edit, :update, :destroy, :attach_image, :purge_images]
7
7
 
8
8
  def index
9
- # optionally add filtering/sorting
9
+ # optionally add filtering/sorting here
10
10
  end
11
11
 
12
12
  def update
@@ -33,7 +33,6 @@ module Spree
33
33
  redirect_to admin_product_product_reviews_path(@product_review.product)
34
34
  end
35
35
 
36
- # --- Attach a single image ---
37
36
  def attach_image
38
37
  if params[:file].present?
39
38
  @product_review.images.attach(params[:file])
@@ -41,7 +40,6 @@ module Spree
41
40
  head :ok
42
41
  end
43
42
 
44
- # DELETE /admin/products/:product_id/product_reviews/:id/purge_images
45
43
  def purge_images
46
44
  ids = params[:ids] || []
47
45
  @product_review.images.where(id: ids).each(&:purge_later)
@@ -52,14 +50,24 @@ module Spree
52
50
  end
53
51
  end
54
52
 
53
+ protected
54
+
55
+ def location_after_destroy
56
+ if request.referer.to_s.include?('/admin/reviews')
57
+ admin_reviews_path
58
+ else
59
+ admin_product_product_reviews_path(@product)
60
+ end
61
+ end
62
+
55
63
  private
56
64
 
57
65
  def permitted_resource_params
58
66
  params.require(:product_review).permit(
59
- :title,
60
- :rating,
61
- :review,
62
- images: []
67
+ :title,
68
+ :rating,
69
+ :review,
70
+ images: []
63
71
  )
64
72
  end
65
73
 
@@ -0,0 +1,23 @@
1
+ module Spree
2
+ module Admin
3
+ class ReviewSettingsController < Spree::Admin::BaseController
4
+ def edit
5
+ @products = Spree::Product.order(updated_at: :desc).limit(500)
6
+ end
7
+
8
+ def update
9
+ current_store.set_preference(:review_status_default, params[:review_status_default])
10
+ current_store.set_preference(:block_spam_reviews, params[:block_spam_reviews] == '1')
11
+ current_store.set_preference(:disable_review_links, params[:disable_review_links] == '1')
12
+ current_store.set_preference(:disable_review_emails, params[:disable_review_emails] == '1')
13
+
14
+ current_store.set_preference(:spam_words, params[:spam_words])
15
+
16
+ current_store.save!
17
+
18
+ flash[:success] = Spree.t(:successfully_updated, resource: Spree.t(:review_settings))
19
+ redirect_to edit_admin_review_settings_path
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module Spree
2
+ module Admin
3
+ class ReviewsController < ResourceController
4
+ # We map this controller to the Spree::ProductReview model
5
+ def model_class
6
+ Spree::ProductReview
7
+ end
8
+
9
+ def index
10
+ # Eager load product and user to prevent N+1 queries
11
+ @product_reviews = Spree::ProductReview.accessible_by(current_ability)
12
+ .includes(:product, :user, images_attachments: :blob)
13
+ .order(created_at: :desc)
14
+ .page(params[:page])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -24,16 +24,53 @@ module Spree
24
24
  flash[:error] = "You have already reviewed this product."
25
25
  redirect_to spree.product_path(@product) and return
26
26
  end
27
+
28
+ default_status = (current_store.preferred_review_status_default rescue 'pending')
29
+ should_approve = (default_status == 'approved')
30
+ spam_detected = false
31
+
32
+ if (current_store.preferred_disable_review_links rescue false) && @product_review.review.present?
33
+ if @product_review.review.match?(%r{https?://|www\.|[a-zA-Z0-9]+\.(com|net|org|info|biz)})
34
+ should_approve = false
35
+ spam_detected = true
36
+ end
37
+ end
38
+
39
+ if (current_store.preferred_disable_review_emails rescue false) && @product_review.review.present?
40
+ if @product_review.review.match?(%r{[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}})
41
+ should_approve = false
42
+ spam_detected = true
43
+ end
44
+ end
45
+
46
+ if (current_store.preferred_block_spam_reviews rescue false) && @product_review.review.present?
47
+ custom_words = (current_store.preferred_spam_words || "").split(',').map(&:strip).reject(&:empty?)
48
+ custom_words = %w[casino viagra crypto bitcoin lottery loan investment] if custom_words.empty?
49
+
50
+ full_text = "#{@product_review.title} #{@product_review.review}".downcase
51
+
52
+ if custom_words.any? { |word| full_text.include?(word.downcase) }
53
+ should_approve = false
54
+ spam_detected = true
55
+ end
56
+ end
57
+
58
+ @product_review.approved = should_approve
59
+ @product_review.spam = spam_detected
27
60
 
28
61
  if @product_review.save
29
- flash[:success] = Spree.t("product_review.flash_messages.create.success")
62
+ if @product_review.approved?
63
+ flash[:success] = Spree.t("product_review.flash_messages.create.approved")
64
+ else
65
+ flash[:success] = Spree.t("product_review.flash_messages.create.success")
66
+ end
30
67
  redirect_to spree.product_path(@product)
31
68
  else
32
69
  flash[:error] = Spree.t("product_review.flash_messages.create.failure")
33
70
  render :new
34
71
  end
35
72
  end
36
-
73
+
37
74
  def index
38
75
  @product_reviews = @product.product_reviews.approved.order(created_at: :desc)
39
76
  end
@@ -41,7 +78,6 @@ module Spree
41
78
  def destroy
42
79
  @product_review = Spree::ProductReview.find(params[:id])
43
80
  @product_review.destroy
44
- # Force CanCanCan to reload abilities for the current user
45
81
  session[:_csrf_token] = nil
46
82
  redirect_to spree.product_path(@product), notice: Spree.t(:review_deleted)
47
83
  end
@@ -54,17 +90,12 @@ module Spree
54
90
 
55
91
  def product_review_params
56
92
  params.require(:product_review).permit(
57
- :rating,
58
- :review,
59
- :show_identifier,
60
- :title,
61
- images: []
93
+ :rating, :review, :show_identifier, :title, images: []
62
94
  )
63
95
  end
64
96
 
65
97
  def authenticate_user!
66
98
  return if spree_current_user
67
-
68
99
  session[:spree_user_return_to] = request.fullpath
69
100
  redirect_to spree.login_path, alert: Spree.t(:please_log_in)
70
101
  end
@@ -0,0 +1,19 @@
1
+ module SpreeProductReviews
2
+ module Spree
3
+ module StoreDecorator
4
+ def self.prepended(base)
5
+ base.preference :review_status_default, :string, default: 'pending'
6
+
7
+ # Content Moderation Preferences
8
+ base.preference :block_spam_reviews, :boolean, default: false
9
+ base.preference :disable_review_links, :boolean, default: false
10
+ base.preference :disable_review_emails, :boolean, default: false
11
+
12
+ # New: Custom Spam Words List (Comma separated)
13
+ base.preference :spam_words, :text, default: "casino, viagra, crypto, bitcoin, lottery, loan, investment, free access, free trial, giveaway, make money"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ::Spree::Store.prepend SpreeProductReviews::Spree::StoreDecorator
@@ -1,79 +1,87 @@
1
+ <% global_view ||= false %>
2
+
1
3
  <tr id="<%= dom_id(product_review) %>">
2
- <td class="w-10 cursor-pointer" data-action="click->row-link#openLink">
4
+
5
+ <%# --- 1. PRODUCT COLUMN (Only for Global View) --- %>
6
+ <% if global_view %>
7
+ <td>
8
+ <div class="d-flex align-items-center">
9
+ <%# IMAGE CONTAINER: Prevent shrinking %>
10
+ <div class="mr-3" style="flex-shrink: 0;">
11
+ <%= render 'spree/admin/shared/product_image', object: product_review.product, width: 40, height: 40 %>
12
+ </div>
13
+
14
+ <%# PRODUCT TITLE: Force wrap, max 2 lines %>
15
+ <%= link_to product_review.product.name,
16
+ spree.edit_admin_product_path(product_review.product),
17
+ class: "text-dark font-weight-bold",
18
+ style: "white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.3;" %>
19
+ </div>
20
+ </td>
21
+ <% end %>
22
+
23
+ <%# --- STATUS COLUMN --- %>
24
+ <td class="w-10 cursor-pointer">
3
25
  <% if product_review.approved? %>
4
- <span class="badge badge-shipped">
5
- <i class="ti ti-check"></i> Approved
26
+ <span class="badge badge-success">
27
+ <%= icon('check', class: 'mr-1') %> Approved
6
28
  </span>
7
29
  <% elsif product_review.approved == false %>
8
- <span class="badge badge-canceled">
9
- <i class="ti ti-x"></i> Disapproved
30
+ <span class="badge badge-danger">
31
+ <%= icon('x', class: 'mr-1') %> Disapproved
10
32
  </span>
11
33
  <% else %>
12
- <span class="badge badge-pending">
13
- <i class="ti ti-progress"></i> Pending
34
+ <span class="badge badge-warning text-white">
35
+ <%= icon('clock', class: 'mr-1') %> Pending
14
36
  </span>
15
37
  <% end %>
16
38
  </td>
17
39
 
18
40
  <td><%= product_review.user&.email || "Guest" %></td>
19
41
  <td><%= product_review.title %></td>
20
- <td><%= product_review.rating %></td>
21
- <td><%= l(product_review.created_at, format: :short) %></td>
22
42
  <td>
23
- <% if product_review.images.attached? %>
24
- <div data-controller="lightbox" class="flex gap-1">
25
- <% product_review.images.each do |img| %>
26
- <% next unless img.blob.present? %>
27
- <% thumb = img.variant(resize_to_limit: [50, 50], saver: { quality: 70 }) %>
43
+ <div class="text-warning">
44
+ <% product_review.rating.times do %>&#9733;<% end %>
45
+ </div>
46
+ </td>
47
+ <td><%= l(product_review.created_at.to_date, format: :short) %></td>
28
48
 
29
- <%= link_to main_app.rails_blob_path(img, only_path: true),
30
- data: {
31
- pswp_width: img.blob.metadata[:width] || 2000,
32
- pswp_height: img.blob.metadata[:height] || 2000
33
- },
34
- class: "inline-block rounded border border-gray-200 admin-review-thumb" do %>
35
- <%= image_tag main_app.rails_representation_path(thumb, only_path: true),
36
- width: 50,
37
- height: 50,
38
- class: "rounded",
39
- loading: "lazy",
40
- decoding: "async" %>
41
- <% end %>
42
- <% end %>
43
- </div>
49
+ <td>
50
+ <% if product_review.images.attached? %>
51
+ <%= icon('photo', class: 'text-primary') %> <%= product_review.images.count %>
52
+ <% else %>
53
+ <span class="text-muted">-</span>
44
54
  <% end %>
45
55
  </td>
46
56
 
47
57
  <td class="actions">
48
58
  <% if product_review.approved? %>
49
- <%= button_to "Disapprove",
50
- disapprove_admin_product_product_review_path(
51
- product_review.product,
52
- product_review
53
- ),
59
+ <%= button_to disapprove_admin_product_product_review_path(product_review.product, product_review),
54
60
  method: :get,
55
- class: "btn btn-warning btn-sm mr-2" %>
61
+ class: "btn btn-light btn-sm mr-1",
62
+ title: "Disapprove" do %>
63
+ <%= icon('x', class: 'text-danger') %>
64
+ <% end %>
56
65
  <% else %>
57
- <%= link_to Spree.t(:approve),
58
- approve_admin_product_product_review_path(
59
- product_review.product,
60
- product_review
61
- ),
66
+ <%= link_to approve_admin_product_product_review_path(product_review.product, product_review),
62
67
  method: :get,
63
- class: "btn btn-success btn-sm mr-2" %>
68
+ class: "btn btn-light btn-sm mr-1",
69
+ title: "Approve" do %>
70
+ <%= icon('check', class: 'text-success') %>
71
+ <% end %>
64
72
  <% end %>
65
73
 
66
- <% if can? :edit, product_review %>
67
- <%= link_to_edit(
68
- product_review,
69
- no_text: true,
70
- url: edit_admin_product_product_review_path(
71
- product_review.product,
72
- product_review,
73
- return_to: request.fullpath
74
- )
75
- ) %>
76
- <% end %>
74
+ <% if can? :edit, product_review %>
75
+ <%= link_to_edit(
76
+ product_review,
77
+ no_text: true,
78
+ url: edit_admin_product_product_review_path(
79
+ product_review.product,
80
+ product_review,
81
+ return_to: request.fullpath
82
+ )
83
+ ) %>
84
+ <% end %>
77
85
 
78
86
  <% if can? :destroy, product_review %>
79
87
  <%= link_to_delete(
@@ -1,8 +1,13 @@
1
- <% colspan = 6 %> <!-- Fix: define colspan -->
1
+ <%# Check if we are in the context of a specific product or the global list %>
2
+ <% global_view = @product.nil? %>
3
+ <% colspan = global_view ? 7 : 6 %>
2
4
 
3
5
  <table class="table">
4
6
  <thead>
5
7
  <tr>
8
+ <% if global_view %>
9
+ <th><%= Spree.t(:product) %></th>
10
+ <% end %>
6
11
  <th><%= Spree.t(:status) %></th>
7
12
  <th><%= Spree.t(:email) %></th>
8
13
  <th><%= Spree.t(:review_title) %></th>
@@ -16,6 +21,7 @@
16
21
  <tbody>
17
22
  <%= render partial: "spree/admin/product_reviews/product_review",
18
23
  collection: @product_reviews,
24
+ locals: { global_view: global_view },
19
25
  cached: spree_base_cache_scope %>
20
26
 
21
27
  <% if @product_reviews.empty? %>
@@ -28,4 +34,4 @@
28
34
  </tr>
29
35
  <% end %>
30
36
  </tbody>
31
- </table>
37
+ </table>
@@ -1,15 +1,88 @@
1
1
  <% content_for :page_title do %>
2
- <%= page_header_back_button spree.admin_product_path(@product) %>
3
- <span><%= Spree.t(:product_reviews) %></span>
2
+ <%= Spree.t(:product_reviews) %>
4
3
  <% end %>
5
4
 
6
- <% content_for :page_actions do %>
7
- <% end %>
5
+ <% if @product_reviews.any? %>
6
+ <div class="table-responsive">
7
+ <table class="table" id="listing_product_reviews">
8
+ <thead>
9
+ <tr>
10
+ <th><%= Spree.t(:product) %></th>
11
+ <th><%= Spree.t(:status) %></th>
12
+ <th><%= Spree.t(:user) %></th>
13
+ <th><%= Spree.t(:review) %></th>
14
+ <th><%= Spree.t(:stars) %></th>
15
+ <th><%= Spree.t(:date) %></th>
16
+ <th data-hook="admin_product_reviews_index_header_actions" class="actions"></th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @product_reviews.each do |product_review| %>
21
+ <tr id="<%= dom_id product_review %>">
22
+
23
+ <%# PRODUCT IMAGE & NAME %>
24
+ <td>
25
+ <% if product_review.product %>
26
+ <div class="d-flex align-items-center">
27
+ <div class="mr-3" style="flex-shrink: 0;">
28
+ <% if product_review.product.images.any? %>
29
+ <%= spree_image_tag product_review.product.images.first, width: 40, height: 40 %>
30
+ <% end %>
31
+ </div>
32
+ <%= link_to product_review.product.name, edit_admin_product_path(product_review.product), class: "text-dark font-weight-bold", style: "white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.3;" %>
33
+ </div>
34
+ <% end %>
35
+ </td>
36
+
37
+ <%# STATUS BADGE %>
38
+ <td class="w-10 cursor-pointer">
39
+ <% if product_review.respond_to?(:spam?) && product_review.spam? %>
40
+ <%# ORANGE BADGE FOR SPAM %>
41
+ <span class="badge badge-warning text-white" style="background-color: #fd7e14;">
42
+ <i class="ti ti-alert-triangle mr-1"></i> Spam
43
+ </span>
44
+ <% elsif product_review.approved? %>
45
+ <span class="badge badge-success">
46
+ <i class="ti ti-check mr-1"></i> Approved
47
+ </span>
48
+ <% else %>
49
+ <span class="badge badge-danger">
50
+ <i class="ti ti-x mr-1"></i> Disapproved
51
+ </span>
52
+ <% end %>
53
+ </td>
8
54
 
9
- <div class="card p-0">
10
- <div class="card-body p-0">
11
- <div class="table-responsive" id="<%= dom_id(@product, :product_reviews) %>">
12
- <%= render "spree/admin/product_reviews/table" %>
13
- </div>
55
+ <td><%= product_review.user&.email || Spree.t(:anonymous) %></td>
56
+
57
+ <td title="<%= product_review.review %>">
58
+ <%= truncate(product_review.review, length: 100) %>
59
+ </td>
60
+
61
+ <td>
62
+ <div class="text-warning">
63
+ <%= "★" * product_review.rating.to_i %>
64
+ </div>
65
+ </td>
66
+
67
+ <td><%= l product_review.created_at.to_date, format: :short_date %></td>
68
+
69
+ <td class="actions" data-hook="admin_product_reviews_index_row_actions">
70
+ <% unless product_review.approved %>
71
+ <%= link_to_with_icon 'check', Spree.t(:approve), approve_admin_product_product_review_path(product_review.product, product_review), no_text: true, class: 'btn btn-light btn-sm mr-1' %>
72
+ <% else %>
73
+ <%= link_to_with_icon 'x', Spree.t(:disapprove), disapprove_admin_product_product_review_path(product_review.product, product_review), no_text: true, class: 'btn btn-light btn-sm mr-1' %>
74
+ <% end %>
75
+
76
+ <%= link_to_edit product_review, url: edit_admin_product_product_review_path(product_review.product, product_review, return_to: request.fullpath), no_text: true, class: 'edit' %>
77
+ <%= link_to_delete product_review, url: admin_product_product_review_path(product_review.product, product_review), no_text: true %>
78
+ </td>
79
+ </tr>
80
+ <% end %>
81
+ </tbody>
82
+ </table>
83
+ </div>
84
+ <% else %>
85
+ <div class="alert alert-info no-objects-found">
86
+ <%= Spree.t(:no_resource_found, resource: Spree.t(:reviews)) %>
14
87
  </div>
15
- </div>
88
+ <% end %>