├── .github ├── stale.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ └── test.yml ├── .rspec ├── .github_changelog_generator ├── app ├── views │ └── spree │ │ ├── reviews │ │ ├── index.html.erb │ │ ├── edit.html.erb │ │ ├── new.html.erb │ │ ├── _stars.html.erb │ │ └── _form.html.erb │ │ ├── feedback_reviews │ │ ├── _summary.html.erb │ │ ├── create.js.erb │ │ └── _form.html.erb │ │ ├── shared │ │ ├── _shortrating.html.erb │ │ ├── _review_summary.html.erb │ │ ├── _rating.html.erb │ │ ├── _reviews.html.erb │ │ └── _review.html.erb │ │ └── admin │ │ ├── reviews │ │ ├── edit.html.erb │ │ ├── _form.html.erb │ │ └── index.html.erb │ │ ├── feedback_reviews │ │ └── index.html.erb │ │ └── review_settings │ │ └── edit.html.erb ├── assets │ ├── stylesheets │ │ └── spree │ │ │ ├── backend │ │ │ └── solidus_reviews.css │ │ │ └── frontend │ │ │ ├── solidus_reviews.css │ │ │ └── solidus_reviews.scss │ ├── images │ │ └── store │ │ │ └── reviews │ │ │ ├── delete.gif │ │ │ └── star.gif │ └── javascripts │ │ └── spree │ │ ├── backend │ │ └── solidus_reviews.js │ │ └── frontend │ │ └── solidus_reviews.js ├── overrides │ └── spree │ │ └── products │ │ └── show │ │ └── add_reviews_after_product_properties.html.erb.deface ├── controllers │ └── spree │ │ ├── admin │ │ ├── feedback_reviews_controller.rb │ │ ├── review_settings_controller.rb │ │ └── reviews_controller.rb │ │ ├── feedback_reviews_controller.rb │ │ └── reviews_controller.rb ├── models │ └── spree │ │ ├── permission_sets │ │ ├── review_management.rb │ │ └── review_display.rb │ │ ├── feedback_review.rb │ │ ├── reviews_ability.rb │ │ ├── reviews_configuration.rb │ │ └── review.rb ├── patches │ └── models │ │ └── solidus_reviews │ │ └── spree │ │ ├── user_patch.rb │ │ └── product_patch.rb └── helpers │ └── spree │ └── reviews_helper.rb ├── config ├── initializers │ ├── constants.rb │ ├── load_preferences.rb │ └── add_spree_reviews_to_menu.rb ├── routes.rb └── locales │ ├── zh-TW.yml │ ├── zh-CN.yml │ ├── tr.yml │ ├── en.yml │ ├── sv.yml │ ├── en-GB.yml │ ├── it.yml │ ├── pl.yml │ ├── pt-BR.yml │ ├── ru.yml │ ├── pt.yml │ ├── fr.yml │ ├── uk.yml │ ├── de.yml │ ├── es.yml │ ├── de-CH.yml │ └── ro.yml ├── lib ├── solidus_reviews │ ├── version.rb │ ├── testing_support │ │ └── factories.rb │ ├── factories │ │ ├── feedback_review_factory.rb │ │ └── review_factory.rb │ ├── configuration.rb │ └── engine.rb ├── views │ └── api │ │ └── spree │ │ └── api │ │ └── reviews │ │ ├── show.json.jbuilder │ │ ├── _feedback_review.json.jbuilder │ │ ├── index.json.jbuilder │ │ └── _review.json.jbuilder ├── solidus_reviews.rb ├── generators │ └── solidus_reviews │ │ └── install │ │ ├── templates │ │ └── initializer.rb │ │ └── install_generator.rb ├── patches │ ├── frontend │ │ └── controllers │ │ │ └── solidus_reviews │ │ │ └── spree │ │ │ └── products_controller_patch.rb │ └── api │ │ └── helpers │ │ └── solidus_reviews │ │ └── spree │ │ └── api │ │ └── api_helpers_patch.rb └── controllers │ └── spree │ └── api │ ├── feedback_reviews_controller.rb │ └── reviews_controller.rb ├── spec ├── fixtures │ └── thinking-cat.jpg ├── support │ └── config.rb ├── helpers │ └── review_helper_spec.rb ├── features │ ├── admin_spec.rb │ └── reviews_spec.rb ├── spec_helper.rb ├── controllers │ └── spree │ │ ├── admin │ │ ├── feedback_reviews_controller_spec.rb │ │ ├── reviews_controller_spec.rb │ │ └── review_settings_controller_spec.rb │ │ ├── feedback_reviews_controller_spec.rb │ │ ├── api │ │ ├── feedback_reviews_controller_spec.rb │ │ └── reviews_controller_spec.rb │ │ └── reviews_controller_spec.rb └── models │ ├── reviews_ability_spec.rb │ ├── reviews_configuration_spec.rb │ ├── product_spec.rb │ ├── feedback_review_spec.rb │ └── review_spec.rb ├── .gem_release.yml ├── CHANGELOG.md ├── bin ├── rake ├── setup ├── rails ├── rails-sandbox ├── console ├── rails-engine └── sandbox ├── .rubocop.yml ├── Rakefile ├── db ├── migrate │ ├── 20120110172331_namespace_tables.rb │ ├── 20190613165528_add_verified_purchaser_to_reviews.rb │ ├── 20110606150524_add_user_to_reviews.rb │ ├── 20140703200946_add_show_identifier_to_reviews.rb │ ├── 20110806093221_add_ip_address_to_reviews.rb │ ├── 20120712182514_add_locale_to_reviews.rb │ ├── 20120712182627_add_locale_to_feedback_reviews.rb │ ├── 20081020220724_create_reviews.rb │ ├── 20101222083309_create_feedback_reviews.rb │ ├── 20120123141326_recalculate_ratings.rb │ └── 20110406083603_add_rating_to_products.rb └── sample │ ├── ratings.yml │ └── reviews.yml ├── .gitignore ├── Gemfile ├── LICENSE ├── solidus_reviews.gemspec ├── README.md ├── .rubocop_todo.yml └── OLD_CHANGELOG.md /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | issues=false 2 | exclude-labels=infrastructure 3 | -------------------------------------------------------------------------------- /app/views/spree/reviews/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'spree/shared/reviews' %> 2 | -------------------------------------------------------------------------------- /config/initializers/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | NB_STARS = 5 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/solidus_reviews.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/backend 3 | */ -------------------------------------------------------------------------------- /lib/solidus_reviews/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | VERSION = '1.8.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/fixtures/thinking-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio-contrib/solidus_reviews/HEAD/spec/fixtures/thinking-cat.jpg -------------------------------------------------------------------------------- /app/assets/images/store/reviews/delete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio-contrib/solidus_reviews/HEAD/app/assets/images/store/reviews/delete.gif -------------------------------------------------------------------------------- /app/assets/images/store/reviews/star.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solidusio-contrib/solidus_reviews/HEAD/app/assets/images/store/reviews/star.gif -------------------------------------------------------------------------------- /lib/views/api/spree/api/reviews/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.partial!("spree/api/reviews/review", review: @review) 4 | -------------------------------------------------------------------------------- /.gem_release.yml: -------------------------------------------------------------------------------- 1 | bump: 2 | recurse: false 3 | file: 'lib/solidus_reviews/version.rb' 4 | message: Bump SolidusReviews to %{version} 5 | tag: true 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See https://github.com/solidusio-contrib/solidus_reviews/releases or [OLD_CHANGELOG.md](OLD_CHANGELOG.md) for older versions. 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "rubygems" 5 | require "bundler/setup" 6 | 7 | load Gem.bin_path("rake", "rake") 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | gem install bundler --conservative 7 | bundle update 8 | bin/rake clobber 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - solidus_dev_support/rubocop 5 | 6 | AllCops: 7 | TargetRubyVersion: 3.0 8 | NewCops: disable 9 | -------------------------------------------------------------------------------- /spec/support/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.before do 5 | Spree::Reviews::Config.reset 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | if %w[g generate].include? ARGV.first 4 | exec "#{__dir__}/rails-engine", *ARGV 5 | else 6 | exec "#{__dir__}/rails-sandbox", *ARGV 7 | end 8 | -------------------------------------------------------------------------------- /app/overrides/spree/products/show/add_reviews_after_product_properties.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render "spree/shared/reviews" %> 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require 'solidus_dev_support/rake_tasks' 5 | SolidusDevSupport::RakeTasks.install 6 | 7 | task default: 'extension:specs' 8 | -------------------------------------------------------------------------------- /app/views/spree/reviews/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag :h2, I18n.t("spree.leave_us_a_review_for", name: @product.name), class: 'new-review-title' %> 2 | 3 | <%= render 'form', review: @review, product: @product %> 4 | -------------------------------------------------------------------------------- /app/views/spree/reviews/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= content_tag :h2, I18n.t("spree.leave_us_a_review_for", name: @product.name), class: 'new-review-title' %> 2 | 3 | <%= render 'form', review: @review, product: @product %> 4 | -------------------------------------------------------------------------------- /lib/views/api/spree/api/reviews/_feedback_review.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.cache! [I18n.locale, feedback_review] do 4 | json.call(feedback_review, *feedback_review_attributes) 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/solidus_reviews.css: -------------------------------------------------------------------------------- 1 | /* 2 | Placeholder manifest file. 3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css' 4 | */ 5 | -------------------------------------------------------------------------------- /lib/solidus_reviews/testing_support/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "factory_bot" 4 | 5 | Dir["#{File.dirname(__FILE__)}/../factories/**"].each do |f| 6 | require File.expand_path(f) 7 | end 8 | -------------------------------------------------------------------------------- /lib/views/api/spree/api/reviews/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.reviews(@reviews) do |review| 4 | json.partial!("spree/api/reviews/review", review: review) 5 | end 6 | json.avg_rating(@product&.avg_rating) 7 | -------------------------------------------------------------------------------- /lib/solidus_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_core' 4 | require 'solidus_reviews/configuration' 5 | require 'solidus_support' 6 | require 'deface' 7 | 8 | require 'solidus_reviews/version' 9 | require 'solidus_reviews/engine' 10 | -------------------------------------------------------------------------------- /config/initializers/load_preferences.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module Reviews 5 | end 6 | end 7 | 8 | Rails.application.reloader.to_prepare do 9 | Spree::Reviews.const_set(:Config, Spree::ReviewsConfiguration.new) 10 | end 11 | -------------------------------------------------------------------------------- /lib/solidus_reviews/factories/feedback_review_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :feedback_review, class: Spree::FeedbackReview do |_f| 5 | user 6 | review 7 | rating { rand(1..5) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20120110172331_namespace_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class NamespaceTables < SolidusSupport::Migration[4.2] 4 | def change 5 | rename_table :reviews, :spree_reviews 6 | rename_table :feedback_reviews, :spree_feedback_reviews 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20190613165528_add_verified_purchaser_to_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddVerifiedPurchaserToReviews < SolidusSupport::Migration[4.2] 4 | def change 5 | add_column :spree_reviews, :verified_purchaser, :boolean, default: false 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/solidus_reviews/install/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | SolidusReviews.configure do |config| 4 | # TODO: Remember to change this with the actual preferences you have implemented! 5 | # config.sample_preference = 'sample_value' 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/solidus_reviews.js: -------------------------------------------------------------------------------- 1 | //= require jquery.rating 2 | //= require spree/backend 3 | 4 | // Navigating to a page with ratings via TurboLinks shows the radio buttons 5 | $(document).on('page:load', function () { 6 | $('input[type=radio].star').rating(); 7 | }); 8 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/feedback_reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::Admin::FeedbackReviewsController < Spree::Admin::ResourceController 4 | belongs_to 'spree/review' 5 | def index 6 | @collection = parent.feedback_reviews 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | \#* 3 | *~ 4 | .#* 5 | .DS_Store 6 | .idea 7 | .project 8 | .sass-cache 9 | coverage 10 | Gemfile.lock 11 | Gemfile-local 12 | tmp 13 | nbproject 14 | pkg 15 | *.swp 16 | spec/dummy 17 | spec/examples.txt 18 | /sandbox 19 | .rvmrc 20 | .ruby-version 21 | .ruby-gemset 22 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/solidus_reviews.js: -------------------------------------------------------------------------------- 1 | //= require jquery.rating 2 | //= require spree/frontend 3 | 4 | // Navigating to a page with ratings via TurboLinks shows the radio buttons 5 | $(document).on('page:load', function () { 6 | $('input[type=radio].star').rating(); 7 | }); 8 | -------------------------------------------------------------------------------- /app/models/spree/permission_sets/review_management.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module PermissionSets 5 | class ReviewManagement < PermissionSets::Base 6 | def activate! 7 | can :manage, Spree::Review 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/spree/permission_sets/review_display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module PermissionSets 5 | class ReviewDisplay < PermissionSets::Base 6 | def activate! 7 | can [:display, :admin], Spree::Review 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/sample/ratings.yml: -------------------------------------------------------------------------------- 1 | t1: 2 | product: ror_jr_spaghetti 3 | value: 3.56 4 | count: 20 5 | t2: 6 | product: ror_bag 7 | value: 2.56 8 | count: 30 9 | t3: 10 | product: ror_tote 11 | value: 3.94 12 | count: 10 13 | t4: 14 | product: apache_baseball_jersey 15 | value: 1.49 16 | count: 6 17 | -------------------------------------------------------------------------------- /db/migrate/20110606150524_add_user_to_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddUserToReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | add_column :reviews, :user_id, :integer, null: true 6 | end 7 | 8 | def self.down 9 | remove_column :reviews, :user_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140703200946_add_show_identifier_to_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddShowIdentifierToReviews < SolidusSupport::Migration[4.2] 4 | def change 5 | add_column :spree_reviews, :show_identifier, :boolean, default: true 6 | add_index :spree_reviews, :show_identifier 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/spree/feedback_reviews/_summary.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= I18n.t('spree.voice', count: review.feedback_reviews.count) %>. 3 | 4 | <%= render 'spree/reviews/stars', stars: review.feedback_stars %> 5 |   6 |

7 | -------------------------------------------------------------------------------- /db/migrate/20110806093221_add_ip_address_to_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIpAddressToReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | add_column :reviews, :ip_address, :string 6 | end 7 | 8 | def self.down 9 | remove_column :reviews, :ip_address 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20120712182514_add_locale_to_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLocaleToReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | add_column :spree_reviews, :locale, :string, default: 'en' 6 | end 7 | 8 | def self.down 9 | remove_column :spree_reviews, :locale 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20120712182627_add_locale_to_feedback_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLocaleToFeedbackReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | add_column :spree_feedback_reviews, :locale, :string, default: 'en' 6 | end 7 | 8 | def self.down 9 | remove_column :spree_feedback_reviews, :locale 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/spree/shared/_shortrating.html.erb: -------------------------------------------------------------------------------- 1 | <% stars = product.stars %> 2 | <% reviews_count = product.reviews_count %> 3 | 4 | <%= render 'spree/reviews/stars', stars: stars %> 5 | 6 | 7 | <%= Spree::Review.model_name.human(count: reviews_count) %> 8 | 9 |
10 | -------------------------------------------------------------------------------- /app/patches/models/solidus_reviews/spree/user_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | module Spree 5 | module UserPatch 6 | def self.prepended(base) 7 | base.class_eval do 8 | has_many :reviews, class_name: 'Spree::Review' 9 | end 10 | end 11 | 12 | ::Spree.user_class.prepend self 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/rails-sandbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | app_root = 'sandbox' 4 | 5 | unless File.exist? "#{app_root}/bin/rails" 6 | warn 'Creating the sandbox app...' 7 | Dir.chdir "#{__dir__}/.." do 8 | system "#{__dir__}/sandbox" or begin 9 | warn 'Automatic creation of the sandbox app failed' 10 | exit 1 11 | end 12 | end 13 | end 14 | 15 | Dir.chdir app_root 16 | exec 'bin/rails', *ARGV 17 | -------------------------------------------------------------------------------- /lib/patches/frontend/controllers/solidus_reviews/spree/products_controller_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | module Spree 5 | module ProductsControllerPatch 6 | def self.prepended(base) 7 | base.class_eval do 8 | helper ::Spree::ReviewsHelper 9 | end 10 | end 11 | 12 | ::Spree::ProductsController.prepend self 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/views/api/spree/api/reviews/_review.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.cache! [I18n.locale, review] do 4 | json.call(review, *review_attributes) 5 | json.images(review.images) do |image| 6 | json.partial!("spree/api/images/image", image: image) 7 | end 8 | json.feedback_reviews(review.feedback_reviews) do |feedback_review| 9 | json.partial!("spree/api/reviews/feedback_review", feedback_review: feedback_review) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/spree/reviews/_stars.html.erb: -------------------------------------------------------------------------------- 1 | <% if defined? edit_enabled 2 | state = "" 3 | name = "review[rating]" 4 | else 5 | state = 'disabled' 6 | name = defined?(review).nil? ? Time.now.tv_usec.to_s : "review_#{review}" 7 | end %> 8 | <% for i in 1..NB_STARS %> 9 | 12 | <%= "checked" if i == stars %> /> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/solidus_reviews/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | class Configuration 5 | # Define here the settings for this extension, e.g.: 6 | # 7 | # attr_accessor :my_setting 8 | end 9 | 10 | class << self 11 | def configuration 12 | @configuration ||= Configuration.new 13 | end 14 | 15 | alias config configuration 16 | 17 | def configure 18 | yield configuration 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/spree/feedback_reviews/create.js.erb: -------------------------------------------------------------------------------- 1 | <% if @feedback_review.valid? %> 2 | $("div#feedback_review_<%= @review.id %>").html("<%= escape_javascript(render('spree/feedback_reviews/summary', review: @review)) %>"); 3 | <% else %> 4 | $("div#feedback_review_<%= @review.id %>").html("<%= escape_javascript(render('spree/feedback_reviews/form', review: @review)) %>"); 5 | 6 | <% end %> 7 | $(document).ready(function(){$("div#feedback_review_<%= @review.id %>").find(".star").rating({required:true});}); 8 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require "bundler/setup" 6 | require "solidus_reviews" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"]) 11 | 12 | # (If you use this, don't forget to add pry to your Gemfile!) 13 | # require "pry" 14 | # Pry.start 15 | 16 | require "irb" 17 | IRB.start(__FILE__) 18 | -------------------------------------------------------------------------------- /db/migrate/20081020220724_create_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | create_table :reviews do |t| 6 | t.integer :product_id 7 | t.string :name 8 | t.string :location 9 | t.integer :rating 10 | t.text :title 11 | t.text :review 12 | t.boolean :approved, default: false 13 | t.timestamps 14 | end 15 | end 16 | 17 | def self.down 18 | drop_table :reviews 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /bin/rails-engine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('..', __dir__) 6 | ENGINE_PATH = File.expand_path('../lib/solidus_reviews/engine', __dir__) 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 11 | 12 | require 'rails/all' 13 | require 'rails/engine/commands' 14 | -------------------------------------------------------------------------------- /db/migrate/20101222083309_create_feedback_reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateFeedbackReviews < SolidusSupport::Migration[4.2] 4 | def self.up 5 | create_table :feedback_reviews do |t| 6 | t.integer :user_id 7 | t.integer :review_id, null: false 8 | t.integer :rating, default: 0 9 | t.text :comment 10 | t.timestamps 11 | end 12 | add_index :feedback_reviews, :review_id 13 | add_index :feedback_reviews, :user_id 14 | end 15 | 16 | def self.down 17 | drop_table :feedback_reviews 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/spree/shared/_review_summary.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render 'spree/shared/rating', locals: @product, review: 0 %> 3 | <% for review in (Spree::Reviews::Config[:track_locale] ? @product.reviews.localized(I18n.locale) : @product.reviews).default_approval_filter.preview %> 4 | <%= render 'spree/shared/review', review: review %> 5 | <% end %> 6 | <% if Spree::Reviews::Config[:feedback_rating] && (!Spree::Reviews::Config[:require_login] || spree_current_user) %> 7 | <%= link_to I18n.t("spree.write_your_own_review"), new_product_review_path(@product), class: "button" %> 8 | <% end %> 9 |
10 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | concurrency: 6 | group: lint-${{ github.ref_name }} 7 | cancel-in-progress: ${{ github.ref_name != 'main' }} 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | ruby: 14 | name: Check Ruby 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | - name: Install Ruby and gems 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: "3.2" 23 | bundler-cache: true 24 | - name: Lint Ruby files 25 | run: bundle exec rubocop -ESP 26 | -------------------------------------------------------------------------------- /lib/solidus_reviews/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'solidus_core' 4 | require 'solidus_support' 5 | 6 | module SolidusReviews 7 | class Engine < Rails::Engine 8 | include SolidusSupport::EngineExtensions 9 | 10 | isolate_namespace ::Spree 11 | 12 | engine_name 'solidus_reviews' 13 | 14 | # use rspec for tests 15 | config.generators do |g| 16 | g.test_framework :rspec 17 | end 18 | 19 | config.to_prepare do 20 | ::Spree::Ability.register_ability(::Spree::ReviewsAbility) 21 | end 22 | 23 | if SolidusSupport.api_available? 24 | paths["app/controllers"] << "lib/controllers" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20120123141326_recalculate_ratings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RecalculateRatings < SolidusSupport::Migration[4.2] 4 | def up 5 | Spree::Product.reset_column_information 6 | 7 | Spree::Product.update_all reviews_count: 0 8 | 9 | Spree::Product.joins(:reviews).where("spree_reviews.id IS NOT NULL").find_each do |p| 10 | Spree::Product.update_counters p.id, reviews_count: p.reviews.approved.length 11 | 12 | # recalculate_product_rating exists on the review, not the product 13 | if p.reviews.approved.count > 0 14 | p.reviews.approved.first.recalculate_product_rating 15 | end 16 | end 17 | end 18 | 19 | def down; end 20 | end 21 | -------------------------------------------------------------------------------- /app/views/spree/shared/_rating.html.erb: -------------------------------------------------------------------------------- 1 | <% stars = product.stars %> 2 | <% reviews_count = product.reviews_count %> 3 |
4 |

<%= I18n.t('spree.average_customer_rating') %>:

5 |

6 | 7 | <%= render 'spree/reviews/stars', stars: stars %> 8 | 9 |

10 |

(<%= I18n.t('spree.based_upon_review_count', count: reviews_count) %>)

11 |
12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /app/views/spree/feedback_reviews/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render 'spree/feedback_reviews/summary', review: review %> 2 | <%= form_for((@feedback_review ||= review.feedback_reviews.build), url: feedback_reviews_path(review), method: :post, remote: true) do |f| %> 3 | <% unless @feedback_review.errors.empty? %> 4 | <%= @feedback_review.errors[:rating] %> 5 |
6 | <% end %> 7 | <%= I18n.t("spree.was_this_review_helpful") %> 8 | <% for i in 1..NB_STARS %> 9 | <%= radio_button_tag "feedback_review[rating]", 10 | I18n.t("spree.star", count: i), false, class: "star" %> 11 | <% end %> 12 | 13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/solidus_reviews/factories/review_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :review, class: Spree::Review do |_f| 5 | sequence(:name) { |i| "User #{i}" } 6 | title { FFaker::Book.title } 7 | review { 'This product is ok!' } 8 | rating { rand(1..5) } 9 | approved { false } 10 | show_identifier { true } 11 | user 12 | product 13 | 14 | trait :approved do 15 | approved { true } 16 | end 17 | 18 | trait :hide_identifier do 19 | show_identifier { false } 20 | end 21 | 22 | trait :with_image do 23 | images { 24 | [ 25 | FactoryBot.create(:image) 26 | ] 27 | } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/helpers/review_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::ReviewsHelper do 6 | context 'star' do 7 | specify do 8 | expect(star('a_class')).to eq '' 9 | end 10 | end 11 | 12 | context 'mk_stars' do 13 | specify do 14 | matches = mk_stars(2).scan(/unlit/) 15 | expect(matches.length).to eq 3 16 | end 17 | end 18 | 19 | context 'txt_stars' do 20 | specify do 21 | expect(txt_stars(2, true)).to eq '2 out of 5' 22 | end 23 | 24 | specify do 25 | expect(txt_stars(3, false)).to be_a String 26 | expect(txt_stars(3, false)).to eq('3') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/review_settings_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::Admin::ReviewSettingsController < Spree::Admin::BaseController 4 | before_action :process_unset_checkboxes, only: [:update] 5 | 6 | def update 7 | Spree::Reviews::Config.set(params[:preferences]) 8 | 9 | respond_to do |format| 10 | format.html do 11 | redirect_to edit_admin_review_settings_path 12 | end 13 | end 14 | end 15 | 16 | def process_unset_checkboxes 17 | # workaround for unset checkbox behaviour 18 | params[:preferences] ||= {} 19 | Spree::ReviewsConfiguration.boolean_preferences.each do |sym| 20 | params[:preferences][sym] = false if params[:preferences][sym].blank? 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/spree/feedback_review.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::FeedbackReview < Spree::Base 4 | belongs_to :user, class_name: Spree.user_class.to_s, optional: true 5 | 6 | belongs_to :review, touch: true 7 | validates :review, presence: true 8 | 9 | validates :rating, numericality: { only_integer: true, 10 | greater_than_or_equal_to: 1, 11 | less_than_or_equal_to: 5, 12 | message: :you_must_enter_value_for_rating } 13 | 14 | scope :most_recent_first, -> { order("spree_feedback_reviews.created_at DESC") } 15 | default_scope { most_recent_first } 16 | 17 | scope :localized, lambda { |lc| where('spree_feedback_reviews.locale = ?', lc) } 18 | end 19 | -------------------------------------------------------------------------------- /app/views/spree/admin/reviews/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= I18n.t("spree.editing_review_for_html", product_name: @review.product.name) %> 3 | <% end %> 4 | 5 | <%= form_for([:admin, @review]) do |f| %> 6 |
7 | <% unless @review.title.blank? %> 8 |

<%= @review.title %>

9 | <% end %> 10 |

11 | <%= simple_format(@review.review) %> 12 |

13 |
14 | 15 |
16 | <%= render 'form', f: f %> 17 |
18 | 19 |
20 | 21 |
22 | <% if can? :manage, Spree::Review %> 23 | <%= render 'spree/admin/shared/edit_resource_links' %> 24 | <% end %> 25 |
26 | <% end %> 27 | -------------------------------------------------------------------------------- /app/patches/models/solidus_reviews/spree/product_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | module Spree 5 | module ProductPatch 6 | def self.prepended(base) 7 | base.class_eval do 8 | has_many :reviews 9 | end 10 | end 11 | 12 | def stars 13 | avg_rating.try(:round) || 0 14 | end 15 | 16 | def recalculate_rating 17 | reviews_count = reviews.reload.default_approval_filter.count 18 | 19 | self.reviews_count = reviews_count 20 | self.avg_rating = if reviews_count > 0 21 | '%.1f' % (reviews.default_approval_filter.sum(:rating).to_f / reviews_count) 22 | else 23 | 0 24 | end 25 | save 26 | end 27 | 28 | ::Spree::Product.prepend self 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/spree/shared/_reviews.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
<%= I18n.t("spree.reviews") %>
3 | <% if Spree::Reviews::Config[:include_unapproved_reviews] == false and @product.reviews.approved.count == 0 %> 4 |

<%= I18n.t("spree.no_reviews_available") %>

5 | <% else %> 6 | <%= render 'spree/shared/rating', product: @product, review: 0 %> 7 | <% for review in (Spree::Reviews::Config[:track_locale] ? @product.reviews.localized(I18n.locale) : @product.reviews).default_approval_filter.preview %> 8 | <%= render 'spree/shared/review', review: review %> 9 | <% end %> 10 | <% end %> 11 | <% if !Spree::Reviews::Config[:require_login] || spree_current_user %> 12 | <%= link_to I18n.t("spree.write_your_own_review"), new_product_review_path(@product), class: "button", 13 | rel: 'nofollow' %> 14 | <% end %> 15 |
16 | -------------------------------------------------------------------------------- /app/helpers/spree/reviews_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree::ReviewsHelper 4 | def star(the_class) 5 | tag.span(" ✮ ".html_safe, class: the_class) 6 | end 7 | 8 | def mk_stars(m) 9 | (1..5).collect { |n| n <= m ? star("lit") : star("unlit") }.join 10 | end 11 | 12 | def txt_stars(n, show_out_of = true) 13 | res = I18n.t('spree.star', count: n) 14 | res += " #{I18n.t('spree.out_of_5')}" if show_out_of 15 | res 16 | end 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 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20110406083603_add_rating_to_products.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddRatingToProducts < SolidusSupport::Migration[4.2] 4 | def self.up 5 | if table_exists?('products') 6 | add_column :products, :avg_rating, :decimal, default: 0.0, null: false, precision: 7, scale: 5 7 | add_column :products, :reviews_count, :integer, default: 0, null: false 8 | elsif table_exists?('spree_products') 9 | add_column :spree_products, :avg_rating, :decimal, default: 0.0, null: false, precision: 7, scale: 5 10 | add_column :spree_products, :reviews_count, :integer, default: 0, null: false 11 | end 12 | end 13 | 14 | def self.down 15 | if table_exists?('products') 16 | remove_column :products, :reviews_count 17 | remove_column :products, :avg_rating 18 | elsif table_exists?('spree_products') 19 | remove_column :spree_products, :reviews_count 20 | remove_column :spree_products, :avg_rating 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/features/admin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'Review Admin' do 6 | stub_authorization! 7 | 8 | let!(:review) { create(:review) } 9 | 10 | context 'index' do 11 | before do 12 | visit spree.admin_reviews_path 13 | end 14 | 15 | it 'list reviews' do 16 | expect(page).to have_text review.product.name 17 | end 18 | 19 | it 'approve reviews' do 20 | expect(review.approved).to be false 21 | within("tr#review_#{review.id}") do 22 | find('.approve').click 23 | end 24 | expect(review.reload.approved).to be true 25 | end 26 | 27 | it 'edit reviews' do 28 | expect(page).to have_text review.product.name 29 | within("tr#review_#{review.id}") do 30 | find('.edit').click 31 | end 32 | 33 | expect(page).to have_text 'Editing' 34 | expect(page).to have_text review.title 35 | expect(page).to have_css('a', text: review.email) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::Admin::ReviewsController < Spree::Admin::ResourceController 4 | helper Spree::ReviewsHelper 5 | 6 | def index 7 | @reviews = collection 8 | end 9 | 10 | def approve 11 | review = Spree::Review.find(params[:id]) 12 | 13 | if review.update_attribute(:approved, true) 14 | flash[:success] = I18n.t('spree.info_approve_review') 15 | else 16 | flash[:error] = I18n.t('spree.error_approve_review') 17 | end 18 | 19 | redirect_to admin_reviews_path 20 | end 21 | 22 | def edit 23 | if @review.product.nil? 24 | flash[:error] = I18n.t('spree.error_no_product') 25 | redirect_to admin_reviews_path 26 | end 27 | end 28 | 29 | private 30 | 31 | def collection 32 | params[:q] ||= {} 33 | 34 | @search = Spree::Review.ransack(params[:q]) 35 | @collection = @search.result.includes([:product, :user, :feedback_reviews]).page(params[:page]).per(params[:per_page]) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/spree/reviews_ability.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::ReviewsAbility 4 | include CanCan::Ability 5 | 6 | def initialize(user) 7 | review_ability_class = self.class 8 | 9 | can :create, Spree::Review do |_review| 10 | review_ability_class.allow_anonymous_reviews? || user.email.present? 11 | end 12 | 13 | can :create, Spree::FeedbackReview do |_review| 14 | review_ability_class.allow_anonymous_reviews? || user.email.present? 15 | end 16 | 17 | # You can only change your own feedback_review 18 | can [:update, :destroy], Spree::FeedbackReview do |feedback_review| 19 | feedback_review.user == user 20 | end 21 | 22 | # You can read your own reviews, and everyone can read approved ones 23 | can :read, Spree::Review do |review| 24 | review.user == user || review.approved? 25 | end 26 | 27 | # You can only change your own review 28 | can [:update, :destroy], Spree::Review do |review| 29 | review.user == user 30 | end 31 | end 32 | 33 | def self.allow_anonymous_reviews? 34 | !Spree::Reviews::Config[:require_login] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Spree::Core::Engine.routes.draw do 4 | namespace :admin do 5 | resources :reviews, only: [:index, :destroy, :edit, :update] do 6 | member do 7 | get :approve 8 | end 9 | resources :feedback_reviews, only: [:index, :destroy] 10 | resources :images, only: [:destroy] 11 | end 12 | resource :review_settings, only: [:edit, :update] 13 | end 14 | 15 | resources :products, only: [] do 16 | resources :reviews, only: [:index, :new, :create, :edit, :update] do 17 | end 18 | end 19 | post '/reviews/:review_id/feedback(.:format)' => 'feedback_reviews#create', as: :feedback_reviews 20 | 21 | if SolidusSupport.api_available? 22 | namespace :api, defaults: { format: 'json' } do 23 | resources :reviews, only: [:show, :create, :update, :destroy] 24 | 25 | resources :feedback_reviews, only: [:create, :update, :destroy] 26 | 27 | resources :products do 28 | resources :reviews, only: [:index] 29 | end 30 | 31 | resources :users do 32 | resources :reviews, only: [:index] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/patches/api/helpers/solidus_reviews/spree/api/api_helpers_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | module Spree 5 | module Api 6 | module ApiHelpersPatch 7 | def self.prepended(base) 8 | base.module_eval do 9 | # rubocop:disable Style/ClassVars 10 | @@review_attributes = [ 11 | :id, :product_id, :name, :location, :rating, :title, :review, :approved, 12 | :created_at, :updated_at, :user_id, :ip_address, :locale, :show_identifier, 13 | :verified_purchaser 14 | ] 15 | 16 | @@feedback_review_attributes = [ 17 | :id, :user_id, :review_id, :rating, :comment, :created_at, :updated_at, :locale 18 | ] 19 | # rubocop:enable Style/ClassVars 20 | 21 | def review_attributes 22 | @@review_attributes 23 | end 24 | 25 | def feedback_review_attributes 26 | @@feedback_review_attributes 27 | end 28 | end 29 | end 30 | ::Spree::Api::ApiHelpers.prepend self 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Configure Rails Environment 4 | ENV['RAILS_ENV'] = 'test' 5 | 6 | require 'rails-controller-testing' 7 | Rails::Controller::Testing.install 8 | 9 | # Run Coverage report 10 | require 'solidus_dev_support/rspec/coverage' 11 | 12 | # Create the dummy app if it's still missing. 13 | dummy_env = "#{__dir__}/dummy/config/environment.rb" 14 | system 'bin/rake extension:test_app' unless File.exist? dummy_env 15 | require dummy_env 16 | 17 | # Requires factories and other useful helpers defined in spree_core. 18 | require 'solidus_dev_support/rspec/feature_helper' 19 | 20 | # Requires supporting ruby files with custom matchers and macros, etc, 21 | # in spec/support/ and its subdirectories. 22 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f } 23 | 24 | # Requires factories defined in Solidus core and this extension. 25 | # See: lib/solidus_reviews/testing_support/factories.rb 26 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusReviews::Engine) 27 | 28 | RSpec.configure do |config| 29 | config.infer_spec_type_from_file_location! 30 | config.use_transactional_fixtures = false 31 | 32 | if Spree.solidus_gem_version < Gem::Version.new('2.11') 33 | config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :system 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /config/initializers/add_spree_reviews_to_menu.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.config.after_initialize do 4 | Spree::Backend::Config.configure do |config| 5 | config.menu_items = config.menu_items.map do |item| 6 | if item.label.to_sym == :settings 7 | # The API of the MenuItem class changes in Solidus 4.2.0 8 | if item.respond_to?(:children) 9 | item.children << Spree::BackendConfiguration::MenuItem.new( 10 | label: :reviews, 11 | condition: -> { can?(:admin, Spree::ReviewsConfiguration) }, 12 | url: -> { Spree::Core::Engine.routes.url_helpers.edit_admin_review_settings_path }, 13 | match_path: /review_settings/ 14 | ) 15 | else 16 | item.sections << :reviews 17 | end 18 | elsif item.label.to_sym == :products 19 | if item.respond_to?(:children) 20 | item.children << Spree::BackendConfiguration::MenuItem.new( 21 | label: :reviews, 22 | condition: -> { can?(:admin, Spree::Review) }, 23 | url: -> { Spree::Core::Engine.routes.url_helpers.admin_reviews_path }, 24 | match_path: /reviews/ 25 | ) 26 | else 27 | item.sections << :reviews 28 | end 29 | end 30 | item 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/spree/feedback_reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::FeedbackReviewsController < Spree::StoreController 4 | helper Spree::BaseHelper 5 | 6 | before_action :sanitize_rating, only: [:create] 7 | before_action :load_review, only: [:create] 8 | 9 | def create 10 | if @review.present? 11 | @feedback_review = @review.feedback_reviews.new(feedback_review_params) 12 | @feedback_review.user = spree_current_user 13 | @feedback_review.locale = I18n.locale.to_s if Spree::Reviews::Config[:track_locale] 14 | authorize! :create, @feedback_review 15 | @feedback_review.save 16 | end 17 | 18 | respond_to do |format| 19 | format.html { redirect_back(fallback_location: root_path) } 20 | format.js { render action: :create } 21 | end 22 | end 23 | 24 | protected 25 | 26 | def load_review 27 | @review ||= Spree::Review.find_by!(id: params[:review_id]) 28 | end 29 | 30 | def permitted_feedback_review_attributes 31 | [:rating, :comment] 32 | end 33 | 34 | def feedback_review_params 35 | params.require(:feedback_review).permit(permitted_feedback_review_attributes) 36 | end 37 | 38 | def sanitize_rating 39 | params[:feedback_review][:rating].to_s.sub!(/\s*[^0-9]*\z/, '') unless params[:feedback_review] && params[:feedback_review][:rating].blank? 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/spree/admin/feedback_reviews/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= I18n.t("spree.feedback_review_for", review: @review.title) %> 3 | <% end %> 4 | 5 | <% content_for :page_actions do %> 6 |
  • <%= link_to I18n.t("spree.back_to_reviews"), admin_reviews_path %>
  • 7 | <% end %> 8 | 9 | <% if @collection.any? %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <%- @collection.each do |feedback| -%> 27 | 28 | 29 | 30 | 31 | 34 | 35 | <% end %> 36 | 37 |
    <%= I18n.t("spree.user") %><%= I18n.t("spree.stars") %><%= I18n.t("spree.date") %>
    <%= l feedback.created_at %><%= feedback.user.try(:login) || I18n.t("spree.anonymous") %><%= feedback.rating %> 32 | <%= link_to_delete feedback, no_text: true %> 33 |
    38 | <% else %> 39 |
    40 | <%= I18n.t("spree.no_results") %> 41 |
    42 | <% end %> 43 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/feedback_reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Admin::FeedbackReviewsController do 6 | stub_authorization! 7 | 8 | before do 9 | user = create(:admin_user) 10 | allow(controller).to receive(:spree_current_user).and_return(user) 11 | end 12 | 13 | describe '#index' do 14 | let!(:review) { create(:review) } 15 | let!(:other_review) { create(:review) } 16 | 17 | let!(:feedback_review_1) { create(:feedback_review, created_at: 10.days.ago, review: review) } 18 | let!(:feedback_review_2) { create(:feedback_review, created_at: 2.days.ago, review: review) } 19 | let!(:feedback_review_3) { create(:feedback_review, created_at: 5.days.ago, review: review) } 20 | 21 | let!(:other_feedback_review_1) { create(:feedback_review, created_at: 10.days.ago, review: other_review) } 22 | let!(:other_feedback_review_2) { create(:feedback_review, created_at: 2.days.ago, review: other_review) } 23 | 24 | it 'looks up feedback reviews for the specified review and renders the template' do 25 | get :index, params: { review_id: review.id } 26 | expect(response.status).to eq(200) 27 | expect(response).to render_template(:index) 28 | expect(assigns(:collection)).to eq([feedback_review_2, feedback_review_3, feedback_review_1]) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/spree/admin/reviews/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "spree/shared/error_messages", target: @review %> 2 | <%= f.field_container :name do %> 3 | <%= f.label :name %>
    4 | <%= f.text_field :name, maxlength: "255", size: "60" %> 5 | <% end %> 6 | 7 | <%= f.field_container :email do %> 8 | <%= f.label :email %>
    9 | <%= mail_to @review.email %> 10 | <% end %> 11 | 12 | <%= f.field_container :title do %> 13 | <%= f.label :title %>
    14 | <%= f.text_field :title, maxlength: "255", size: "60" %> 15 | <% end %> 16 | 17 | <%= f.field_container :review do %> 18 | <%= f.label :review %>
    19 | <%= f.text_area :review, wrap: "virtual", rows: "10", cols: "60" %> 20 | <% end %> 21 | 22 | <%= f.field_container :images do %> 23 | <%= f.label :images %> 24 | 25 | <% @review.images.each do |image| %> 26 | 27 | 30 | 33 | 34 | <% end %> 35 |
    28 | <%= image_tag image.url(:original) %> 29 | 31 | <%= link_to_delete image, url: admin_review_image_url(@review, image, product_id: @review.product), no_text: true %> 32 |
    36 | <% end %> 37 | 38 | <% if Spree::Reviews::Config[:track_locale] %> 39 | <%= f.field_container :locale do %> 40 | <%= f.label :locale %>
    41 | <%= f.select :locale, I18n.available_locales.map { |lc| [t(lc, default: lc.to_s), lc.to_s] } %> 42 | <% end %> 43 | <% end %> 44 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | branch = ENV.fetch('SOLIDUS_BRANCH', 'main') 7 | gem 'solidus', github: 'solidusio/solidus', branch: branch 8 | 9 | # The solidus_frontend gem has been pulled out since v3.2 10 | if branch >= 'v3.2' 11 | gem 'solidus_frontend' 12 | elsif branch == 'main' 13 | gem 'solidus_frontend', github: 'solidusio/solidus_frontend', branch: branch 14 | else 15 | gem 'solidus_frontend', github: 'solidusio/solidus', branch: branch 16 | end 17 | 18 | # Needed to help Bundler figure out how to resolve dependencies, 19 | # otherwise it takes forever to resolve them. 20 | # See https://github.com/bundler/bundler/issues/6677 21 | rails_version = ENV.fetch('RAILS_VERSION', '7.2') 22 | gem 'rails', "~> #{rails_version}" 23 | 24 | case ENV.fetch('DB', nil) 25 | when 'mysql' 26 | gem 'mysql2' 27 | when 'postgresql' 28 | gem 'pg' 29 | else 30 | gem 'sqlite3', '~> 1.7' 31 | end 32 | 33 | gemspec 34 | 35 | # Use a local Gemfile to include development dependencies that might not be 36 | # relevant for the project or for other contributors, e.g. pry-byebug. 37 | # 38 | # We use `send` instead of calling `eval_gemfile` to work around an issue with 39 | # how Dependabot parses projects: https://github.com/dependabot/dependabot-core/issues/1658. 40 | send(:eval_gemfile, 'Gemfile-local') if File.exist? 'Gemfile-local' 41 | 42 | # Necessary for Ruby 3.4 support 43 | gem "csv", "~> 3.3" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Solidus Contrib 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name Solidus nor the names of its contributors may be used to 13 | endorse or promote products derived from this software without specific 14 | prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /app/views/spree/reviews/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for review, url: product_reviews_path(product), html: { method: :post, multipart: true} do |f| %> 2 | <%= render "spree/shared/error_messages", target: review %> 3 | 4 |

    5 | <%= f.label :rating %> *
    6 | <%= render "spree/reviews/stars", stars: review.rating, edit_enabled: true %> 7 |

    8 | 9 |

    10 | <%= f.label :name %>
    11 | <%= f.text_field :name, maxlength: "255", size: "50" %> 12 |

    13 | 14 |

    15 | <%= f.label :title %>
    16 | <%= f.text_field :title, maxlength: "255", size: "50" %> 17 |

    18 | 19 |

    20 | <%= f.label :review %>
    21 | <%= f.text_area :review, wrap: "virtual", rows: "10", cols: "50" %> 22 |

    23 | 24 | <% if Spree::Reviews::Config[:allow_image_upload] %> 25 |

    26 | <%= f.label :images %>
    27 | <%= f.file_field :images, :accept => "image/*", multiple: true %> 28 |

    29 | <% end %> 30 | 31 | <% if Spree::Reviews::Config[:render_show_identifier_checkbox] %> 32 |

    33 | <%= f.label :show_identifier %>
    34 | <%= f.check_box :show_identifier, wrap: "virtual", rows: "10", cols: "50", checked: true %> 35 |

    36 | <% end %> 37 | 38 |

    39 | <%= f.submit I18n.t("spree.submit_your_review"), class: "button bg_darkfirst" %> 40 |

    41 | <% end %> 42 | -------------------------------------------------------------------------------- /solidus_reviews.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/solidus_reviews/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'solidus_reviews' 7 | spec.version = SolidusReviews::VERSION 8 | spec.authors = ['Solidus Contrib'] 9 | 10 | spec.summary = 'Review and rating functionality for your Solidus store.' 11 | spec.description = 'Review and rating functionality for your Solidus store.' 12 | spec.homepage = 'https://github.com/solidusio-contrib/solidus_reviews' 13 | spec.license = 'BSD-3-Clause' 14 | 15 | spec.metadata['homepage_uri'] = spec.homepage 16 | spec.metadata['source_code_uri'] = 'https://github.com/solidusio-contrib/solidus_reviews' 17 | spec.metadata['changelog_uri'] = 'https://github.com/solidusio-contrib/solidus_reviews/releases' 18 | 19 | spec.required_ruby_version = '>= 3.0' 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") } 24 | 25 | spec.files = files.grep_v(%r{^(test|spec|features)/}) 26 | spec.test_files = files.grep(%r{^(test|spec|features)/}) 27 | spec.bindir = "exe" 28 | spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency 'deface', ['>= 1.9.0', '< 2.0'] 32 | spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 5'] 33 | spec.add_dependency 'solidus_support', ['>= 0.14.1', '< 1'] 34 | 35 | spec.add_development_dependency 'rails-controller-testing' 36 | spec.add_development_dependency 'solidus_dev_support', ['>= 2.11', '< 3'] 37 | end 38 | -------------------------------------------------------------------------------- /db/sample/reviews.yml: -------------------------------------------------------------------------------- 1 | <% 2 | require 'faker' 3 | %> 4 | r1: 5 | product: ror_tote 6 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 7 | location: here 8 | rating: 4 9 | title: "more uses for this item" 10 | review: <%= Faker::Lorem.paragraph %> 11 | approved: true 12 | r2: 13 | product: ror_tote 14 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 15 | location: here 16 | rating: 3 17 | title: "further comments" 18 | review: <%= Faker::Lorem.paragraph %> 19 | approved: false 20 | r3: 21 | product: ror_ringer 22 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 23 | location: here 24 | rating: 4 25 | title: "essential wear" 26 | review: <%= Faker::Lorem.paragraph %> 27 | approved: false 28 | r4: 29 | product: apache_baseball_jersey 30 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 31 | location: here 32 | rating: 2 33 | title: "not impressed" 34 | review: <%= Faker::Lorem.paragraph %> 35 | approved: true 36 | r5: 37 | product: ror_ringer 38 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 39 | location: an office 40 | rating: 5 41 | title: "I have one for every day of the week" 42 | review: <%= Faker::Lorem.paragraph %> 43 | approved: true 44 | r6: 45 | product: ror_ringer 46 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 47 | location: here 48 | rating: 4 49 | title: "cooler than my toga" 50 | review: <%= Faker::Lorem.paragraph %> 51 | approved: true 52 | r7: 53 | product: ror_ringer 54 | name: <%= Faker::Name.first_name %> <%= Faker::Name.last_name %> 55 | location: emerald city 56 | rating: 2 57 | title: "my other half didn't like it" 58 | review: <%= Faker::Lorem.paragraph %> 59 | approved: false -------------------------------------------------------------------------------- /lib/generators/solidus_reviews/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SolidusReviews 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | class_option :auto_run_migrations, type: :boolean, default: false 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | def self.exit_on_failure? 10 | true 11 | end 12 | 13 | def copy_initializer 14 | template 'initializer.rb', 'config/initializers/solidus_reviews.rb' 15 | end 16 | 17 | def add_javascripts 18 | append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/solidus_reviews\n" 19 | append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_reviews\n" 20 | end 21 | 22 | def add_stylesheets 23 | inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/solidus_reviews\n", before: %r{\*/}, verbose: true # rubocop:disable Layout/LineLength 24 | inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/solidus_reviews\n", before: %r{\*/}, verbose: true # rubocop:disable Layout/LineLength 25 | end 26 | 27 | def add_migrations 28 | run 'bin/rails railties:install:migrations FROM=solidus_reviews' 29 | end 30 | 31 | def run_migrations 32 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Layout/LineLength 33 | if run_migrations 34 | run 'bin/rails db:migrate' 35 | else 36 | puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/solidus_reviews.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/frontend 3 | */ 4 | 5 | #reviews div.header { 6 | margin-top: 10px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .lit { 11 | background-color: #aaaaaa; 12 | color: #0000bb; 13 | margin-right: 1px; 14 | padding-left: 1px; 15 | } 16 | 17 | .unlit { 18 | background-color: #aaaaaa; 19 | color: #000000; 20 | margin-right: 1px; 21 | padding-left: 1px; 22 | } 23 | 24 | .star-rating-control { 25 | display: inline-block; 26 | height: 14px; 27 | } 28 | 29 | .review_hr { 30 | size: 1px; 31 | background: silver; 32 | border: none; 33 | margin: 1em 0; 34 | } 35 | 36 | /* jQuery.Rating Plugin CSS - http://www.fyneworks.com/jquery/star-rating/ */ 37 | div.rating-cancel, div.star-rating { 38 | float: left; 39 | width: 17px; 40 | height: 16px; 41 | text-indent: -999em; 42 | cursor: pointer; 43 | display: block; 44 | background: transparent; 45 | overflow: hidden 46 | } 47 | 48 | div.rating-cancel, div.rating-cancel a { 49 | background: transparent image-url('store/reviews/delete.gif') no-repeat scroll 0 -1px; 50 | } 51 | 52 | div.star-rating, div.star-rating a { 53 | background: image-url('store/reviews/star.gif') no-repeat 0 0px 54 | } 55 | 56 | div.rating-cancel a, div.star-rating a { 57 | display: block; 58 | width: 16px; 59 | height: 100%; 60 | background-position: 0 0px; 61 | border: 0 62 | } 63 | 64 | div.star-rating-on a { 65 | background-position: 0 -32px !important 66 | } 67 | 68 | div.star-rating-hover a { 69 | background-position: 0 -32px 70 | } 71 | 72 | /* Read Only CSS */ 73 | div.star-rating-readonly a { 74 | cursor: default !important 75 | } 76 | 77 | /* Partial Star CSS */ 78 | div.star-rating { 79 | background: transparent !important; 80 | overflow: hidden !important 81 | } 82 | 83 | /* END jQuery.Rating Plugin CSS */ 84 | -------------------------------------------------------------------------------- /app/models/spree/reviews_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::ReviewsConfiguration < Spree::Preferences::Configuration 4 | def self.boolean_preferences 5 | %w(display_unapproved_reviews include_unapproved_reviews feedback_rating show_email require_login track_locale allow_image_upload) 6 | end 7 | 8 | # include non-approved reviews in (public) listings 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 13 | 14 | # control how many reviews are shown in summaries etc. 15 | preference :preview_size, :integer, default: 3 16 | 17 | # show a reviewer's email address 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 22 | 23 | # show helpfullness rating form elements 24 | preference :feedback_rating, :boolean, default: false 25 | 26 | # require login to post reviews 27 | preference :require_login, :boolean, default: true 28 | 29 | # whether to keep track of the reviewer's locale 30 | preference :track_locale, :boolean, default: false 31 | 32 | # render checkbox for a user to approve to show their identifier (name or email) on their review 33 | preference :render_show_identifier_checkbox, :boolean, default: false 34 | 35 | # Approves star only reviews automatically (Reviews without a Title/Review) 36 | preference :approve_star_only, :boolean, default: false 37 | 38 | # Approves star only reviews for verified purchasers only. 39 | preference :approve_star_only_for_verified_purchaser, :boolean, default: false 40 | 41 | # allow customer to update image with the review 42 | preference :allow_image_upload, :boolean, default: true 43 | end 44 | -------------------------------------------------------------------------------- /app/views/spree/shared/_review.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | 3 | <%= render "spree/reviews/stars", stars: review.rating %> 4 | 5 | <% if review.approved? || Spree::Reviews::Config[:display_unapproved_reviews] %> 6 | <%= review.title %> 7 |
    8 | <%= I18n.t("spree.submitted_on") %> <%= l review.created_at.to_date %> 9 | 10 | 11 | 12 | <% if review.show_identifier %> 13 | <% if Spree::Reviews::Config[:show_email] && review.user %> 14 | 15 | <% else %> 16 | 17 | <% end %> 18 | <% else %> 19 | 20 | <% end %> 21 | <% if Spree::Reviews::Config[:show_verified_purchaser] && review.verified_purchaser? %> 22 |
    <%= I18n.t("spree.verified_purchaser") %>
    23 | <% end %> 24 |
    25 | <%= simple_format(review.review) %> 26 |
    27 | <% if Spree::Reviews::Config[:allow_image_upload] %> 28 | <% review.images.each do |image| %> 29 |
    30 | <%= link_to image_tag(image.url(:product)), image.url(:original) %> 31 |
    32 | <% end %> 33 | <% end %> 34 | <% if Spree::Reviews::Config[:feedback_rating] && (!Spree::Reviews::Config[:require_login] || spree_current_user) %> 35 |
    36 | <%= render "spree/feedback_reviews/form", review: review %> 37 |
    38 | <% end %> 39 | <% end %> 40 |
    41 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Admin::ReviewsController do 6 | stub_authorization! 7 | 8 | let(:product) { create(:product) } 9 | let(:review) { create(:review, approved: false) } 10 | 11 | before do 12 | user = create(:admin_user) 13 | allow(controller).to receive(:spree_current_user).and_return(user) 14 | end 15 | 16 | describe '#index' do 17 | it 'list reviews' do 18 | reviews = create_list(:review, 2, product: product) 19 | get :index, params: { product_id: product.slug } 20 | expect(assigns[:reviews]).to match_array reviews 21 | end 22 | end 23 | 24 | describe '#approve' do 25 | it 'show notice message when approved' do 26 | review.update_attribute(:approved, true) 27 | get :approve, params: { id: review.id } 28 | expect(response).to redirect_to spree.admin_reviews_path 29 | expect(flash[:success]).to eq I18n.t('spree.info_approve_review') 30 | end 31 | 32 | it 'show error message when not approved' do 33 | expect_any_instance_of(Spree::Review).to receive(:save).and_return(false) 34 | get :approve, params: { id: review.id } 35 | expect(flash[:error]).to eq I18n.t('spree.error_approve_review') 36 | end 37 | end 38 | 39 | describe '#edit' do 40 | specify do 41 | get :edit, params: { id: review.id } 42 | expect(response.status).to eq(200) 43 | end 44 | 45 | context 'when product is nil' do 46 | before do 47 | review.product = nil 48 | review.save! 49 | end 50 | 51 | it 'flash error' do 52 | get :edit, params: { id: review.id } 53 | expect(flash[:error]).to eq I18n.t('spree.error_no_product') 54 | end 55 | 56 | it 'redirect to admin-reviews page' do 57 | get :edit, params: { id: review.id } 58 | expect(response).to redirect_to spree.admin_reviews_path 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/models/reviews_ability_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require "cancan/matchers" 6 | 7 | describe Spree::ReviewsAbility do 8 | describe '.allow_anonymous_reviews?' do 9 | it 'depends on Spree::Reviews::Config[:require_login]' do 10 | stub_spree_preferences(Spree::Reviews::Config, require_login: false) 11 | expect(described_class.allow_anonymous_reviews?).to be true 12 | stub_spree_preferences(Spree::Reviews::Config, require_login: true) 13 | expect(described_class.allow_anonymous_reviews?).to be false 14 | end 15 | end 16 | 17 | context 'permissions' do 18 | let(:user_without_email) { double(:user, email: nil) } 19 | let(:user_with_email) { double(:user, email: 'a@b.com') } 20 | 21 | context 'when anonymous reviews are allowed' do 22 | before do 23 | stub_spree_preferences(Spree::Reviews::Config, require_login: false) 24 | end 25 | 26 | it 'lets anyone create a review or feedback review' do 27 | [user_without_email, user_with_email].each do |u| 28 | expect(described_class.new(u)).to be_able_to(:create, Spree::Review.new) 29 | expect(described_class.new(u)).to be_able_to(:create, Spree::FeedbackReview.new) 30 | end 31 | end 32 | end 33 | 34 | context 'when anonymous reviews are not allowed' do 35 | before do 36 | stub_spree_preferences(Spree::Reviews::Config, require_login: true) 37 | end 38 | 39 | it 'only allows users with an email to create a review or feedback review' do 40 | expect(described_class.new(user_without_email)).not_to be_able_to(:create, Spree::Review.new) 41 | expect(described_class.new(user_without_email)).not_to be_able_to(:create, Spree::FeedbackReview.new) 42 | 43 | expect(described_class.new(user_with_email)).to be_able_to(:create, Spree::Review.new) 44 | expect(described_class.new(user_with_email)).to be_able_to(:create, Spree::FeedbackReview.new) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: "0 0 * * 4" # every Thursday 10 | 11 | concurrency: 12 | group: test-${{ github.ref_name }} 13 | cancel-in-progress: ${{ github.ref_name != 'main' }} 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | rspec: 20 | name: Solidus ${{ matrix.solidus-branch }}, Rails ${{ matrix.rails-version }} and Ruby ${{ matrix.ruby-version }} on ${{ matrix.database }} 21 | runs-on: ubuntu-24.04 22 | strategy: 23 | fail-fast: true 24 | matrix: 25 | rails-version: 26 | - "7.0" 27 | - "7.1" 28 | - "7.2" 29 | ruby-version: 30 | - "3.1" 31 | - "3.4" 32 | solidus-branch: 33 | - "v4.1" 34 | - "v4.2" 35 | - "v4.3" 36 | - "v4.4" 37 | database: 38 | - "postgresql" 39 | - "mysql" 40 | - "sqlite" 41 | exclude: 42 | - rails-version: "7.2" 43 | solidus-branch: "v4.3" 44 | - rails-version: "7.2" 45 | solidus-branch: "v4.2" 46 | - rails-version: "7.2" 47 | solidus-branch: "v4.1" 48 | - rails-version: "7.1" 49 | solidus-branch: "v4.2" 50 | - rails-version: "7.1" 51 | solidus-branch: "v4.1" 52 | - ruby-version: "3.4" 53 | rails-version: "7.0" 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Run extension tests 57 | uses: solidusio/test-solidus-extension@main 58 | with: 59 | database: ${{ matrix.database }} 60 | rails-version: ${{ matrix.rails-version }} 61 | ruby-version: ${{ matrix.ruby-version }} 62 | solidus-branch: ${{ matrix.solidus-branch }} 63 | - name: Upload coverage reports to Codecov 64 | uses: codecov/codecov-action@v5 65 | continue-on-error: true 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | files: ./coverage/coverage.xml 69 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/review_settings_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Admin::ReviewSettingsController do 6 | stub_authorization! 7 | 8 | before do 9 | user = create(:admin_user) 10 | allow(controller).to receive(:spree_current_user).and_return(user) 11 | end 12 | 13 | describe '#update' do 14 | it 'redirects to edit-review-settings page' do 15 | put :update, params: { preferences: { preview_size: 4 } } 16 | expect(response).to redirect_to spree.edit_admin_review_settings_path 17 | end 18 | 19 | context 'with parameters: 20 | preview_size: 4, 21 | show_email: false, 22 | feedback_rating: false, 23 | require_login: true, 24 | track_locale: true' do 25 | it 'sets preferred_preview_size to 4' do 26 | put :update, params: { preferences: { preview_size: 4 } } 27 | expect(Spree::Reviews::Config.preferred_preview_size).to eq 4 28 | end 29 | 30 | it 'sets preferred_show_email to false' do 31 | put :update, params: { preferences: { show_email: false } } 32 | expect(Spree::Reviews::Config.preferred_show_email).to be false 33 | end 34 | 35 | it 'sets preferred_feedback_rating to false' do 36 | put :update, params: { preferences: { feedback_rating: false } } 37 | expect(Spree::Reviews::Config.preferred_feedback_rating).to be false 38 | end 39 | 40 | it 'sets preferred_require_login to true' do 41 | put :update, params: { preferences: { require_login: true } } 42 | expect(Spree::Reviews::Config.preferred_require_login).to be true 43 | end 44 | 45 | it 'sets preferred_track_locale to true' do 46 | put :update, params: { preferences: { track_locale: true } } 47 | expect(Spree::Reviews::Config.preferred_track_locale).to be true 48 | end 49 | end 50 | end 51 | 52 | describe '#edit' do 53 | it 'renders the edit template' do 54 | get :edit 55 | expect(response).to render_template(:edit) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /config/locales/zh-TW.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zh-TW: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: 您的姓名 7 | title: 標題 8 | review: 內文 9 | rating: 評級 10 | created_at: 日期 11 | ip_address: IP 12 | user: 用戶 13 | models: 14 | spree/review: 15 | one: 一個評論 16 | other: '%{count}個評論' 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: 您必需輸入評級 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: 您必需輸入評級 27 | spree: 28 | approve: 核準 29 | approved_reviews: 核準 30 | average_customer_rating: 平均評級 31 | back_reviews: 返回評論 32 | based_upon_review_count: 33 | one: 以評論為基礎 34 | other: '以 %{count} 個評論為基礎' 35 | by: 由 36 | editing_review_for_html: '正在修改 %{product_name} 的評論' 37 | error_approve_review: 出錯了,不能核準評論 38 | error_no_product: 被評論的產品不存在了。 39 | feedback: 回饋 40 | feedback_review_for: "評論: '%{review}'" 41 | for: 給 42 | from: 由 43 | info_approve_review: 已核準評論 44 | leave_us_a_review_for: "請給我們的 '%{name}' 評論和評級" 45 | no_reviews_available: '此產品沒有評論。' 46 | out_of_5: '頂級為 5' 47 | rating: 評級 48 | reviews: 評論 49 | admin: 50 | tab: 51 | reviews: 評論 52 | review_management: 評論 53 | review_successfully_submitted: 已成功遞交評論。 54 | spree_reviews: 55 | feedback_rating: 評級回饋 56 | display_unapproved: 在列表中顯示未經批准的評論 57 | include_unapproved: 包括未核準評論在列表中 58 | manage_review_settings: 控制評論的顯示 59 | preview_size: 評論片段的大小 60 | require_login: 用者必需登入 61 | review_settings: 評論設定 62 | show_email: 顯示電郵地址 63 | show_verified_purchaser: 顯示經過驗證的購買者 64 | track_locale: 追蹤用戶地域 65 | star: 66 | one: "1" 67 | other: "%{count}" 68 | stars: 星 69 | submit_your_review: 遞交你的評論 70 | submitted_on: 遞交日期 71 | unapproved_reviews: 沒核準 72 | verified_purchaser: 驗證購買者 73 | voice: 74 | one: "1 聲音" 75 | other: "%{count} 聲音" 76 | was_this_review_helpful: 此評論對你是否有幫助? 77 | write_your_own_review: 寫下您的評論 78 | anonymous: 匿名者 79 | -------------------------------------------------------------------------------- /config/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | --- 2 | zh-CN: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: 您的姓名 7 | title: 标题 8 | review: 內文 9 | rating: 评级 10 | created_at: 日期 11 | ip_address: IP 12 | user: 用戶 13 | models: 14 | spree/review: 15 | one: 一个评论 16 | other: "%{count}个评论" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "您必需输入评级" 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "您必需输入评级" 27 | spree: 28 | approve: 核准 29 | approved_reviews: 核准 30 | average_customer_rating: 平均评级 31 | back_reviews: 返回评论 32 | based_upon_review_count: 33 | one: 以评论为基础 34 | other: "以 %{count} 個评论为基础" 35 | by: 由 36 | editing_review_for_html: '正在修改 %{product_name} 的评论' 37 | error_approve_review: 出错了,不能核准评论 38 | error_no_product: 被评论的产品不存在了。 39 | feedback: 回馈 40 | feedback_review_for: "评论: '%{review}'" 41 | for: 給 42 | from: 由 43 | info_approve_review: 已核准评论 44 | leave_us_a_review_for: "请给我们的 '%{name}' 评论和评级" 45 | no_reviews_available: "此产品没有评论。" 46 | out_of_5: "顶级为 5" 47 | rating: 评级 48 | reviews: 评论 49 | admin: 50 | tab: 51 | reviews: 评论 52 | review_management: 评论 53 | review_successfully_submitted: 已成功递交评论。 54 | spree_reviews: 55 | feedback_rating: 评级回馈 56 | display_unapproved: 在列表中显示未经批准的评论 57 | include_unapproved: 包括未核准评论在列表中 58 | manage_review_settings: 控制评论的显示 59 | preview_size: 评论片段的大小 60 | require_login: 用者必需登入 61 | review_settings: 评论设定 62 | show_email: 显示电邮地址 63 | show_verified_purchaser: 显示经过验证的购买者 64 | track_locale: 追踪用户地域 65 | star: 66 | one: "1" 67 | other: "%{count}" 68 | stars: 星 69 | submit_your_review: 递交你的评论 70 | submitted_on: 递交日期 71 | unapproved_reviews: 沒核准 72 | verified_purchaser: 验证购买者 73 | voice: 74 | one: "1 聲音" 75 | other: "%{count} 聲音" 76 | was_this_review_helpful: "此评论对你是否有帮助?" 77 | write_your_own_review: 写下您的评论 78 | anonymous: 匿名者 79 | -------------------------------------------------------------------------------- /bin/sandbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | test -z "${DEBUG+empty_string}" || set -x 5 | 6 | test "$DB" = "sqlite" && export DB="sqlite3" 7 | 8 | if [ -z "$SOLIDUS_BRANCH" ] 9 | then 10 | echo "~~> Use 'export SOLIDUS_BRANCH=[main|v3.2|...]' to control the Solidus branch" 11 | SOLIDUS_BRANCH="main" 12 | fi 13 | echo "~~> Using branch $SOLIDUS_BRANCH of solidus" 14 | 15 | if [ -z "$SOLIDUS_FRONTEND" ] 16 | then 17 | echo "~~> Use 'export SOLIDUS_FRONTEND=[solidus_frontend|solidus_starter_frontend]' to control the Solidus frontend" 18 | SOLIDUS_FRONTEND="solidus_frontend" 19 | fi 20 | echo "~~> Using branch $SOLIDUS_FRONTEND as the solidus frontend" 21 | 22 | extension_name="solidus_reviews" 23 | 24 | # Stay away from the bundler env of the containing extension. 25 | function unbundled { 26 | ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@ 27 | } 28 | 29 | rm -rf ./sandbox 30 | unbundled bundle exec rails new sandbox \ 31 | --database="${DB:-sqlite3}" \ 32 | --skip-bundle \ 33 | --skip-git \ 34 | --skip-keeps \ 35 | --skip-rc \ 36 | --skip-spring \ 37 | --skip-test \ 38 | --skip-javascript 39 | 40 | if [ ! -d "sandbox" ]; then 41 | echo 'sandbox rails application failed' 42 | exit 1 43 | fi 44 | 45 | cd ./sandbox 46 | cat <> Gemfile 47 | gem 'solidus', github: 'solidusio/solidus', branch: '$SOLIDUS_BRANCH' 48 | gem 'rails-i18n' 49 | gem 'solidus_i18n' 50 | 51 | gem '$extension_name', path: '..' 52 | 53 | group :test, :development do 54 | platforms :mri do 55 | gem 'pry-byebug' 56 | end 57 | end 58 | RUBY 59 | 60 | unbundled bundle install --gemfile Gemfile 61 | 62 | unbundled bundle exec rake db:drop db:create 63 | 64 | unbundled bundle exec rails generate solidus:install \ 65 | --auto-accept \ 66 | --user_class=Spree::User \ 67 | --enforce_available_locales=true \ 68 | --with-authentication=true \ 69 | --payment-method=none \ 70 | --frontend=${SOLIDUS_FRONTEND} \ 71 | $@ 72 | 73 | unbundled bundle exec rails generate solidus:auth:install --auto-run-migrations 74 | unbundled bundle exec rails generate ${extension_name}:install --auto-run-migrations 75 | 76 | echo 77 | echo "🚀 Sandbox app successfully created for $extension_name!" 78 | echo "🧪 This app is intended for test purposes." 79 | -------------------------------------------------------------------------------- /app/controllers/spree/reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::ReviewsController < Spree::StoreController 4 | helper Spree::BaseHelper 5 | before_action :load_product, only: [:index, :new, :create, :edit, :update] 6 | 7 | def index 8 | @approved_reviews = Spree::Review.approved.where(product: @product) 9 | end 10 | 11 | def new 12 | @review = Spree::Review.new(product: @product) 13 | authorize! :create, @review 14 | end 15 | 16 | def edit 17 | @review = Spree::Review.find(params[:id]) 18 | if @review.product.nil? 19 | flash[:error] = I18n.t('spree.error_no_product') 20 | end 21 | authorize! :update, @review 22 | end 23 | 24 | # save if all ok 25 | def create 26 | review_params[:rating].sub!(/\s*[^0-9]*\z/, '') if review_params[:rating].present? 27 | 28 | @review = Spree::Review.new(review_params) 29 | @review.product = @product 30 | @review.user = spree_current_user if spree_user_signed_in? 31 | @review.ip_address = request.remote_ip 32 | @review.locale = I18n.locale.to_s if Spree::Reviews::Config[:track_locale] 33 | # Handle images 34 | params[:review][:images]&.each do |image| 35 | @review.images.new(attachment: image) if image.present? 36 | end 37 | 38 | authorize! :create, @review 39 | if @review.save 40 | flash[:notice] = I18n.t('spree.review_successfully_submitted') 41 | redirect_to spree.product_path(@product) 42 | else 43 | render :new 44 | end 45 | end 46 | 47 | def update 48 | review_params[:rating].sub!(/\s*[^0-9]*\z/, '') if params[:review][:rating].present? 49 | 50 | @review = Spree::Review.find(params[:id]) 51 | 52 | # Handle images 53 | params[:review][:images]&.each do |image| 54 | @review.images.new(attachment: image) if image.present? 55 | end 56 | 57 | authorize! :update, @review 58 | if @review.update(review_params) 59 | flash[:notice] = I18n.t('spree.review_successfully_submitted') 60 | redirect_to spree.product_path(@product) 61 | else 62 | render :edit 63 | end 64 | end 65 | 66 | private 67 | 68 | def load_product 69 | @product = Spree::Product.friendly.find(params[:product_id]) 70 | end 71 | 72 | def permitted_review_attributes 73 | [:rating, :title, :review, :name, :show_identifier, :images] 74 | end 75 | 76 | def review_params 77 | params.require(:review).permit(permitted_review_attributes) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/models/reviews_configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::ReviewsConfiguration do 6 | subject { described_class.new } 7 | 8 | before do 9 | subject.reset 10 | end 11 | 12 | it 'has the include_unapproved_reviews preference' do 13 | expect(subject).to respond_to(:preferred_include_unapproved_reviews) 14 | expect(subject).to respond_to(:preferred_include_unapproved_reviews=) 15 | expect(subject.preferred_include_unapproved_reviews).to be false 16 | end 17 | 18 | it 'has the preview_size preference' do 19 | expect(subject).to respond_to(:preferred_preview_size) 20 | expect(subject).to respond_to(:preferred_preview_size=) 21 | expect(subject.preferred_preview_size).to eq(3) 22 | end 23 | 24 | it 'has the show_email preference' do 25 | expect(subject).to respond_to(:preferred_show_email) 26 | expect(subject).to respond_to(:preferred_show_email=) 27 | expect(subject.preferred_show_email).to be false 28 | end 29 | 30 | it 'has the show_verified_purchaser preference' do 31 | expect(subject).to respond_to(:preferred_show_verified_purchaser) 32 | expect(subject).to respond_to(:preferred_show_verified_purchaser=) 33 | expect(subject.preferred_show_verified_purchaser).to be false 34 | end 35 | 36 | it 'has the feedback_rating preference' do 37 | expect(subject).to respond_to(:preferred_feedback_rating) 38 | expect(subject).to respond_to(:preferred_feedback_rating=) 39 | expect(subject.preferred_feedback_rating).to be false 40 | end 41 | 42 | it 'has the require_login preference' do 43 | expect(subject).to respond_to(:preferred_require_login) 44 | expect(subject).to respond_to(:preferred_require_login=) 45 | expect(subject.preferred_require_login).to be true 46 | end 47 | 48 | it 'has the track_locale preference' do 49 | expect(subject).to respond_to(:preferred_track_locale) 50 | expect(subject).to respond_to(:preferred_track_locale=) 51 | expect(subject.preferred_track_locale).to be false 52 | end 53 | 54 | it 'has the approve_star_only preference' do 55 | expect(subject).to respond_to(:preferred_approve_star_only) 56 | expect(subject).to respond_to(:preferred_approve_star_only=) 57 | expect(subject.preferred_approve_star_only).to be false 58 | end 59 | 60 | it 'has the approve_star_only_for_verified_purchaser preference' do 61 | expect(subject).to respond_to(:preferred_approve_star_only_for_verified_purchaser) 62 | expect(subject).to respond_to(:preferred_approve_star_only_for_verified_purchaser=) 63 | expect(subject.preferred_approve_star_only_for_verified_purchaser).to be false 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidus Reviews 2 | 3 | [![Test](https://github.com/solidusio-contrib/solidus_reviews/actions/workflows/test.yml/badge.svg)](https://github.com/solidusio-contrib/solidus_reviews/actions/workflows/test.yml) 4 | [![codecov](https://codecov.io/gh/solidusio-contrib/solidus_reviews/graph/badge.svg?token=khhn97J549)](https://codecov.io/gh/solidusio-contrib/solidus_reviews) 5 | 6 | Straightforward review/rating functionality, updated for [Solidus](https://solidus.io). 7 | 8 | While the gem's name has changed, the module namespace and commands are still `spree` for now. 9 | 10 | ## Installation 11 | 12 | Add solidus_reviews to your Gemfile: 13 | 14 | ```ruby 15 | gem 'solidus_reviews' 16 | ``` 17 | 18 | Bundle your dependencies and run the installation generator: 19 | 20 | ```shell 21 | bin/rails generate solidus_reviews:install 22 | ``` 23 | 24 | ## Usage 25 | 26 | The `Spree::ReviewsController` controller provides all the CRUD functionality for product reviews. 27 | 28 | The `Spree::FeedbackReviewsController` allows user to express their feedback on a specific review. 29 | You can think of these as meta-reviews (e.g. the classic "Was this useful?" modal). 30 | 31 | You can approve, edit and delete reviews and feedback reviews from the backend. 32 | 33 | ## Development 34 | 35 | ### Testing the extension 36 | 37 | First bundle your dependencies, then run `bin/rake`. `bin/rake` will default to building the dummy 38 | app if it does not exist, then it will run specs. The dummy app can be regenerated by using 39 | `bin/rake extension:test_app`. 40 | 41 | ```shell 42 | bin/rake 43 | ``` 44 | 45 | To run [Rubocop](https://github.com/bbatsov/rubocop) static code analysis run 46 | 47 | ```shell 48 | bundle exec rubocop 49 | ``` 50 | 51 | When testing your application's integration with this extension you may use its factories. 52 | You can load Solidus core factories along with this extension's factories using this statement: 53 | 54 | ```ruby 55 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusReviews::Engine) 56 | ``` 57 | 58 | ### Running the sandbox 59 | 60 | To run this extension in a sandboxed Solidus application, you can run `bin/sandbox`. The path for 61 | the sandbox app is `./sandbox` and `bin/rails` will forward any Rails commands to 62 | `sandbox/bin/rails`. 63 | 64 | Here's an example: 65 | 66 | ``` 67 | $ bin/rails server 68 | => Booting Puma 69 | => Rails 6.0.2.1 application starting in development 70 | * Listening on tcp://127.0.0.1:3000 71 | Use Ctrl-C to stop 72 | ``` 73 | 74 | ### Releasing new versions 75 | 76 | Please refer to the [dedicated page](https://github.com/solidusio/solidus/wiki/How-to-release-extensions) in the Solidus wiki. 77 | 78 | ## License 79 | 80 | Copyright (c) 2023 Solidus Contrib, released under the New BSD License. 81 | -------------------------------------------------------------------------------- /spec/controllers/spree/feedback_reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::FeedbackReviewsController do 6 | let(:user) { create(:user) } 7 | let(:product) { create(:product) } 8 | let(:review) { create(:review, user: user) } 9 | let(:valid_attributes) do 10 | { review_id: review.id, 11 | user_id: user.id, 12 | feedback_review: { 13 | rating: '4 stars', 14 | comment: 'some comment' 15 | } } 16 | end 17 | 18 | before do 19 | allow(controller).to receive(:spree_current_user).and_return(user) 20 | allow(controller).to receive(:spree_user_signed_in?).and_return(true) 21 | request.env['HTTP_REFERER'] = '/' 22 | end 23 | 24 | describe '#create' do 25 | it 'creates a new feedback review' do 26 | rating = 4 27 | comment = ['Thanks for your review!', 'Cheers'].join("\n") 28 | expect { 29 | post :create, params: { review_id: review.id, 30 | feedback_review: { 31 | comment: comment, 32 | rating: rating 33 | }, 34 | format: :js } 35 | expect(response.status).to eq(200) 36 | expect(response).to render_template(:create) 37 | }.to change(Spree::Review, :count).by(1) 38 | feedback_review = Spree::FeedbackReview.last 39 | expect(feedback_review.comment).to eq(comment) 40 | expect(feedback_review.review).to eq(review) 41 | expect(feedback_review.rating).to eq(rating) 42 | expect(feedback_review.user).to eq(user) 43 | end 44 | 45 | it 'redirects back to the calling page' do 46 | post :create, params: valid_attributes 47 | expect(response).to redirect_to '/' 48 | end 49 | 50 | it 'sets locale on feedback-review if required by config' do 51 | stub_spree_preferences(Spree::Reviews::Config, track_locale: true) 52 | post :create, params: valid_attributes 53 | expect(assigns[:review].locale).to eq I18n.locale.to_s 54 | end 55 | 56 | it 'fails when user is not authorized' do 57 | allow(controller).to receive(:authorize!).and_raise(RuntimeError) 58 | 59 | expect { 60 | post :create, params: valid_attributes 61 | }.to raise_error(RuntimeError) 62 | end 63 | 64 | it 'removes all non-numbers from ratings parameter' do 65 | post :create, params: valid_attributes 66 | expect(controller.params[:feedback_review][:rating]).to eq '4' 67 | end 68 | 69 | it 'do not create feedback-review if review doesnt exist' do 70 | expect { 71 | post :create, params: valid_attributes.merge!(review_id: nil) 72 | }.to raise_error ActionController::UrlGenerationError 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /config/locales/tr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tr: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: İsminiz 7 | title: Başlık 8 | review: Konu 9 | rating: Değerlendirme 10 | created_at: Tarih 11 | ip_address: IP 12 | user: Kullanıcı 13 | models: 14 | spree/review: 15 | one: bir yorum 16 | other: "%{count} yorumlar" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Yorum yazmak için giriş yapmalısınız." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Yorum yazmak için giriş yapmalısınız." 27 | spree: 28 | approval_status: Onay Durumu 29 | approve: Onayla 30 | approved_reviews: Onaylı 31 | approved_text: Yorum 1-2 iş günü içinde yayımlanacaktır. 32 | average_customer_rating: Ortalama Oy 33 | back_reviews: Geri değerlendirme 34 | based_upon_review_count: 35 | one: bir incelemeye göre 36 | other: "%{count} incelemeye göre" 37 | by: tarafından 38 | editing_review_for_html: 'Yorumu düzenle %{product_name}' 39 | error_approve_review: Yorum onaylamada hata 40 | error_no_product: Yorumlanan ürün artık yok 41 | feedback: Geri bildirim 42 | feedback_review_for: "Yorum: '%{review}'" 43 | for: için 44 | from: itibaren 45 | info_approve_review: Onaylanmış yorum 46 | leave_us_a_review_for: "%{name}" 47 | no_reviews_available: "Bu ürün hakkında henüz herhangi bir yorum bulunmuyor.." 48 | out_of_5: "5 üzerinden" 49 | rating: değerlendirme 50 | reviews: Yorumlar 51 | admin: 52 | tab: 53 | reviews: Yorumlar 54 | review_management: Yorumlar 55 | review_successfully_submitted: Yorumunuz başarıyla iletildi 56 | spree_reviews: 57 | feedback_rating: Geribildirimi değerlendir 58 | display_unapproved: Listelerde onaylanmayan yorumları göster 59 | include_unapproved: Listede ki onaylanmamış değerlendirmeleri dahil et 60 | manage_review_settings: Yorum ekranını kontrol edin 61 | preview_size: Yorum büyüklüğü 62 | require_login: Yorumları görebilmek için kullanıcı girişi 63 | review_settings: Yorum Ayarları 64 | show_email: e-posta adresini göster 65 | show_verified_purchaser: Doğrulanmış alıcıyı göster 66 | track_locale: Kullanıcının yerel bilgilerini kaydet/takip et 67 | star: 68 | one: "1" 69 | other: "%{count}" 70 | stars: Yıldızlar 71 | submit_your_review: Gönder 72 | submitted_on: "Gönderilme Tarihi:" 73 | unapproved_reviews: Onaylanmamış 74 | verified_purchaser: Doğrulanmış alıcı 75 | voice: 76 | one: "1 değerlendirme" 77 | other: "%{count} değerlendirme" 78 | was_this_review_helpful: "Bu yorum size yardımcı oldu mu?" 79 | write_your_own_review: Yorum Yap 80 | anonymous: anonim 81 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Your Name 7 | title: Title 8 | review: Content 9 | rating: Rating 10 | created_at: Date 11 | ip_address: IP 12 | user: User 13 | show_identifier: Show Identifier 14 | models: 15 | spree/review: 16 | one: one review 17 | other: "%{count} reviews" 18 | errors: 19 | models: 20 | spree/review: 21 | attributes: 22 | rating: 23 | you_must_enter_value_for_rating: "You must enter a value for rating." 24 | spree/feedback_review: 25 | attributes: 26 | rating: 27 | you_must_enter_value_for_rating: "You must enter a value for rating." 28 | spree: 29 | approval_status: Approval status 30 | approve: Approve 31 | approved_reviews: Approved 32 | average_customer_rating: Average rating 33 | back_reviews: Back to the reviews 34 | based_upon_review_count: 35 | one: based upon one review 36 | other: "based upon %{count} reviews" 37 | by: by 38 | editing_review_for_html: 'Editing review for %{product_name}' 39 | error_approve_review: Error approving review 40 | error_no_product: The reviewed product doesn't exist anymore 41 | feedback: Feedback 42 | feedback_review_for: "Review: '%{review}'" 43 | for: for 44 | from: from 45 | info_approve_review: Review approved 46 | leave_us_a_review_for: "Please leave us a review and a rating for our '%{name}'" 47 | no_reviews_available: "No reviews have been written for this product." 48 | out_of_5: "out of 5" 49 | rating: rating 50 | reviews: Reviews 51 | admin: 52 | tab: 53 | reviews: Reviews 54 | review_management: Reviews 55 | review_successfully_submitted: Review was successfully submitted 56 | spree_reviews: 57 | feedback_rating: Rate feedback 58 | display_unapproved: Display unapproved reviews in listings 59 | include_unapproved: Include unapproved reviews in listings 60 | manage_review_settings: Control the display of reviews 61 | preview_size: Size of the review snippets 62 | require_login: Require user to be logged in 63 | review_settings: Review Settings 64 | show_email: Show email addresses 65 | show_verified_purchaser: Show verified purchaser 66 | track_locale: Track user's locale 67 | allow_image_upload: Allow images to be attached to reviews 68 | star: 69 | one: "1" 70 | other: "%{count}" 71 | stars: Stars 72 | submit_your_review: Submit your review 73 | submitted_on: Submitted on 74 | unapproved_reviews: Unapproved 75 | verified_purchaser: Verified purchaser 76 | voice: 77 | one: "1 voice" 78 | other: "%{count} voices" 79 | was_this_review_helpful: "Was this review helpful to you?" 80 | write_your_own_review: Write your own review 81 | anonymous: Anonymous 82 | -------------------------------------------------------------------------------- /app/models/spree/review.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Spree::Review < Spree::Base 4 | belongs_to :product, touch: true, optional: true 5 | belongs_to :user, class_name: Spree.user_class.to_s, optional: true 6 | has_many :feedback_reviews, dependent: :destroy 7 | has_many :images, -> { order(:position) }, as: :viewable, 8 | dependent: :destroy, class_name: "Spree::Image" 9 | 10 | before_save :verify_purchaser 11 | before_save :approve_review, unless: :approved? 12 | after_save :recalculate_product_rating, if: :approved? 13 | after_destroy :recalculate_product_rating 14 | 15 | validates :rating, numericality: { only_integer: true, 16 | greater_than_or_equal_to: 1, 17 | less_than_or_equal_to: 5, 18 | message: :you_must_enter_value_for_rating } 19 | 20 | default_scope { order("spree_reviews.created_at DESC") } 21 | 22 | scope :localized, ->(lc) { where('spree_reviews.locale = ?', lc) } 23 | scope :most_recent_first, -> { order('spree_reviews.created_at DESC') } 24 | scope :oldest_first, -> { reorder('spree_reviews.created_at ASC') } 25 | scope :preview, -> { limit(Spree::Reviews::Config[:preview_size]).oldest_first } 26 | scope :approved, -> { where(approved: true) } 27 | scope :not_approved, -> { where(approved: false) } 28 | scope :default_approval_filter, -> { Spree::Reviews::Config[:include_unapproved_reviews] ? all : approved } 29 | 30 | self.allowed_ransackable_associations = %w[feedback_reviews product user] 31 | self.allowed_ransackable_attributes = %w[approved name review title] 32 | 33 | def feedback_stars 34 | return 0 if feedback_reviews.size <= 0 35 | 36 | ((feedback_reviews.sum(:rating) / feedback_reviews.size) + 0.5).floor 37 | end 38 | 39 | def recalculate_product_rating 40 | product.recalculate_rating if product.present? 41 | end 42 | 43 | def email 44 | user&.email 45 | end 46 | 47 | def verify_purchaser 48 | return unless user_id && product_id 49 | 50 | verified_purchase = Spree::LineItem.joins(:order, :variant) 51 | .where.not(spree_orders: { completed_at: nil }) 52 | .find_by( 53 | spree_variants: { product_id: product_id }, 54 | spree_orders: { user_id: user_id } 55 | ).present? 56 | 57 | self.verified_purchaser = verified_purchase 58 | end 59 | 60 | def star_only? 61 | [title, review].all?(&:blank?) && rating.present? 62 | end 63 | 64 | def approve_review 65 | # Checks if we should auto approve the review. 66 | if Spree::Reviews::Config[:approve_star_only] 67 | self.approved = true if star_only? 68 | elsif Spree::Reviews::Config[:approve_star_only_for_verified_purchaser] 69 | self.approved = true if star_only? && verified_purchaser? 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /config/locales/sv.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sv: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | created_at: Datum 7 | ip_address: IP 8 | name: "Ditt namn" 9 | rating: Betyg 10 | review: Innehåll 11 | title: Rubrik 12 | user: Användare 13 | models: 14 | spree/review: 15 | one: "en recension" 16 | other: "%{count} recensioner" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Du måste ange ett värde för betyg." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Du måste ange ett värde för betyg." 27 | spree: 28 | anonymous: Anonym 29 | approval_status: Status för godkännande 30 | approve: Godkänn 31 | approved_reviews: Godkänd 32 | average_customer_rating: "Genomsnittligt betyg" 33 | back_reviews: "Tillbaka till recensionerna" 34 | based_upon_review_count: 35 | one: "baserad på en recension" 36 | other: "baserad på %{count} recensioner" 37 | by: av 38 | editing_review_for_html: "Editerad recension för %{product_name}" 39 | error_approve_review: "Problem med att godkänna recension" 40 | error_no_product: "Den betygsatta produkten finns inte längre" 41 | feedback: Återkoppling 42 | feedback_review_for: "Recension: '%{review}'" 43 | for: för 44 | from: från 45 | info_approve_review: "Recensionen godkänd" 46 | leave_us_a_review_for: "Recension och betyg för %{name}" 47 | no_reviews_available: "Inga recensioner har ännu skrivits för den här produkten." 48 | out_of_5: "ut av 5" 49 | rating: betyg 50 | admin: 51 | tab: 52 | reviews: "Recensioner" 53 | review_management: "Hantera recensioner" 54 | review_successfully_submitted: "Recension har skapats" 55 | reviews: Recensioner 56 | spree_reviews: 57 | feedback_rating: Betygsåterkoppling 58 | display_unapproved: "Visa ej godkända recensioner i listor" 59 | include_unapproved: "Inkludera inte godkända recensioner i listan" 60 | manage_review_settings: "Kontrollera visningen av recensionerna" 61 | preview_size: "Storlek på recensions snippets" 62 | require_login: "Kräv att användaren är inloggad" 63 | review_settings: "Recensions inställningar" 64 | show_email: "Visa epost adresser" 65 | show_verified_purchaser: Visa verifierad köpare 66 | track_locale: "Spåra användarens språk" 67 | star: 68 | one: "1" 69 | other: "%{count}" 70 | stars: Stärnor 71 | submit_your_review: "Skicka in din recension" 72 | submitted_on: "Skapad den" 73 | unapproved_reviews: "Icke godkänd" 74 | verified_purchaser: Verifierad köpare 75 | voice: 76 | one: "1 röst" 77 | other: "%{count} röster" 78 | was_this_review_helpful: "Var denna recension användbar för dig?" 79 | write_your_own_review: "Skriv din egen recension" 80 | -------------------------------------------------------------------------------- /spec/models/product_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Product do 6 | it { is_expected.to respond_to(:avg_rating) } 7 | it { is_expected.to respond_to(:reviews) } 8 | it { is_expected.to respond_to(:stars) } 9 | 10 | describe '#stars' do 11 | let(:product) { build(:product) } 12 | 13 | it 'rounds' do 14 | allow(product).to receive(:avg_rating).and_return(3.7) 15 | expect(product.stars).to eq(4) 16 | 17 | allow(product).to receive(:avg_rating).and_return(2.3) 18 | expect(product.stars).to eq(2) 19 | end 20 | 21 | it 'handles a nil value' do 22 | allow(product).to receive(:avg_rating).and_return(nil) 23 | 24 | expect { 25 | expect(product.stars).to eq(0) 26 | }.not_to raise_error 27 | end 28 | end 29 | 30 | describe '#recalculate_rating' do 31 | let!(:product) { create(:product) } 32 | 33 | context 'when there are approved reviews' do 34 | let!(:approved_review_1) { create(:review, product: product, approved: true, rating: 4) } 35 | let!(:approved_review_2) { create(:review, product: product, approved: true, rating: 5) } 36 | let!(:unapproved_review_1) { create(:review, product: product, approved: false, rating: 4) } 37 | 38 | context "including unapproved reviews" do 39 | before do 40 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: true) 41 | end 42 | 43 | it "updates the product average rating and ignores unapproved reviews" do 44 | product.avg_rating = 0 45 | product.reviews_count = 0 46 | product.save! 47 | 48 | product.recalculate_rating 49 | expect(product.avg_rating).to eq(4.3) 50 | expect(product.reviews_count).to eq(3) 51 | end 52 | end 53 | 54 | context "only approved reviews" do 55 | before do 56 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: false) 57 | end 58 | 59 | it "updates the product average rating and ignores unapproved reviews" do 60 | product.avg_rating = 0 61 | product.reviews_count = 0 62 | product.save! 63 | 64 | product.recalculate_rating 65 | expect(product.avg_rating).to eq(4.5) 66 | expect(product.reviews_count).to eq(2) 67 | end 68 | end 69 | end 70 | 71 | context "without unapproved reviews" do 72 | let!(:unapproved_review_1) { create(:review, product: product, approved: false, rating: 4) } 73 | 74 | before do 75 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: false) 76 | end 77 | 78 | it "updates the product average rating and ignores unapproved reviews" do 79 | product.update_columns(avg_rating: 3, reviews_count: 20) 80 | 81 | product.recalculate_rating 82 | expect(product.avg_rating).to eq(0) 83 | expect(product.reviews_count).to eq(0) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /config/locales/en-GB.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en-GB: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Your Name 7 | title: Title 8 | review: Content 9 | rating: Rating 10 | created_at: Date 11 | ip_address: IP 12 | user: User 13 | models: 14 | spree/review: 15 | one: one review 16 | other: "%{count} reviews" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "You must enter a value for rating." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "You must enter a value for rating." 27 | spree: 28 | approval_status: Approval status 29 | approve: Approve 30 | approved_reviews: Approved 31 | approved_text: Unlocked comments will be published within 1-2 business days. 32 | average_customer_rating: Average rating 33 | back_reviews: Back to the reviews 34 | based_upon_review_count: 35 | one: based upon one review 36 | other: "based upon %{count} reviews" 37 | by: by 38 | editing_review_for_html: 'Editing review for %{product_name}' 39 | error_approve_review: Error approving review 40 | error_no_product: The reviewed product doesn't exist anymore 41 | feedback: Feedback 42 | feedback_review_for: "Review: '%{review}'" 43 | for: for 44 | from: from 45 | info_approve_review: Review approved 46 | leave_us_a_review_for: "Please leave us a review and a rating for our '%{name}'" 47 | no_reviews_available: "No reviews have yet been written for this product." 48 | out_of_5: "out of 5" 49 | rating: rating 50 | reviews: Reviews 51 | admin: 52 | tab: 53 | reviews: Reviews 54 | review_management: Reviews 55 | review_successfully_submitted: Review was successfully submitted 56 | spree_reviews: 57 | feedback_rating: Rate feedback 58 | display_unapproved: Display unapproved reviews in listings 59 | include_unapproved: Include unapproved reviews in listings 60 | manage_review_settings: Control the display of reviews 61 | preview_size: Size of the review snippets 62 | require_login: Require user to be logged in 63 | review_settings: Review Settings 64 | show_email: Show email addresses 65 | show_verified_purchaser: Show verified purchaser 66 | track_locale: Track user's locale 67 | allow_image_upload: Allow images to be attached to reviews 68 | star: 69 | one: "1" 70 | other: "%{count}" 71 | stars: Stars 72 | submit_your_review: Submit your review 73 | submitted_on: Submitted on 74 | unapproved_reviews: Unapproved 75 | verified_purchaser: Verified purchaser 76 | voice: 77 | one: "1 voice" 78 | other: "%{count} voices" 79 | was_this_review_helpful: "Was this review helpful to you?" 80 | write_your_own_review: Write your own review 81 | anonymous: Anonymous 82 | -------------------------------------------------------------------------------- /config/locales/it.yml: -------------------------------------------------------------------------------- 1 | --- 2 | it: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Nome 7 | title: Titolo 8 | review: Contenuto 9 | rating: Valutazione 10 | created_at: Data 11 | ip_address: IP 12 | user: Utente 13 | models: 14 | spree/review: 15 | one: una recensione 16 | other: "%{count} recensioni" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Devi indicare una valutazione per questa recensione." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Devi indicare una valutazione per questa recensione." 27 | spree: 28 | approval_status: Stato 29 | approve: Approva 30 | approved_reviews: Approvate 31 | average_customer_rating: Media recensioni 32 | back_reviews: Torna alle recensioni 33 | based_upon_review_count: 34 | one: basato su una recensione 35 | other: "basato su %{count} recensioni" 36 | by: da 37 | editing_review_for_html: 'Modifica la recensione per %{product_name}' 38 | error_approve_review: Errore approvazione recensione 39 | error_no_product: "Il prodotto recensito non esiste più" 40 | feedback: Feedback 41 | feedback_review_for: "Recensione: '%{review}'" 42 | for: per 43 | from: da 44 | info_approve_review: Recensione approvata 45 | leave_us_a_review_for: "Scrivi una recensione per '%{name}'" 46 | no_reviews_available: "Non ci sono recensioni per questo prodotto." 47 | out_of_5: "su 5" 48 | rating: Valutazione 49 | reviews: Recensioni 50 | admin: 51 | tab: 52 | reviews: Recensioni 53 | review_management: Recensioni 54 | review_successfully_submitted: La recensione è stata inviata con successo 55 | spree_reviews: 56 | feedback_rating: Aggiungi la possibilità di votare una recensione 57 | display_unapproved: Visualizza recensioni non approvate negli elenchi 58 | include_unapproved: Mostra le recensioni non approvate 59 | manage_review_settings: Approva le recensioni prima della pubblicazione 60 | preview_size: Dimensione degli snippet per la recensione 61 | require_login: "L'utente deve essere loggato" 62 | review_settings: Impostazioni recensioni 63 | show_email: "Mostra l'indirizzo email" 64 | show_verified_purchaser: Mostra acquirente verificato 65 | track_locale: "Mostra solo le recensioni con lo stesso locale dell'utente" 66 | star: 67 | one: "1" 68 | other: "%{count}" 69 | stars: Stelle 70 | submit_your_review: Salva la recensione 71 | submitted_on: Salvata su 72 | unapproved_reviews: Non approvate 73 | verified_purchaser: Acquirente verificato 74 | voice: 75 | one: "1 voto" 76 | other: "%{count} voti" 77 | was_this_review_helpful: "Questa recensione è stata utile per te?" 78 | write_your_own_review: "Scrivi la tua recensione" 79 | anonymous: Anonimo 80 | -------------------------------------------------------------------------------- /config/locales/pl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pl: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Imię 7 | title: Tytuł 8 | review: Treść 9 | rating: Ocena 10 | created_at: Utworzone 11 | ip_address: IP 12 | user: Użytkownik 13 | models: 14 | spree/review: 15 | one: jedna opinia 16 | few: "%{count} opinii" 17 | other: "%{count} opinii" 18 | errors: 19 | models: 20 | spree/review: 21 | attributes: 22 | rating: 23 | you_must_enter_value_for_rating: "Musisz wystawić jakąś ocenę." 24 | spree/feedback_review: 25 | attributes: 26 | rating: 27 | you_must_enter_value_for_rating: "Musisz wystawić jakąś ocenę." 28 | spree: 29 | approval_status: Status zatwierdzenia 30 | approve: Zatwierdź 31 | approved_reviews: Zatwierdzone opinie 32 | approved_text: Zatwierdzone opinie zostaną opublikowane w ciągu 1-2 dni roboczych. 33 | average_customer_rating: Średnia ocena 34 | back_reviews: Wróc do opinii 35 | based_upon_review_count: 36 | one: na podstawie jednej opinii 37 | few: "na podstawie %{count} opinii" 38 | other: "na podstawie %{count} opinii" 39 | by: "przez" 40 | editing_review_for_html: 'Edycja opinii dla %{product_name}' 41 | error_approve_review: Błąd przy zatwierdzaniu opinii 42 | error_no_product: Oceniamy produkt nie istnieje 43 | feedback: Komentarz 44 | feedback_review_for: "Komentarz: '%{review}'" 45 | for: dla 46 | from: od 47 | info_approve_review: Opinia zatwierdzona 48 | leave_us_a_review_for: "Dodaj opinię o %{name}" 49 | no_reviews_available: "Ten produkt nie posiada opinii." 50 | out_of_5: "z 5" 51 | rating: Ocena 52 | reviews: Opinie 53 | admin: 54 | tab: 55 | reviews: Opinie 56 | review_management: Opinie 57 | review_successfully_submitted: Opinia została dodana 58 | spree_reviews: 59 | feedback_rating: Oceń komentarz 60 | display_unapproved: Wyświetlaj niezatwierdzone recenzje we wpisach 61 | include_unapproved: Załącz niezaakceptowane do listy 62 | manage_review_settings: Zarządzaj wyśiwetlaniem opinii 63 | preview_size: Liczba opinii na stronie produktu 64 | require_login: "Wymagaj, aby użytkownik był zalogowany" 65 | review_settings: Ustawienia opinii 66 | show_email: Pokaż adresy email 67 | show_verified_purchaser: Pokaż zweryfikowanego nabywcę 68 | track_locale: Śledź język użytkownika 69 | star: 70 | one: "1" 71 | few: "%{count}" 72 | other: "%{count}" 73 | stars: Gwiazdki 74 | submit_your_review: Dodaj opinię 75 | submitted_on: Dodano 76 | unapproved_reviews: Niezaakceptowane 77 | verified_purchaser: Zweryfikowany nabywca 78 | voice: 79 | one: "1 głos" 80 | few: "%{count} głosów" 81 | other: "%{count} głosów" 82 | was_this_review_helpful: "Czy ta opinia była pomocna dla Ciebie?" 83 | write_your_own_review: Napisz swoją opinię 84 | anonymous: Anonimowy 85 | -------------------------------------------------------------------------------- /config/locales/pt-BR.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pt-BR: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: 'Seu Nome' 7 | title: 'Titulo' 8 | review: 'Conteúdo' 9 | rating: 'Avaliação' 10 | created_at: 'Data' 11 | ip_address: 'IP' 12 | user: 'Usuário' 13 | models: 14 | spree/review: 15 | one: 'uma avaliação' 16 | other: "%{count} avaliações" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Você tem que preencher um valor para avaliar." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Você tem que preencher um valor para avaliar." 27 | spree: 28 | approval_status: 'Estado de aprovação' 29 | approve: 'Aprovar' 30 | approved_reviews: 'Aprovado' 31 | approved_text: 'Comentários destravados serão publicados em 1-2 dias.' 32 | average_customer_rating: 'Média de avaliação' 33 | back_reviews: 'Voltar para as avaliações' 34 | based_upon_review_count: 35 | one: 'baseado em uma avaliação' 36 | other: "baseado em %{count} avaliações" 37 | by: 'por' 38 | editing_review_for_html: 'Editando avaliação para %{product_name}' 39 | error_approve_review: 'Erro aprovando avaliação' 40 | error_no_product: "O produto avaliado não existe mais" 41 | feedback: 'Parecer' 42 | feedback_review_for: "Avaliação: '%{review}'" 43 | for: 'para' 44 | from: 'de' 45 | info_approve_review: 'Avaliação aprovada' 46 | leave_us_a_review_for: "Por favor, deixe uma avaliação para nosso %{name}" 47 | no_reviews_available: "Nenhuma avaliação feita ainda para esse produto." 48 | out_of_5: "fora de 5" 49 | rating: 'classificação' 50 | reviews: 'Avaliações' 51 | admin: 52 | tab: 53 | reviews: 'Avaliações' 54 | review_management: 'Avaliações' 55 | review_successfully_submitted: 'Avaliação enviada com sucesso' 56 | spree_reviews: 57 | feedback_rating: 'Classifique um parecer' 58 | display_unapproved: 'Exibir avaliações não aprovadas nas listagens' 59 | include_unapproved: 'Incluir avaliações não aprovadas na listagem' 60 | manage_review_settings: 'Controlar aparições das avaliações' 61 | preview_size: 'Tamanho da avaliação' 62 | require_login: 'É necessário estar logado' 63 | review_settings: 'Configurações de avaliação' 64 | show_email: 'Mostrar endereço de email' 65 | show_verified_purchaser: Mostrar comprador confirmado 66 | track_locale: 'Rastrear localização do usuário' 67 | star: 68 | one: "1" 69 | other: "%{count}" 70 | stars: 'Estrelas' 71 | submit_your_review: 'Enviar sua avaliação' 72 | submitted_on: 'Enviado em' 73 | unapproved_reviews: 'Desaprovado' 74 | verified_purchaser: Comprador verificado 75 | voice: 76 | one: "1 voz" 77 | other: "%{count} vozes" 78 | was_this_review_helpful: 'Essa avaliação foi útil?' 79 | write_your_own_review: 'Escreva sua avaliação' 80 | anonymous: Anônimo 81 | -------------------------------------------------------------------------------- /app/views/spree/admin/review_settings/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= I18n.t("spree.spree_reviews.review_settings") %> 3 | <% end %> 4 | 5 | <% admin_breadcrumb(I18n.t("spree.settings")) %> 6 | 7 | <% content_for :tabs do %> 8 | 13 | <% end %> 14 | 15 | <%= form_tag(admin_review_settings_path, method: :put) do %> 16 |
    17 |
    18 | 22 |
    23 |
    24 | 28 |
    29 |
    30 | 34 |
    35 |
    36 | 40 |
    41 |
    42 | 46 |
    47 |
    48 | 52 |
    53 |
    54 | 58 |
    59 |
    60 | 64 |
    65 | 66 |
    67 |
    68 | <%= text_field_tag('preferences[preview_size]', Spree::Reviews::Config[:preview_size], size: 3) %> 69 |
    70 | 71 |
    72 | <%= button_tag I18n.t("spree.actions.update") %> 73 | <%= link_to I18n.t("spree.actions.cancel"), edit_admin_general_settings_url, class: 'button' %> 74 |
    75 |
    76 | <% end %> 77 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ru: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | title: Заголовок 7 | review: Содержание 8 | rating: Рейтинг 9 | name: Ваше имя 10 | created_at: Дата отзыва 11 | ip_address: IP 12 | user: Пользователь 13 | models: 14 | spree/review: 15 | zero: "0 отзывов" 16 | one: "%{count} отзыв" 17 | few: "%{count} отзыва" 18 | other: "%{count} отзывов" 19 | many: "%{count} отзыва" 20 | errors: 21 | models: 22 | spree/review: 23 | attributes: 24 | rating: 25 | you_must_enter_value_for_rating: "Вы должны оценить отзыв." 26 | spree/feedback_review: 27 | attributes: 28 | rating: 29 | you_must_enter_value_for_rating: "Вы должны оценить отзыв." 30 | spree: 31 | reviews: Отзывы 32 | write_your_own_review: Оставьте свой отзыв 33 | admin: 34 | tab: 35 | reviews: Отзывы 36 | review_management: Управление отзывами 37 | back_reviews: Назад к отзывам 38 | info_approve_review: Отзыв одобрен 39 | error_approve_review: Ошибка при одобрении отзыва 40 | approve: Одобрить 41 | leave_us_a_review_for: "Пожалуйста, оставьте отзыв и оценку для" 42 | review_successfully_submitted: Ваш отзыв был успешно отправлен 43 | rating: Оценка 44 | submit_your_review: Оставить отзыв 45 | spree_reviews: 46 | review_settings: Настройки отзывов покупателей 47 | manage_review_settings: Управление отображением отзывов 48 | preview_size: Количество отзывов на странице товара 49 | display_unapproved: Показать неутвержденные отзывы в списках 50 | include_unapproved: Показывать неодобренные отзывы на странице товара 51 | your_name: Ваше имя 52 | your_location: Город 53 | feedback_rating: Рейтинг отзыва 54 | show_email: Показывать электронную почту 55 | show_verified_purchaser: Показать подтвержденного покупателя 56 | require_login: Требовать вход 57 | track_locale: Отслеживать язык 58 | average_customer_rating: Средняя оценка 59 | approved_text: Разблокированные комментарии будут опубликованы в течение 1-2 рабочих дней. 60 | for: для 61 | approval_status: Статус отзывов 62 | unapproved_reviews: Неутверждённые отзывы 63 | approved_reviews: Утверждённые отзывы 64 | from: от 65 | by: "" 66 | submitted_on: Добавлено 67 | based_upon: "на основе" 68 | verified_purchaser: Проверенный покупатель 69 | voice: 70 | zero: "0 голосов" 71 | one: "%{count} голос" 72 | few: "%{count} голоса" 73 | other: "%{count} голосов" 74 | many: "%{count} голоса" 75 | star: "%{count}" 76 | feedback: Обратная связь 77 | stars: Звёзды 78 | feedback_review_for: "Отзывы: '%{review}'" 79 | out_of_5: "из 5" 80 | was_this_review_helpful: "Был ли Вам полезен этот отзыв?" 81 | editing_review_for_html: 'Редактирование отзыва для HTML' 82 | no_reviews_available: "Отзывов нет" 83 | based_upon_review_count: "На основании всех отзывов" 84 | error_no_product: Отзывы продукт больше не существует 85 | anonymous: анонимный 86 | -------------------------------------------------------------------------------- /config/locales/pt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pt: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Nome 7 | title: Título Avaliação 8 | review: Conteúdo 9 | rating: Avaliação 10 | created_at: Data 11 | ip_address: IP 12 | user: Utilizador 13 | models: 14 | spree/review: 15 | one: uma avaliação 16 | other: "%{count} avaliações" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Tem de incluir uma pontuação." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Tem de incluir uma pontuação." 27 | spree: 28 | approval_status: Estado de aprovação 29 | approve: Aprovar 30 | approved_reviews: Avaliações aprovadas 31 | approved_text: As avaliações podem demorar 1-2 dias úteis a ser publicadas. 32 | average_customer_rating: Pontuação média dos utilizadores fruga 33 | back_reviews: Voltar para as avaliações 34 | based_upon_review_count: 35 | one: baseada numa avaliação 36 | other: "baseada em %{count} avaliações" 37 | by: por 38 | editing_review_for_html: 'Editando a avaliação para %{product_name}' 39 | error_approve_review: Erro aprovando a avaliação 40 | error_no_product: O produto avaliado já não existe 41 | feedback: Feedback 42 | feedback_review_for: "Avaliação: '%{review}'" 43 | for: para 44 | from: de 45 | info_approve_review: Avaliação aprovada 46 | leave_us_a_review_for: "Por favor, deixe-nos uma avaliação para produto %{name}" 47 | no_reviews_available: "Não foram ainda feitas avaliações para este produto." 48 | out_of_5: "de 5" 49 | rating: pontuação 50 | reviews: Avaliações 51 | admin: 52 | tab: 53 | reviews: Gestão Avaliações 54 | review_management: Gestão Avaliações 55 | review_successfully_submitted: A avaliação foi enviada com sucesso 56 | submission_guidelines: Diretrizes para enviar boas avaliações 57 | spree_reviews: 58 | feedback_rating: Pontuação de feedback 59 | display_unapproved: Exibir avaliações não aprovadas nas listagens 60 | include_unapproved: Incluir avaliaçoes não-aprovadas nas lista 61 | manage_review_settings: Gerir a publicação de avaliações 62 | preview_size: Tamanho da versão resumida de avaliações 63 | require_login: Requerer que utilizador esteja registado 64 | review_settings: Configurações de Avaliação 65 | show_email: Mostrar email 66 | show_verified_purchaser: Mostrar comprador confirmado 67 | track_locale: Seguir locale do utilizador 68 | star: 69 | one: "1" 70 | other: "%{count}" 71 | stars: Stars 72 | submit_your_review: Envia tua avaliação 73 | submitted_on: Enviada em 74 | unapproved_reviews: Não aprovadas 75 | verified_purchaser: Comprador verificado 76 | voice: 77 | one: "uma opinião" 78 | other: "%{count} opiniões" 79 | was_this_review_helpful: "Esta avaliação foi util?" 80 | write_your_own_review: Escreve a tua própria avaliação 81 | anonymous: Anônimo 82 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fr: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Votre nom 7 | title: Titre 8 | review: Contenu 9 | rating: Note 10 | created_at: Date 11 | ip_address: IP 12 | user: Utilisateur 13 | models: 14 | spree/review: 15 | one: un commentaire 16 | other: "%{count} commentaires" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Vous devez entrer une valeur pour noter." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Vous devez entrer une valeur pour noter." 27 | spree: 28 | approval_status: Statut de l'approbation 29 | approve: Approuver 30 | approved_reviews: Approuvé 31 | approved_text: Les commentaires authorisés seront affichés dans un délai de un à deux jours ouvrables. 32 | average_customer_rating: Note moyenne 33 | back_reviews: Retour aux commentaires 34 | based_upon_review_count: 35 | one: basé sur un commentaire 36 | other: "basé sur %{count} commentaires" 37 | by: par 38 | editing_review_for_html: 'Modification du commentaire pour %{product_name}' 39 | error_approve_review: Erreur en approuvant le commentaire 40 | error_no_product: Le produit commenté n'existe plus 41 | feedback: Réaction 42 | feedback_review_for: "Commentaire: '%{review}'" 43 | for: pour 44 | from: de la part de 45 | info_approve_review: Commentaire approuvé 46 | leave_us_a_review_for: "Veuillez laisser un commentaire et une note pour le produit : %{name}" 47 | no_reviews_available: "Il n'y pas encore de commentaire pour ce produit." 48 | out_of_5: "sur 5" 49 | rating: note 50 | reviews: Commentaires 51 | admin: 52 | tab: 53 | reviews: Commentaires 54 | review_management: Commentaires 55 | review_successfully_submitted: Votre commentaire a été envoyé avec succès 56 | spree_reviews: 57 | feedback_rating: Note de réaction 58 | display_unapproved: Afficher les commentaires non approuvés dans les listes 59 | include_unapproved: Afficher les commentaires non modérés 60 | manage_review_settings: Contrôler l'affichage des commentaires 61 | preview_size: Taille des extraits de commentaires 62 | require_login: Demander à l'utilisateur d'être connecté 63 | review_settings: Paramètres des commentaires 64 | show_email: Montrer l'adresse de courriel 65 | show_verified_purchaser: Montrer l'acheteur vérifié 66 | track_locale: Détécter la langue de l'utilisateur 67 | star: 68 | one: "1" 69 | other: "%{count}" 70 | stars: Étoiles 71 | submit_your_review: Envoyer votre commentaire 72 | submitted_on: Envoyé le 73 | unapproved_reviews: Non approuvé 74 | verified_purchaser: Acheteur vérifié 75 | voice: 76 | one: "1 voix" 77 | other: "%{count} voix" 78 | was_this_review_helpful: "Est-ce que ce commentaire vous a été utile ?" 79 | write_your_own_review: Écrire un commentaire 80 | anonymous: Anonyme 81 | -------------------------------------------------------------------------------- /config/locales/uk.yml: -------------------------------------------------------------------------------- 1 | --- 2 | uk: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Ваше ім’я 7 | title: Заголовок 8 | review: Відгук 9 | rating: Оцінка 10 | created_at: Дата 11 | ip_address: IP 12 | user: Користувач 13 | show_identifier: Показувати автора 14 | models: 15 | spree/review: 16 | one: один відгук 17 | few: '%{count} відгуки' 18 | many: '%{count} відгуків' 19 | other: '%{count} відгуків' 20 | errors: 21 | models: 22 | spree/review: 23 | attributes: 24 | rating: 25 | you_must_enter_value_for_rating: 'Потрібно вибрати оцінку.' 26 | spree/feedback_review: 27 | attributes: 28 | rating: 29 | you_must_enter_value_for_rating: 'Потрібно вибрати оцінку.' 30 | spree: 31 | approval_status: Підтвердження 32 | approve: Схвалити 33 | approved_reviews: Схвалено 34 | average_customer_rating: Середня оцінка 35 | back_reviews: Назад до відгуків 36 | based_upon_review_count: 37 | one: на основі одного відгуку 38 | few: "на основі %{count} відгуків" 39 | many: "на основі %{count} відгуків" 40 | other: "на основі %{count} відгуків" 41 | by: від 42 | editing_review_for_html: 'Редагувати відгук на %{product_name}' 43 | error_approve_review: Не вдалось схвалити відгук 44 | error_no_product: Товару, на який писався відгук, більше не існує 45 | feedback: Корисність 46 | feedback_review_for: "Відгук: '%{review}'" 47 | for: для 48 | from: від 49 | info_approve_review: Відгук схвалено 50 | leave_us_a_review_for: "Будь ласка, залиште відгук та оцінку для '%{name}'" 51 | no_reviews_available: 'Поки що немає відгуків на цей товар.' 52 | out_of_5: 'з 5' 53 | rating: оцінка 54 | reviews: Відгуки 55 | admin: 56 | tab: 57 | reviews: Відгуки 58 | review_management: Відгуки 59 | review_successfully_submitted: Відгук надіслано 60 | spree_reviews: 61 | feedback_rating: Оцінювати корисність 62 | display_unapproved: Показувати в списках не схвалені відгуки 63 | include_unapproved: Показувати незатверджені відгуки 64 | manage_review_settings: Керувати відображенням відгуків 65 | preview_size: Розмір скороченої версії 66 | require_login: Тільки для зареєстрованих користувачів 67 | review_settings: Налаштування відгуків 68 | show_email: Показувати email 69 | show_verified_purchaser: Показати підтвердженого покупця 70 | track_locale: Зберігати мову користувача 71 | star: 72 | one: '1' 73 | few: '%{count}' 74 | many: '%{count}' 75 | other: '%{count}' 76 | stars: Зірочок 77 | submit_your_review: Надіслати відгук 78 | submitted_on: Написано 79 | unapproved_reviews: Непідтверджений 80 | verified_purchaser: Перевірений покупець 81 | voice: 82 | one: '1 голос' 83 | few: '%{count} голоси' 84 | many: '%{count} голосів' 85 | other: '%{count} голосів' 86 | was_this_review_helpful: 'Цей відгук корисний для вас?' 87 | write_your_own_review: Напишіть свій відгук 88 | anonymous: Анонім 89 | -------------------------------------------------------------------------------- /lib/controllers/spree/api/feedback_reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module Api 5 | class FeedbackReviewsController < Spree::Api::BaseController 6 | respond_to :json 7 | 8 | before_action :load_review, only: [:create, :update, :destroy] 9 | before_action :load_feedback_review, only: [:update, :destroy] 10 | before_action :find_review_user 11 | before_action :sanitize_rating, only: [:create, :update] 12 | before_action :prevent_multiple_feedback_reviews, only: [:create] 13 | 14 | def create 15 | if @review.present? 16 | @feedback_review = @review.feedback_reviews.new(feedback_review_params) 17 | @feedback_review.user = @current_api_user 18 | @feedback_review.locale = I18n.locale.to_s if Spree::Reviews::Config[:track_locale] 19 | end 20 | 21 | authorize! :create, @feedback_review 22 | if @feedback_review.save 23 | render json: @feedback_review, status: :created 24 | else 25 | invalid_resource!(@feedback_review) 26 | end 27 | end 28 | 29 | def update 30 | authorize! :update, @feedback_review 31 | 32 | if @feedback_review.update(feedback_review_params) 33 | render json: @feedback_review, status: :ok 34 | else 35 | invalid_resource!(@feedback_review) 36 | end 37 | end 38 | 39 | def destroy 40 | authorize! :destroy, @feedback_review 41 | 42 | if @feedback_review.destroy 43 | render json: @feedback_review, status: :ok 44 | else 45 | invalid_resource!(@feedback_review) 46 | end 47 | end 48 | 49 | private 50 | 51 | def permitted_feedback_review_attributes 52 | [:rating, :comment] 53 | end 54 | 55 | def feedback_review_params 56 | params.require(:feedback_review).permit(permitted_feedback_review_attributes) 57 | end 58 | 59 | # Finds user based on api_key or by user_id if api_key belongs to an admin. 60 | def find_review_user 61 | if params[:user_id] && @current_user_roles.include?('admin') 62 | @current_api_user = Spree.user_class.find(params[:user_id]) 63 | end 64 | end 65 | 66 | # Loads any review that is shared between the user and product 67 | def load_review 68 | @review = Spree::Review.find(params[:review_id]) 69 | end 70 | 71 | # Loads the feedback_review 72 | def load_feedback_review 73 | @feedback_review = Spree::FeedbackReview.find(params[:id]) 74 | end 75 | 76 | # Ensures that a user can't leave multiple feedbacks on a single review 77 | def prevent_multiple_feedback_reviews 78 | @feedback_review = @review.feedback_reviews.find_by(user_id: @current_api_user) 79 | if @feedback_review.present? 80 | invalid_resource!(@feedback_review) 81 | end 82 | end 83 | 84 | # Converts rating strings like "5 units" to "5" 85 | # Operates on params 86 | def sanitize_rating 87 | params[:rating].to_s.dup.sub!(/\s*[^0-9]*\z/, '') unless params[:feedback_review] && params[:feedback_review][:rating].blank? 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Name 7 | title: Überschrift 8 | review: Inhalt 9 | rating: Bewertung 10 | created_at: Erstellt am 11 | ip_address: IP 12 | user: Benutzer 13 | locale: Sprache 14 | models: 15 | spree/review: 16 | one: eine Rezension 17 | other: "%{count} Rezensionen" 18 | errors: 19 | models: 20 | spree/review: 21 | attributes: 22 | rating: 23 | you_must_enter_value_for_rating: "Bitte geben Sie eine Bewertung an." 24 | spree/feedback_review: 25 | attributes: 26 | rating: 27 | you_must_enter_value_for_rating: "Bitte geben Sie eine Bewertung an." 28 | spree: 29 | approval_status: Freischaltungsstatus 30 | approve: Freischalten 31 | approved_reviews: Freigeschaltete Rezensionen 32 | approved_text: Freigeschalte Kommentare werden innerhalb von 1-2 Werktagen veröffentlicht. 33 | average_customer_rating: Durchschnittliche Bewertung 34 | back_reviews: Zurück zu den Rezensionen 35 | based_upon_review_count: 36 | one: basiert auf einer Rezension 37 | other: "basiert auf %{count} Rezensionen" 38 | by: Von 39 | editing_review_for_html: 'Rezension über %{product_name} bearbeiten' 40 | error_approve_review: Fehler bei der Freischaltung 41 | error_no_product: Das rezensierte Produkt existiert nicht mehr. 42 | feedback: Feedback 43 | feedback_review_for: "Rezension: '%{review}'" 44 | for: über 45 | from: aus 46 | info_approve_review: Die Rezension wurde freigeschaltet. 47 | leave_us_a_review_for: "Ihre Rezension über %{name}" 48 | no_reviews_available: "Es sind noch keine Rezensionen für dieses Produkt vorhanden." 49 | out_of_5: "von 5" 50 | rating: Bewertung 51 | reviews: Rezensionen 52 | admin: 53 | tab: 54 | reviews: Rezensionen 55 | review_management: Rezensionen 56 | review_successfully_submitted: Ihre Rezension wurde erfolgreich übertragen. Vielen Dank! 57 | spree_reviews: 58 | feedback_rating: Feedback für Rezensionen erlauben 59 | display_unapproved: Zeigen Sie nicht genehmigte Bewertungen in Auflistungen an 60 | include_unapproved: Unveröffentlichte Rezensionen anzeigen 61 | manage_review_settings: Einstellungen für die Darstellung von Rezensionen 62 | preview_size: Größe der Vorschau 63 | require_login: Login erforderlich 64 | review_settings: Rezensionseinstellungen 65 | show_email: E-Mail Adressen anzeigen 66 | show_verified_purchaser: Verifizierten Käufer anzeigen 67 | track_locale: Sprache des Benutzers tracken 68 | star: 69 | one: "ein Sternchen" 70 | other: "%{count} Sternchen" 71 | stars: Sternchen 72 | submit_your_review: Rezension abschicken 73 | submitted_on: Eingeschickt am 74 | unapproved_reviews: Nicht freigeschaltete Rezensionen 75 | verified_purchaser: Verifizierter Käufer 76 | voice: 77 | one: "Eine Stimme" 78 | other: "%{count} Stimmen" 79 | was_this_review_helpful: War diese Rezension für Sie hilfreich? 80 | write_your_own_review: Rezension schreiben 81 | anonymous: Anonym 82 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | --- 2 | es: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Tu Nombre 7 | title: Título 8 | review: Contenido 9 | rating: Puntuación 10 | created_at: Fecha 11 | ip_address: IP 12 | user: Usuario 13 | models: 14 | spree/review: 15 | one: una valoración 16 | other: "%{count} valoraciones" 17 | errors: 18 | models: 19 | spree/review: 20 | attributes: 21 | rating: 22 | you_must_enter_value_for_rating: "Debes incluir una puntuación." 23 | spree/feedback_review: 24 | attributes: 25 | rating: 26 | you_must_enter_value_for_rating: "Debes incluir una puntuación." 27 | spree: 28 | approval_status: Estado de aprobación 29 | approve: Aprobar 30 | approved_reviews: Aprobado 31 | approved_text: "Los comentarios desbloqueados serán publicados en 1-2 días laborales." 32 | average_customer_rating: "Puntuación media" 33 | back_reviews: "Volver a las valoraciones" 34 | based_upon_review_count: 35 | one: "basado en una valoración" 36 | other: "basado en %{count} valoraciones" 37 | by: por 38 | editing_review_for_html: 'Editando la valoración para %{product_name}' 39 | error_approve_review: "Error aprobando la valoración" 40 | error_no_product: "El producto valorado ya no existe" 41 | feedback: Feedback 42 | feedback_review_for: "Review: '%{review}'" 43 | for: para 44 | from: desde 45 | info_approve_review: "Valoración aprobada" 46 | leave_us_a_review_for: "Por favor, déjanos una valoración para %{name}" 47 | no_reviews_available: "No hay ninguna valoración para este producto." 48 | out_of_5: "de 5" 49 | rating: puntuación 50 | reviews: Valoraciones 51 | admin: 52 | tab: 53 | reviews: Valoraciones 54 | review_management: "Valoraciones" 55 | review_successfully_submitted: "La valoración se ha enviado correctamente" 56 | spree_reviews: 57 | feedback_rating: "Puntuación en el feedback" 58 | display_unapproved: "Mostrar comentarios no aprobados en los listados" 59 | include_unapproved: "Incluir en los listados las valoraciones sin aprobar" 60 | manage_review_settings: "Gestionar la publicación de valoraciones" 61 | preview_size: "Tamaño de la versión resumida de la valoración" 62 | require_login: "Requerir que el usuario esté logado" 63 | review_settings: "Configuración de las Valoraciones" 64 | show_email: "Mostrar las direcciones de correo" 65 | show_verified_purchaser: Mostrar comprador verificado 66 | track_locale: "Rastrear el locale del usuario" 67 | allow_image_upload: "Permitir adjuntar imagenes con el comentario" 68 | star: 69 | one: "1" 70 | other: "%{count}" 71 | stars: Estrellas 72 | submit_your_review: "Envía tu valoración" 73 | submitted_on: "Enviada el" 74 | unapproved_reviews: "Sin aprobar" 75 | verified_purchaser: Comprador verificado 76 | voice: 77 | one: "1 voz" 78 | other: "%{count} voces" 79 | was_this_review_helpful: "¿Te ha sido útil esta valoración?" 80 | write_your_own_review: "Escribe tu propia valoración" 81 | anonymous: Anónimo 82 | -------------------------------------------------------------------------------- /config/locales/de-CH.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de-CH: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Name 7 | title: Überschrift 8 | review: Inhalt 9 | rating: Bewertung 10 | created_at: Erstellt am 11 | ip_address: IP 12 | user: Benutzer 13 | locale: Sprache 14 | models: 15 | spree/review: 16 | one: eine Rezension 17 | other: "%{count} Rezensionen" 18 | errors: 19 | models: 20 | spree/review: 21 | attributes: 22 | rating: 23 | you_must_enter_value_for_rating: "Bitte geben Sie eine Bewertung an." 24 | spree/feedback_review: 25 | attributes: 26 | rating: 27 | you_must_enter_value_for_rating: "Bitte geben Sie eine Bewertung an." 28 | spree: 29 | approval_status: Freischaltungsstatus 30 | approve: Freischalten 31 | approved_reviews: Freigeschaltete Rezensionen 32 | approved_text: Freigeschalte Kommentare werden innerhalb von 1-2 Werktagen veröffentlicht. 33 | average_customer_rating: Durchschnittliche Bewertung 34 | back_reviews: Zurück zu den Rezensionen 35 | based_upon_review_count: 36 | one: basiert auf einer Rezension 37 | other: "basiert auf %{count} Rezensionen" 38 | by: Von 39 | editing_review_for_html: 'Rezension über %{product_name} bearbeiten' 40 | error_approve_review: Fehler bei der Freischaltung 41 | error_no_product: Das rezensierte Produkt existiert nicht mehr. 42 | feedback: Feedback 43 | feedback_review_for: "Rezension: '%{review}'" 44 | for: über 45 | from: aus 46 | info_approve_review: Die Rezension wurde freigeschaltet. 47 | leave_us_a_review_for: "Ihre Rezension über %{name}" 48 | no_reviews_available: "Es sind noch keine Rezensionen für dieses Produkt vorhanden." 49 | out_of_5: "von 5" 50 | rating: Bewertung 51 | reviews: Rezensionen 52 | admin: 53 | tab: 54 | reviews: Rezensionen 55 | review_management: Rezensionen 56 | review_successfully_submitted: "Ihre Rezension wurde erfolgreich übertragen. Vielen Dank!" 57 | spree_reviews: 58 | feedback_rating: Feedback für Rezensionen erlauben 59 | display_unapproved: Zeigen Sie nicht genehmigte Bewertungen in Auflistungen an 60 | include_unapproved: Unveröffentlichte Rezensionen anzeigen 61 | manage_review_settings: Einstellungen für die Darstellung von Rezensionen 62 | preview_size: Größe der Vorschau 63 | require_login: Login erforderlich 64 | review_settings: Rezensionseinstellungen 65 | show_email: E-Mail Adressen anzeigen 66 | show_verified_purchaser: Verifizierten Käufer anzeigen 67 | track_locale: Sprache des Benutzers tracken 68 | star: 69 | one: "ein Sternchen" 70 | other: "%{count} Sternchen" 71 | stars: Sternchen 72 | submit_your_review: Rezension abschicken 73 | submitted_on: Eingeschickt am 74 | unapproved_reviews: Nicht freigeschaltete Rezensionen 75 | verified_purchaser: Verifizierter Käufer 76 | voice: 77 | one: "Eine Stimme" 78 | other: "%{count} Stimmen" 79 | was_this_review_helpful: War diese Rezension für Sie hilfreich? 80 | write_your_own_review: Rezension schreiben 81 | anonymous: Anonym 82 | -------------------------------------------------------------------------------- /spec/models/feedback_review_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::FeedbackReview do 6 | context 'validations' do 7 | it 'validates by default' do 8 | expect(build(:feedback_review)).to be_valid 9 | end 10 | 11 | it 'validates with a nil user' do 12 | expect(build(:feedback_review, user: nil)).to be_valid 13 | end 14 | 15 | it 'does not validate with a nil review' do 16 | expect(build(:feedback_review, review: nil)).not_to be_valid 17 | end 18 | 19 | context 'rating' do 20 | it 'does not validate when no rating is specified' do 21 | expect(build(:feedback_review, rating: nil)).not_to be_valid 22 | end 23 | 24 | it 'does not validate when the rating is not a number' do 25 | expect(build(:feedback_review, rating: 'not_a_number')).not_to be_valid 26 | end 27 | 28 | it 'does not validate when the rating is a float' do 29 | expect(build(:feedback_review, rating: 2.718)).not_to be_valid 30 | end 31 | 32 | it 'does not validate when the rating is less than 1' do 33 | expect(build(:feedback_review, rating: 0)).not_to be_valid 34 | expect(build(:feedback_review, rating: -5)).not_to be_valid 35 | end 36 | 37 | it 'does not validate when the rating is greater than 5' do 38 | expect(build(:feedback_review, rating: 6)).not_to be_valid 39 | expect(build(:feedback_review, rating: 8)).not_to be_valid 40 | end 41 | 42 | (1..5).each do |i| 43 | it "validates when the rating is #{i}" do 44 | expect(build(:feedback_review, rating: i)).to be_valid 45 | end 46 | end 47 | end 48 | end 49 | 50 | context 'scopes' do 51 | context 'most_recent_first' do 52 | let!(:feedback_review_1) { create(:feedback_review, created_at: 10.days.ago) } 53 | let!(:feedback_review_2) { create(:feedback_review, created_at: 2.days.ago) } 54 | let!(:feedback_review_3) { create(:feedback_review, created_at: 5.days.ago) } 55 | 56 | it 'properly runs most_recent_first queries' do 57 | expect(described_class.most_recent_first.to_a).to eq([feedback_review_2, feedback_review_3, feedback_review_1]) 58 | end 59 | 60 | it 'defaults to most_recent_first queries' do 61 | expect(described_class.all.to_a).to eq([feedback_review_2, feedback_review_3, feedback_review_1]) 62 | end 63 | end 64 | 65 | context 'localized' do 66 | let!(:en_feedback_review_1) { create(:feedback_review, locale: 'en', created_at: 10.days.ago) } 67 | let!(:en_feedback_review_2) { create(:feedback_review, locale: 'en', created_at: 2.days.ago) } 68 | let!(:en_feedback_review_3) { create(:feedback_review, locale: 'en', created_at: 5.days.ago) } 69 | 70 | let!(:es_feedback_review_1) { create(:feedback_review, locale: 'es', created_at: 10.days.ago) } 71 | let!(:fr_feedback_review_1) { create(:feedback_review, locale: 'fr', created_at: 10.days.ago) } 72 | 73 | it 'properly runs localized queries' do 74 | expect(described_class.localized('en').to_a).to eq([en_feedback_review_2, en_feedback_review_3, en_feedback_review_1]) 75 | expect(described_class.localized('es').to_a).to eq([es_feedback_review_1]) 76 | expect(described_class.localized('fr').to_a).to eq([fr_feedback_review_1]) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /config/locales/ro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ro: 3 | activerecord: 4 | attributes: 5 | spree/review: 6 | name: Nume 7 | title: Titlu 8 | review: Conținut 9 | rating: Evaluare 10 | created_at: Data 11 | ip_address: IP 12 | user: Utilizator 13 | models: 14 | spree/review: 15 | one: o recenzie 16 | few: "%{count} recenzii" 17 | other: "%{count} recenzii" 18 | many: "%{count} de recenzii" 19 | errors: 20 | models: 21 | spree/review: 22 | attributes: 23 | rating: 24 | you_must_enter_value_for_rating: "Trebuie să introduci o valoare pentru evaluare." 25 | spree/feedback_review: 26 | attributes: 27 | rating: 28 | you_must_enter_value_for_rating: "Trebuie să introduci o valoare pentru evaluare." 29 | spree: 30 | approval_status: Starea de aprobare 31 | approve: Aprobă 32 | approved_reviews: Aprobate 33 | approved_text: Comentariile deblocate vor fi publicate în 1-2 zile lucrătoare. 34 | average_customer_rating: Evaluare media 35 | back_reviews: Înapoi la recenzii 36 | based_upon_review_count: 37 | one: bazată pe o recenzie 38 | few: "bazată pe %{count} recenzii" 39 | other: "bazată pe %{count} recenzii" 40 | many: "bazată pe %{count} de recenzii" 41 | by: de 42 | editing_review_for_html: 'Editare recenzie pentru %{product_name}' 43 | error_approve_review: Eroare la aprobarea recenziei 44 | error_no_product: Produsul recenziat nu mai există 45 | feedback: Feedback 46 | feedback_review_for: "Recenzie: '%{review}'" 47 | for: pentru 48 | from: de la 49 | info_approve_review: Recenzie aprobată 50 | leave_us_a_review_for: "Vă rugăm să publicați o recenzie și să evaluați '%{name}'" 51 | no_reviews_available: "Încă nu au fost scrise recenzii pentru acest produs." 52 | out_of_5: "din 5" 53 | rating: evaluare 54 | reviews: Recenzii 55 | admin: 56 | tab: 57 | reviews: Recenzii 58 | review_management: Recenzii 59 | review_successfully_submitted: Recenzia a fost trimisă cu succes 60 | spree_reviews: 61 | feedback_rating: Evaluează feedback 62 | display_unapproved: Afișați recenzii neaprobate în înregistrări 63 | include_unapproved: Includeți recenzii neaprobate în listări 64 | manage_review_settings: Controlează afișarea recenziilor 65 | preview_size: Mărimea fragmentului recenziei 66 | require_login: Solicită ca utilizatorul să fie autentificat 67 | review_settings: Setări recenzii 68 | show_email: Afișează adresa de email 69 | show_verified_purchaser: Afișați cumpărătorul verificat 70 | track_locale: Urmărește setarea de limbă a utilizatorului 71 | star: 72 | one: "o stea" 73 | few: "%{count} stele" 74 | other: "%{count} stele" 75 | many: "%{count} de stele" 76 | stars: Stele 77 | submit_your_review: Trimite recenzia 78 | submitted_on: Trimisă la 79 | unapproved_reviews: Neaprobat 80 | verified_purchaser: Achizitor verificat 81 | voice: 82 | one: "o opinie" 83 | few: "%{count} opinii" 84 | other: "%{count} opinii" 85 | many: "%{count} de opinii" 86 | was_this_review_helpful: "A fost de ajutor această recenzie?" 87 | write_your_own_review: Scrie propria ta recenzie 88 | anonymous: anonim 89 | -------------------------------------------------------------------------------- /lib/controllers/spree/api/reviews_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Spree 4 | module Api 5 | class ReviewsController < Spree::Api::BaseController 6 | respond_to :json 7 | 8 | before_action :load_review, only: [:show, :update, :destroy] 9 | before_action :load_product, :find_review_user 10 | before_action :sanitize_rating, only: [:create, :update] 11 | before_action :prevent_multiple_reviews, only: [:create] 12 | 13 | def index 14 | @reviews = if @product 15 | Spree::Review.default_approval_filter.where(product: @product) 16 | else 17 | Spree::Review.where(user: @current_api_user) 18 | end 19 | 20 | respond_with(@reviews) 21 | end 22 | 23 | def show 24 | authorize! :read, @review 25 | render json: @review, include: [:images, :feedback_reviews] 26 | end 27 | 28 | def create 29 | return not_found if @product.nil? 30 | 31 | @review = Spree::Review.new(review_params) 32 | @review.product = @product 33 | @review.user = @current_api_user 34 | @review.ip_address = request.remote_ip 35 | @review.locale = I18n.locale.to_s if Spree::Reviews::Config[:track_locale] 36 | 37 | authorize! :create, @review 38 | if @review.save 39 | render json: @review, include: [:images, :feedback_reviews], status: :created 40 | else 41 | invalid_resource!(@review) 42 | end 43 | end 44 | 45 | def update 46 | authorize! :update, @review 47 | 48 | attributes = review_params.merge(ip_address: request.remote_ip, approved: false) 49 | 50 | if @review.update(attributes) 51 | render json: @review, include: [:images, :feedback_reviews], status: :ok 52 | else 53 | invalid_resource!(@review) 54 | end 55 | end 56 | 57 | def destroy 58 | authorize! :destroy, @review 59 | 60 | if @review.destroy 61 | render json: @review, status: :ok 62 | else 63 | invalid_resource!(@review) 64 | end 65 | end 66 | 67 | private 68 | 69 | def permitted_review_attributes 70 | [:product_id, :rating, :title, :review, :name, :show_identifier] 71 | end 72 | 73 | def review_params 74 | params.permit(permitted_review_attributes) 75 | end 76 | 77 | # Loads product from product id. 78 | def load_product 79 | @product = if params[:product_id] 80 | Spree::Product.friendly.find(params[:product_id]) 81 | else 82 | @review&.product 83 | end 84 | end 85 | 86 | # Finds user based on api_key or by user_id if api_key belongs to an admin. 87 | def find_review_user 88 | if params[:user_id] && @current_user_roles.include?('admin') 89 | @current_api_user = Spree.user_class.find(params[:user_id]) 90 | end 91 | end 92 | 93 | # Loads any review that is shared between the user and product 94 | def load_review 95 | @review = Spree::Review.find(params[:id]) 96 | end 97 | 98 | # Ensures that a user can't create more than 1 review per product 99 | def prevent_multiple_reviews 100 | @review = @current_api_user.reviews.find_by(product: @product) 101 | if @review.present? 102 | invalid_resource!(@review) 103 | end 104 | end 105 | 106 | # Converts rating strings like "5 units" to "5" 107 | # Operates on params 108 | def sanitize_rating 109 | params[:rating].sub!(/\s*[^0-9]*\z/, '') if params[:rating].present? 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/controllers/spree/api/feedback_reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Api::FeedbackReviewsController, type: :controller do 6 | render_views 7 | 8 | let!(:user) { create(:user) } 9 | let!(:review) { create(:review) } 10 | let!(:feedback_review) { create(:feedback_review, review: review) } 11 | 12 | before do 13 | user.generate_spree_api_key! 14 | end 15 | 16 | describe '#create' do 17 | subject do 18 | params = { review_id: review.id, token: user.spree_api_key, format: 'json' }.merge(feedback_review_params) 19 | post :create, params: params 20 | JSON.parse(response.body) 21 | end 22 | 23 | let(:feedback_review_params) do 24 | { 25 | "feedback_review": { 26 | "rating": "5", 27 | "comment": "I agree with what you said" 28 | } 29 | } 30 | end 31 | 32 | context 'when user has already left feedback on a reviewed this product' do 33 | before do 34 | feedback_review.update(user_id: user.id) 35 | end 36 | 37 | it 'returns with a fail' do 38 | expect(subject["error"]).not_to be_empty 39 | expect(subject["error"]).to match(/invalid resource/i) 40 | end 41 | end 42 | 43 | context 'when it is a users first feedback for a review' do 44 | it 'returns success with feedback' do 45 | expect(subject).not_to be_empty 46 | expect(subject["review_id"]).to eq(review.id) 47 | expect(subject["rating"]).to eq(5) 48 | expect(subject["comment"]).to eq("I agree with what you said") 49 | end 50 | 51 | it 'updates the review' do 52 | expect(review).to receive(:touch) 53 | feedback = create(:feedback_review, review: review) 54 | feedback.save! 55 | end 56 | end 57 | end 58 | 59 | describe '#update' do 60 | subject do 61 | put :update, params: params 62 | JSON.parse(response.body) 63 | end 64 | 65 | before { feedback_review.update(user_id: user.id) } 66 | 67 | let(:params) { { review_id: review.id, id: feedback_review.id, token: user.spree_api_key, format: 'json' }.merge(feedback_review_params) } 68 | 69 | let(:feedback_review_params) do 70 | { 71 | "feedback_review": { 72 | "rating": "1", 73 | "comment": "Actually I don't agree" 74 | } 75 | } 76 | end 77 | 78 | context 'when a user updates their own feedback for a review' do 79 | it 'successfully updates their feedback' do 80 | original = feedback_review 81 | expect(subject["id"]).to eq(original.id) 82 | expect(subject["user_id"]).to eq(original.user_id) 83 | expect(subject["review_id"]).to eq(original.review_id) 84 | expect(subject["rating"]).to eq(1) 85 | expect(subject["comment"]).to eq("Actually I don't agree") 86 | end 87 | end 88 | 89 | context 'when a user updates another users review' do 90 | let(:other_user) { create(:user) } 91 | let(:params) { { review_id: review.id, id: feedback_review.id, token: other_user.spree_api_key, format: 'json' }.merge(feedback_review_params) } 92 | 93 | before do 94 | other_user.generate_spree_api_key! 95 | end 96 | 97 | it 'returns an error' do 98 | expect(subject["error"]).not_to be_empty 99 | expect(subject["error"]).to match(/not authorized/i) 100 | end 101 | end 102 | end 103 | 104 | describe '#destroy' do 105 | subject do 106 | delete :destroy, params: params 107 | JSON.parse(response.body) 108 | end 109 | 110 | before { feedback_review.update(user_id: user.id) } 111 | 112 | let(:params) { { review_id: review.id, id: feedback_review.id, token: user.spree_api_key, format: 'json' } } 113 | 114 | context "when a user destroys their own feedback" do 115 | it 'returns the deleted feedback' do 116 | expect(subject["id"]).to eq(feedback_review.id) 117 | expect(subject["review_id"]).to eq(review.id) 118 | expect(Spree::FeedbackReview.find_by(id: feedback_review.id)).to be_falsey 119 | end 120 | end 121 | 122 | context "when a user destroys another users feedback" do 123 | let(:other_user) { create(:user) } 124 | let(:params) { { review_id: review.id, id: feedback_review.id, token: other_user.spree_api_key, format: 'json' } } 125 | 126 | before do 127 | other_user.generate_spree_api_key! 128 | end 129 | 130 | it 'returns an error' do 131 | expect(subject["error"]).not_to be_empty 132 | expect(subject["error"]).to match(/not authorized/i) 133 | end 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/features/reviews_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'spree/testing_support/authorization_helpers' 5 | 6 | describe 'Reviews', js: true do 7 | let!(:someone) { create(:user, email: 'ryan@spree.com') } 8 | let!(:review) { create(:review, :approved, user: someone) } 9 | let!(:unapproved_review) { create(:review, product: review.product) } 10 | 11 | before do 12 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: false) 13 | end 14 | 15 | context 'product with no review' do 16 | let!(:product_no_reviews) { create(:product) } 17 | 18 | it 'informs that no reviews has been written yet' do 19 | visit spree.product_path(product_no_reviews) 20 | expect(page).to have_text I18n.t('spree.no_reviews_available') 21 | end 22 | 23 | # Regression test for #103 24 | context "shows correct number of previews" do 25 | before do 26 | FactoryBot.create_list :review, 3, product: product_no_reviews, approved: true 27 | stub_spree_preferences(Spree::Reviews::Config, preview_size: 2) 28 | end 29 | 30 | it "displayed reviews are limited by the set preview size" do 31 | visit spree.product_path(product_no_reviews) 32 | expect(page.all(".review").count).to be(2) 33 | end 34 | end 35 | end 36 | 37 | context 'when anonymous user' do 38 | before do 39 | stub_spree_preferences(Spree::Reviews::Config, require_login: true) 40 | end 41 | 42 | context 'visit product with review' do 43 | before do 44 | visit spree.product_path(review.product) 45 | end 46 | 47 | it 'sees review title' do 48 | expect(page).to have_text review.title 49 | end 50 | 51 | it 'can not create review' do 52 | expect(page).not_to have_text I18n.t('spree.write_your_own_review') 53 | end 54 | end 55 | end 56 | 57 | context 'when logged in user' do 58 | let!(:user) { create(:user) } 59 | 60 | before do 61 | sign_in_as! user 62 | end 63 | 64 | context 'visit product with review' do 65 | before do 66 | visit spree.product_path(review.product) 67 | end 68 | 69 | it 'can see review title' do 70 | expect(page).to have_text review.title 71 | end 72 | 73 | context 'with unapproved content allowed' do 74 | before do 75 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: true) 76 | stub_spree_preferences(Spree::Reviews::Config, display_unapproved_reviews: true) 77 | visit spree.product_path(review.product) 78 | end 79 | 80 | it 'can see unapproved content when allowed' do 81 | expect(unapproved_review.approved?).to eq(false) 82 | expect(page).to have_text unapproved_review.title 83 | end 84 | end 85 | 86 | it 'can see create new review button' do 87 | expect(page).to have_text I18n.t('spree.write_your_own_review') 88 | end 89 | 90 | it 'can create new review' do 91 | click_on I18n.t('spree.write_your_own_review') 92 | 93 | expect(page).to have_text I18n.t('spree.leave_us_a_review_for', name: review.product.name) 94 | expect(page).not_to have_text 'Show Identifier' 95 | 96 | within '#new_review' do 97 | click_star(3) 98 | 99 | fill_in 'review_name', with: user.email 100 | fill_in 'review_title', with: 'Great product!' 101 | fill_in 'review_review', with: 'Some big review text..' 102 | attach_file 'review_images', 'spec/fixtures/thinking-cat.jpg' 103 | click_on 'Submit your review' 104 | end 105 | 106 | expect(page.find('.flash.notice', text: I18n.t('spree.review_successfully_submitted'))).to be_truthy 107 | expect(page).not_to have_text 'Some big review text..' 108 | end 109 | end 110 | end 111 | 112 | context 'visit product with review where show_identifier is false' do 113 | let!(:user) { create(:user) } 114 | let!(:review) { create(:review, :approved, :hide_identifier, review: 'review text', user: user) } 115 | 116 | before do 117 | visit spree.product_path(review.product) 118 | end 119 | 120 | it 'show anonymous review' do 121 | expect(page).to have_text I18n.t('spree.anonymous') 122 | expect(page).to have_text 'review text' 123 | end 124 | end 125 | 126 | private 127 | 128 | def sign_in_as!(user) 129 | # rubocop:disable RSpec/AnyInstance 130 | allow_any_instance_of(ApplicationController).to receive_messages current_user: user 131 | allow_any_instance_of(ApplicationController).to receive_messages spree_current_user: user 132 | allow_any_instance_of(ApplicationController).to receive_messages spree_user_signed_in?: true 133 | # rubocop:enable RSpec/AnyInstance 134 | end 135 | 136 | def click_star(num) 137 | page.all(:xpath, "//a[@title='#{num} stars']")[0].click 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /app/views/spree/admin/reviews/index.html.erb: -------------------------------------------------------------------------------- 1 | <% admin_breadcrumb(link_to plural_resource_name(Spree::Product), spree.admin_products_path) %> 2 | 3 | <% content_for :page_title do %> 4 | <%= I18n.t("spree.reviews") %> 5 | <% end %> 6 | 7 | <% content_for :table_filter_title do %> 8 | <% if can? :display, Spree::Review %> 9 | <%= I18n.t("spree.search") %> 10 | <% end %> 11 | <% end %> 12 | 13 | <% content_for :table_filter do %> 14 |
    15 | <%= search_form_for [:admin, @search] do |f| %> 16 |
    17 |
    18 | <%= f.label :name_cont, I18n.t("spree.user") %>
    19 | <%= f.text_field :name_cont, class: 'fullwidth' %> 20 |
    21 |
    22 | 23 |
    24 |
    25 | <%= f.label :title_cont, I18n.t("spree.title") -%>
    26 | <%= f.text_field :title_cont, class: 'fullwidth' -%> 27 |
    28 |
    29 | 30 |
    31 |
    32 | <%= f.label :review_cont, I18n.t("spree.review") -%>
    33 | <%= f.text_field :review_cont, class: 'fullwidth' -%> 34 |
    35 |
    36 | 37 |
    38 |
    39 | <%= f.label :approved_eq, I18n.t("spree.approval_status")-%>
    40 | <%= f.select :approved_eq, [ 41 | [I18n.t("spree.all"), nil], 42 | [I18n.t("spree.approved_reviews"), true], 43 | [I18n.t("spree.unapproved_reviews"), false] 44 | ], {}, class: 'select2 fullwidth' -%> 45 |
    46 |
    47 | 48 |
    49 | 50 |
    51 | <%= button_tag I18n.t("spree.search") %> 52 |
    53 | <%- end -%> 54 |
    55 | <%- end -%> 56 | 57 | <%= paginate @reviews, theme: "solidus_admin" %> 58 | 59 | <% if @reviews.any? %> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <%- @reviews.each do |review| -%> 83 | 84 | 89 | 93 | 98 | 102 | 105 | 110 | 117 | 118 | <% end %> 119 | 120 |
    <%= I18n.t("spree.product") %><%= "#{Spree::Review.human_attribute_name(:rating)}/#{I18n.t("spree.feedback")}" %><%= I18n.t("spree.verified_purchaser") %><%= Spree::Review.human_attribute_name(:user) %><%= Spree::Review.human_attribute_name(:created_at) %><%= Spree::Review.human_attribute_name(:images) %>
    85 | <% if review.product %> 86 | <%= link_to review.product.name, product_path(review.product) %> 87 | <% end %> 88 | 90 | <%= txt_stars(review.rating) %>
    91 | <%= link_to "(#{review.feedback_stars}/#{review.feedback_reviews.size})", admin_review_feedback_reviews_path(review) %> 92 |
    94 | <% if review.verified_purchaser? %> 95 | <%= solidus_icon('fa fa-check') %> 96 | <% end %> 97 | 99 |

    <%= review.user_id ? link_to(review.user.try(:email), [:admin, review.user]) : I18n.t("spree.anonymous") %>

    100 |

    <%= Spree::Review.human_attribute_name(:ip_address) %>: <%= review.ip_address ? link_to(review.ip_address, "http://whois.domaintools.com/#{review.ip_address}") : '-' %>

    101 |
    103 | <%= l review.created_at, format: :short %> 104 | 106 | <% review.images.each do |image| %> 107 | <%= link_to image_tag(image.url(:product)), image.url(:original) %> 108 | <% end %> 109 | 111 | <% if can? :manage, Spree::Review %> 112 | <%= link_to_with_icon 'ok', I18n.t("spree.approve"), approve_admin_review_url(review), no_text: true, class: 'approve' unless review.approved %> 113 | <%= link_to_edit review, no_text: true, class: 'edit' %> 114 | <%= link_to_delete review, no_text: true %> 115 | <% end %> 116 |
    121 | <% else %> 122 |
    123 | <%= I18n.t("spree.no_results") %> 124 |
    125 | <% end %> 126 | 127 | <%= paginate @reviews, theme: "solidus_admin" -%> 128 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2020-09-11 09:54:47 UTC using RuboCop version 0.87.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | Lint/RedundantCopDisableDirective: 12 | Exclude: 13 | - 'lib/generators/solidus_reviews/install/install_generator.rb' 14 | 15 | # Offense count: 1 16 | # Configuration parameters: EnforcedStyleForLeadingUnderscores. 17 | # SupportedStylesForLeadingUnderscores: disallowed, required, optional 18 | Naming/MemoizedInstanceVariableName: 19 | Exclude: 20 | - 'app/controllers/spree/feedback_reviews_controller.rb' 21 | 22 | # Offense count: 2 23 | # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. 24 | # AllowedNames: io, id, to, by, on, in, at, ip, db, os, pp 25 | Naming/MethodParameterName: 26 | Exclude: 27 | - 'app/helpers/spree/reviews_helper.rb' 28 | 29 | # Offense count: 17 30 | # Configuration parameters: EnforcedStyle. 31 | # SupportedStyles: snake_case, normalcase, non_integer 32 | Naming/VariableNumber: 33 | Exclude: 34 | - 'spec/controllers/spree/admin/feedback_reviews_controller_spec.rb' 35 | - 'spec/models/feedback_review_spec.rb' 36 | - 'spec/models/product_spec.rb' 37 | - 'spec/models/review_spec.rb' 38 | 39 | # Offense count: 3 40 | RSpec/AnyInstance: 41 | Exclude: 42 | - 'spec/controllers/spree/admin/reviews_controller_spec.rb' 43 | - 'spec/controllers/spree/reviews_controller_spec.rb' 44 | 45 | # Offense count: 37 46 | # Configuration parameters: Prefixes. 47 | # Prefixes: when, with, without 48 | RSpec/ContextWording: 49 | Exclude: 50 | - 'spec/controllers/spree/api/reviews_controller_spec.rb' 51 | - 'spec/controllers/spree/reviews_controller_spec.rb' 52 | - 'spec/features/admin_spec.rb' 53 | - 'spec/features/reviews_spec.rb' 54 | - 'spec/helpers/review_helper_spec.rb' 55 | - 'spec/models/feedback_review_spec.rb' 56 | - 'spec/models/product_spec.rb' 57 | - 'spec/models/review_spec.rb' 58 | - 'spec/models/reviews_ability_spec.rb' 59 | 60 | # Offense count: 6 61 | # Configuration parameters: CustomTransform, IgnoreMethods, SpecSuffixOnly. 62 | RSpec/FilePath: 63 | Exclude: 64 | - 'spec/helpers/review_helper_spec.rb' 65 | - 'spec/models/feedback_review_spec.rb' 66 | - 'spec/models/product_spec.rb' 67 | - 'spec/models/review_spec.rb' 68 | - 'spec/models/reviews_ability_spec.rb' 69 | - 'spec/models/reviews_configuration_spec.rb' 70 | 71 | # Offense count: 12 72 | # Configuration parameters: AssignmentOnly. 73 | RSpec/InstanceVariable: 74 | Exclude: 75 | - 'spec/controllers/spree/reviews_controller_spec.rb' 76 | 77 | # Offense count: 6 78 | RSpec/LetSetup: 79 | Exclude: 80 | - 'spec/controllers/spree/admin/feedback_reviews_controller_spec.rb' 81 | - 'spec/models/product_spec.rb' 82 | 83 | # Offense count: 4 84 | # Configuration parameters: . 85 | # SupportedStyles: have_received, receive 86 | RSpec/MessageSpies: 87 | EnforcedStyle: receive 88 | 89 | # Offense count: 67 90 | RSpec/MultipleExpectations: 91 | Max: 8 92 | 93 | # Offense count: 3 94 | # Configuration parameters: AllowSubject. 95 | RSpec/MultipleMemoizedHelpers: 96 | Max: 7 97 | 98 | # Offense count: 94 99 | # Configuration parameters: IgnoreSharedExamples. 100 | RSpec/NamedSubject: 101 | Exclude: 102 | - 'spec/controllers/spree/api/feedback_reviews_controller_spec.rb' 103 | - 'spec/controllers/spree/api/reviews_controller_spec.rb' 104 | - 'spec/models/reviews_configuration_spec.rb' 105 | 106 | # Offense count: 8 107 | RSpec/NestedGroups: 108 | Max: 4 109 | 110 | # Offense count: 3 111 | RSpec/StubbedMock: 112 | Exclude: 113 | - 'spec/controllers/spree/admin/reviews_controller_spec.rb' 114 | - 'spec/controllers/spree/reviews_controller_spec.rb' 115 | 116 | # Offense count: 1 117 | RSpec/UnspecifiedException: 118 | Exclude: 119 | - 'spec/controllers/spree/feedback_reviews_controller_spec.rb' 120 | 121 | # Offense count: 2 122 | # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. 123 | RSpec/VerifiedDoubles: 124 | Exclude: 125 | - 'spec/models/reviews_ability_spec.rb' 126 | 127 | # Offense count: 1 128 | # Configuration parameters: Include. 129 | # Include: app/models/**/*.rb 130 | Rails/InverseOf: 131 | Exclude: 132 | - 'app/models/spree/review.rb' 133 | 134 | # Offense count: 2 135 | Rails/ReflectionClassName: 136 | Exclude: 137 | - 'app/models/spree/feedback_review.rb' 138 | - 'app/models/spree/review.rb' 139 | 140 | # Offense count: 5 141 | # Configuration parameters: ForbiddenMethods, AllowedMethods. 142 | # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all 143 | Rails/SkipsModelValidations: 144 | Exclude: 145 | - 'app/controllers/spree/admin/reviews_controller.rb' 146 | - 'db/migrate/20120123141326_recalculate_ratings.rb' 147 | - 'spec/controllers/spree/admin/reviews_controller_spec.rb' 148 | - 'spec/models/product_spec.rb' 149 | 150 | # Offense count: 10 151 | # Cop supports --auto-correct. 152 | # Configuration parameters: AutoCorrect, EnforcedStyle. 153 | # SupportedStyles: nested, compact 154 | Style/ClassAndModuleChildren: 155 | Exclude: 156 | - 'app/controllers/spree/admin/feedback_reviews_controller.rb' 157 | - 'app/controllers/spree/admin/review_settings_controller.rb' 158 | - 'app/controllers/spree/admin/reviews_controller.rb' 159 | - 'app/controllers/spree/feedback_reviews_controller.rb' 160 | - 'app/controllers/spree/reviews_controller.rb' 161 | - 'app/helpers/spree/reviews_helper.rb' 162 | - 'app/models/spree/feedback_review.rb' 163 | - 'app/models/spree/review.rb' 164 | - 'app/models/spree/reviews_ability.rb' 165 | - 'app/models/spree/reviews_configuration.rb' 166 | 167 | # Offense count: 1 168 | # Configuration parameters: EnforcedStyle. 169 | # SupportedStyles: annotated, template, unannotated 170 | Style/FormatStringToken: 171 | Exclude: 172 | - 'app/decorators/models/solidus_reviews/spree/product_decorator.rb' 173 | 174 | # Offense count: 5 175 | # Configuration parameters: MinBodyLength. 176 | Style/GuardClause: 177 | Exclude: 178 | - 'app/controllers/spree/admin/reviews_controller.rb' 179 | - 'lib/controllers/spree/api/feedback_reviews_controller.rb' 180 | - 'lib/controllers/spree/api/reviews_controller.rb' 181 | 182 | # Offense count: 1 183 | Style/OptionalBooleanParameter: 184 | Exclude: 185 | - 'app/helpers/spree/reviews_helper.rb' 186 | 187 | # Offense count: 10 188 | # Cop supports --auto-correct. 189 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 190 | # URISchemes: http, https 191 | Layout/LineLength: 192 | Max: 150 193 | -------------------------------------------------------------------------------- /spec/controllers/spree/api/reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Api::ReviewsController, type: :controller do 6 | render_views 7 | 8 | let!(:user) { create(:user) } 9 | let!(:review) { create(:review, rating: 5) } 10 | let!(:product) { review.product } 11 | 12 | before do 13 | user.generate_spree_api_key! 14 | Array.new(3).each do 15 | create(:review) 16 | end 17 | end 18 | 19 | describe '#index' do 20 | context 'when given a product_id' do 21 | subject do 22 | get :index, params: { product_id: product.id, token: user.spree_api_key, format: 'json' } 23 | JSON.parse(response.body) 24 | end 25 | 26 | context 'there are no reviews for a product' do 27 | it 'returns an empty array' do 28 | expect(Spree::Review.count).to be >= 0 29 | expect(subject["reviews"]).to be_empty 30 | end 31 | end 32 | 33 | context 'there are reviews for the product and other products' do 34 | it 'returns all approved reviews for the product' do 35 | review.update(approved: true) 36 | review.images << create(:image) 37 | review.feedback_reviews << create(:feedback_review, review: review) 38 | expect(Spree::Review.count).to be >= 2 39 | expect(subject.size).to eq(2) 40 | expect(subject["reviews"][0]["id"]).to eq(review.id) 41 | expect(subject["reviews"][0]["images"].count).to eq(1) 42 | expect(subject["reviews"][0]["feedback_reviews"].count).to eq(1) 43 | expect(subject["reviews"][0]["verified_purchaser"]).to eq(false) 44 | expect(subject["avg_rating"]).to eq("5.0") 45 | end 46 | end 47 | end 48 | 49 | context 'when given a user_id' do 50 | subject do 51 | get :index, params: { user_id: user.id, token: user.spree_api_key, format: 'json' } 52 | JSON.parse(response.body) 53 | end 54 | 55 | context 'there are no reviews for the user' do 56 | it 'returns an empty array' do 57 | expect(Spree::Review.count).to be >= 0 58 | expect(subject["reviews"]).to be_empty 59 | end 60 | end 61 | 62 | context 'there are reviews for user' do 63 | before { review.update(user_id: user.id) } 64 | 65 | it 'returns all reviews for the user' do 66 | expect(Spree::Review.count).to be >= 2 67 | expect(subject.size).to eq(2) 68 | expect(subject["reviews"][0]["id"]).to eq(review.id) 69 | expect(subject["avg_rating"]).to eq(nil) 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe '#show' do 76 | subject do 77 | get :show, params: { id: review.id, token: user.spree_api_key, format: 'json' } 78 | JSON.parse(response.body) 79 | end 80 | 81 | context 'when it is the users review' do 82 | before { review.update(user_id: user.id) } 83 | 84 | it 'returns the review' do 85 | expect(subject).not_to be_empty 86 | expect(subject["product_id"]).to eq(product.id) 87 | expect(subject["name"]).to eq(review[:name]) 88 | expect(subject["review"]).to eq(review[:review]) 89 | expect(subject["title"]).to eq(review[:title]) 90 | expect(subject["verified_purchaser"]).to eq(false) 91 | expect(subject["images"]).to eq([]) 92 | expect(subject["feedback_reviews"]).to eq([]) 93 | end 94 | end 95 | 96 | context 'when it is not the users review' do 97 | it 'returns with not authorized' do 98 | expect(subject["error"]).not_to be_empty 99 | expect(subject["error"]).to match(/not authorized/i) 100 | end 101 | 102 | context 'and it the review is approved' do 103 | before { review.update(approved: true) } 104 | 105 | it 'returns the review' do 106 | expect(subject).not_to be_empty 107 | expect(subject["product_id"]).to eq(product.id) 108 | expect(subject["name"]).to eq(review[:name]) 109 | expect(subject["review"]).to eq(review[:review]) 110 | expect(subject["title"]).to eq(review[:title]) 111 | expect(subject["images"]).to eq([]) 112 | expect(subject["feedback_reviews"]).to eq([]) 113 | end 114 | end 115 | end 116 | end 117 | 118 | describe '#create' do 119 | subject do 120 | params = { product_id: product.id, token: user.spree_api_key, format: 'json' }.merge(review_params) 121 | post :create, params: params 122 | JSON.parse(response.body) 123 | end 124 | 125 | let(:review_params) do 126 | { 127 | "user_id": user.id, 128 | "rating": "3 stars", 129 | "title": "My title 2", 130 | "name": "Full Name", 131 | "review": "My review of the product" 132 | } 133 | end 134 | 135 | context 'when user has already reviewed this product' do 136 | before do 137 | review.update(user_id: user.id) 138 | end 139 | 140 | it 'returns with a fail' do 141 | expect(subject["error"]).not_to be_empty 142 | expect(subject["error"]).to match(/invalid resource/i) 143 | end 144 | end 145 | 146 | context 'when it is a users first review for the product' do 147 | it 'returns success with review' do 148 | expect(subject).not_to be_empty 149 | expect(subject["product_id"]).to eq(product.id) 150 | expect(subject["name"]).to eq(review_params[:name]) 151 | expect(subject["review"]).to eq(review_params[:review]) 152 | expect(subject["title"]).to eq(review_params[:title]) 153 | expect(subject["images"]).to eq([]) 154 | expect(subject["feedback_reviews"]).to eq([]) 155 | end 156 | end 157 | end 158 | 159 | describe '#update' do 160 | subject do 161 | put :update, params: params 162 | JSON.parse(response.body) 163 | end 164 | 165 | before { review.update(approved: true, user_id: user.id) } 166 | 167 | let(:params) { { product_id: product.id, id: review.id, token: user.spree_api_key, format: 'json' }.merge(review_params) } 168 | 169 | let(:review_params) do 170 | { 171 | "rating": "3 stars", 172 | "title": "My title 2", 173 | "name": "Full name", 174 | "review": "My review of the product", 175 | } 176 | end 177 | 178 | context 'when a user updates their own review' do 179 | it 'successfullies update the review and set approved back to false' do 180 | original = review 181 | expect(original.approved?).to be true 182 | expect(subject["id"]).to eq(original.id) 183 | expect(subject["user_id"]).to eq(original.user_id) 184 | expect(subject["product_id"]).to eq(original.product_id) 185 | expect(subject["approved"]).to be false 186 | expect(subject["images"]).to eq([]) 187 | expect(subject["feedback_reviews"]).to eq([]) 188 | end 189 | end 190 | 191 | context 'when a user updates another users review' do 192 | let(:other_user) { create(:user) } 193 | let(:params) { { product_id: product.id, id: review.id, token: other_user.spree_api_key, format: 'json' }.merge(review_params) } 194 | 195 | before do 196 | other_user.generate_spree_api_key! 197 | end 198 | 199 | it 'returns an error' do 200 | expect(subject["error"]).not_to be_empty 201 | expect(subject["error"]).to match(/not authorized/i) 202 | end 203 | end 204 | end 205 | 206 | describe '#destroy' do 207 | subject do 208 | delete :destroy, params: params 209 | JSON.parse(response.body) 210 | end 211 | 212 | before { review.update(approved: true, user_id: user.id) } 213 | 214 | let(:params) { { product_id: product.id, id: review.id, token: user.spree_api_key, format: 'json' } } 215 | 216 | context "when a user destroys their own review" do 217 | it 'returns the deleted review' do 218 | expect(subject["id"]).to eq(review.id) 219 | expect(subject["product_id"]).to eq(product.id) 220 | expect(Spree::Review.find_by(id: review.id)).to be_falsey 221 | end 222 | end 223 | 224 | context "when a user destroys another users review" do 225 | let(:other_user) { create(:user) } 226 | let(:params) { { product_id: product.id, id: review.id, token: other_user.spree_api_key, format: 'json' } } 227 | 228 | before do 229 | other_user.generate_spree_api_key! 230 | end 231 | 232 | it 'returns an error' do 233 | expect(subject["error"]).not_to be_empty 234 | expect(subject["error"]).to match(/not authorized/i) 235 | end 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/models/review_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::Review do 6 | context 'validations' do 7 | it 'validates by default' do 8 | expect(build(:review)).to be_valid 9 | end 10 | 11 | it 'validates with a nil user' do 12 | expect(build(:review, user: nil)).to be_valid 13 | end 14 | 15 | it 'validates with a nil review' do 16 | expect(build(:review, review: nil)).to be_valid 17 | end 18 | 19 | context 'rating' do 20 | it 'does not validate when no rating is specified' do 21 | expect(build(:review, rating: nil)).not_to be_valid 22 | end 23 | 24 | it 'does not validate when the rating is not a number' do 25 | expect(build(:review, rating: 'not_a_number')).not_to be_valid 26 | end 27 | 28 | it 'does not validate when the rating is a float' do 29 | expect(build(:review, rating: 2.718)).not_to be_valid 30 | end 31 | 32 | it 'does not validate when the rating is less than 1' do 33 | expect(build(:review, rating: 0)).not_to be_valid 34 | expect(build(:review, rating: -5)).not_to be_valid 35 | end 36 | 37 | it 'does not validate when the rating is greater than 5' do 38 | expect(build(:review, rating: 6)).not_to be_valid 39 | expect(build(:review, rating: 8)).not_to be_valid 40 | end 41 | 42 | (1..5).each do |i| 43 | it "validates when the rating is #{i}" do 44 | expect(build(:review, rating: i)).to be_valid 45 | end 46 | end 47 | end 48 | 49 | context 'review body' do 50 | it 'is valid without a body' do 51 | expect(build(:review, review: nil)).to be_valid 52 | end 53 | end 54 | end 55 | 56 | context 'scopes' do 57 | context 'most_recent_first' do 58 | let!(:review_1) { create(:review, created_at: 10.days.ago) } 59 | let!(:review_2) { create(:review, created_at: 2.days.ago) } 60 | let!(:review_3) { create(:review, created_at: 5.days.ago) } 61 | 62 | it 'properly runs most_recent_first queries' do 63 | expect(described_class.most_recent_first.to_a).to eq([review_2, review_3, review_1]) 64 | end 65 | 66 | it 'defaults to most_recent_first queries' do 67 | expect(described_class.all.to_a).to eq([review_2, review_3, review_1]) 68 | end 69 | end 70 | 71 | context 'oldest_first' do 72 | let!(:review_1) { create(:review, created_at: 10.days.ago) } 73 | let!(:review_2) { create(:review, created_at: 2.days.ago) } 74 | let!(:review_3) { create(:review, created_at: 5.days.ago) } 75 | let!(:review_4) { create(:review, created_at: 1.day.ago) } 76 | 77 | it 'properly runs oldest_first queries' do 78 | expect(described_class.oldest_first.to_a).to eq([review_1, review_3, review_2, review_4]) 79 | end 80 | 81 | it 'uses oldest_first for preview' do 82 | expect(described_class.preview.to_a).to eq([review_1, review_3, review_2]) 83 | end 84 | end 85 | 86 | context 'localized' do 87 | let!(:en_review_1) { create(:review, locale: 'en', created_at: 10.days.ago) } 88 | let!(:en_review_2) { create(:review, locale: 'en', created_at: 2.days.ago) } 89 | let!(:en_review_3) { create(:review, locale: 'en', created_at: 5.days.ago) } 90 | 91 | let!(:es_review_1) { create(:review, locale: 'es', created_at: 10.days.ago) } 92 | let!(:fr_review_1) { create(:review, locale: 'fr', created_at: 10.days.ago) } 93 | 94 | it 'properly runs localized queries' do 95 | expect(described_class.localized('en').to_a).to eq([en_review_2, en_review_3, en_review_1]) 96 | expect(described_class.localized('es').to_a).to eq([es_review_1]) 97 | expect(described_class.localized('fr').to_a).to eq([fr_review_1]) 98 | end 99 | end 100 | 101 | context 'approved / not_approved / default_approval_filter' do 102 | let!(:approved_review_1) { create(:review, approved: true, created_at: 10.days.ago) } 103 | let!(:approved_review_2) { create(:review, approved: true, created_at: 2.days.ago) } 104 | let!(:approved_review_3) { create(:review, approved: true, created_at: 5.days.ago) } 105 | 106 | let!(:unapproved_review_1) { create(:review, approved: false, created_at: 7.days.ago) } 107 | let!(:unapproved_review_2) { create(:review, approved: false, created_at: 1.day.ago) } 108 | 109 | it 'properly runs approved and unapproved queries' do 110 | expect(described_class.approved.to_a).to eq([approved_review_2, approved_review_3, approved_review_1]) 111 | expect(described_class.not_approved.to_a).to eq([unapproved_review_2, unapproved_review_1]) 112 | 113 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: true) 114 | expect(described_class.default_approval_filter.to_a).to eq([unapproved_review_2, 115 | approved_review_2, 116 | approved_review_3, 117 | unapproved_review_1, 118 | approved_review_1]) 119 | 120 | stub_spree_preferences(Spree::Reviews::Config, include_unapproved_reviews: false) 121 | expect(described_class.default_approval_filter.to_a).to eq([approved_review_2, approved_review_3, approved_review_1]) 122 | end 123 | end 124 | end 125 | 126 | describe '.ransackable_attributes' do 127 | subject { described_class.ransackable_attributes } 128 | 129 | it { is_expected.to contain_exactly("id", "approved", "name", "review", "title") } 130 | end 131 | 132 | describe '.ransackable_associations' do 133 | subject { described_class.ransackable_associations } 134 | 135 | it { is_expected.to contain_exactly("feedback_reviews", "product", "user") } 136 | end 137 | 138 | describe '#recalculate_product_rating' do 139 | let(:product) { create(:product) } 140 | let!(:review) { create(:review, product: product) } 141 | 142 | before { product.reviews << review } 143 | 144 | it 'if approved' do 145 | expect(review).to receive(:recalculate_product_rating) 146 | review.approved = true 147 | review.save! 148 | end 149 | 150 | it 'if not approved' do 151 | expect(review).not_to receive(:recalculate_product_rating) 152 | review.save! 153 | end 154 | 155 | it 'updates the product average rating' do 156 | expect(review.product).to receive(:recalculate_rating) 157 | review.approved = true 158 | review.save! 159 | end 160 | end 161 | 162 | describe '#feedback_stars' do 163 | let!(:review) { create(:review) } 164 | 165 | before do 166 | 3.times do |i| 167 | f = Spree::FeedbackReview.new 168 | f.review = review 169 | f.rating = (i + 1) 170 | f.save 171 | end 172 | end 173 | 174 | it 'returns the average rating from feedback reviews' do 175 | expect(review.feedback_stars).to eq 2 176 | end 177 | end 178 | 179 | describe '#email' do 180 | it 'returns email from user' do 181 | user = build(:user, email: 'john@smith.com') 182 | review = build(:review, user: user) 183 | expect(review.email).to eq('john@smith.com') 184 | end 185 | end 186 | 187 | context 'images' do 188 | it 'supports images' do 189 | review = build(:review, images: [build(:image)]) 190 | expect(review.images).not_to eq(nil) 191 | end 192 | 193 | it 'respects order' do 194 | image_1 = build(:image, position: 2) 195 | image_2 = build(:image, position: 1) 196 | review = create(:review, images: [image_1, image_2]) 197 | review.reload 198 | expect(review.images.first).to eq(image_2) 199 | end 200 | end 201 | 202 | describe "#verify_purchaser" do 203 | let(:order) { create(:completed_order_with_totals) } 204 | let(:product) { order.products.first } 205 | let(:user) { order.user } 206 | let(:review) { build(:review, user: user, product: product) } 207 | 208 | it "returns true if the user has purchased the product" do 209 | expect(review.verified_purchaser).to eq(false) 210 | review.verify_purchaser 211 | expect(review.verified_purchaser).to eq(true) 212 | end 213 | 214 | it "returns false if the user has not purchased the product" do 215 | review.user = create(:user) 216 | expect(review.verified_purchaser).to eq(false) 217 | review.verify_purchaser 218 | expect(review.verified_purchaser).to eq(false) 219 | end 220 | 221 | it "returns nothing if there is no user_id or product_id" do 222 | review.product_id = nil 223 | expect(review.verified_purchaser).to eq(false) 224 | review.verify_purchaser 225 | expect(review.verified_purchaser).to eq(false) 226 | 227 | review.product_id = product.id 228 | review.user_id = nil 229 | expect(review.verified_purchaser).to eq(false) 230 | review.verify_purchaser 231 | expect(review.verified_purchaser).to eq(false) 232 | end 233 | end 234 | 235 | describe "#approve_review" do 236 | let(:order) { create(:completed_order_with_totals) } 237 | let(:product) { order.products.first } 238 | let(:user) { order.user } 239 | let(:review) { build(:review, title: '', review: '', user: user, product: product) } 240 | 241 | it "auto approves star only review" do 242 | stub_spree_preferences(Spree::Reviews::Config, approve_star_only: true) 243 | 244 | expect(review.approved).to eq(false) 245 | review.approve_review 246 | expect(review.approved).to eq(true) 247 | end 248 | 249 | it "auto approves star only review for verified purchaser" do 250 | stub_spree_preferences(Spree::Reviews::Config, approve_star_only_for_verified_purchaser: true) 251 | 252 | expect(review.verified_purchaser).to eq(false) 253 | expect(review.approved).to eq(false) 254 | review.verify_purchaser 255 | expect(review.verified_purchaser).to eq(true) 256 | expect(review.approved).to eq(false) 257 | review.approve_review 258 | expect(review.approved).to eq(true) 259 | end 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /spec/controllers/spree/reviews_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Spree::ReviewsController, type: :controller do 6 | let(:user) { create(:user) } 7 | let(:product) { create(:product) } 8 | let(:review) { create(:review, :approved, product: product, user: user) } 9 | let(:review_params) do 10 | { product_id: product.slug, 11 | review: { rating: 3, 12 | name: 'Ryan Bigg', 13 | title: 'Great Product', 14 | review: 'Some big review text..', 15 | images: [ 16 | fixture_file_upload(File.new('spec/fixtures/thinking-cat.jpg')) 17 | ] } } 18 | end 19 | 20 | before do 21 | allow(controller).to receive(:spree_current_user).and_return(user) 22 | allow(controller).to receive(:spree_user_signed_in?).and_return(true) 23 | end 24 | 25 | describe '#index' do 26 | context 'for a product that does not exist' do 27 | it 'responds with a 404' do 28 | expect { 29 | get :index, params: { product_id: 'not_real' } 30 | }.to raise_error(ActiveRecord::RecordNotFound) 31 | end 32 | end 33 | 34 | context 'for a valid product' do 35 | it 'list approved reviews' do 36 | approved_reviews = create_list(:review, 2, :approved, product: product) 37 | get :index, params: { product_id: product.slug } 38 | expect(assigns[:approved_reviews]).to match_array approved_reviews 39 | end 40 | end 41 | end 42 | 43 | describe '#new' do 44 | context 'for a product that does not exist' do 45 | it 'responds with a 404' do 46 | expect { 47 | get :new, params: { product_id: 'not_real' } 48 | }.to raise_error(ActiveRecord::RecordNotFound) 49 | end 50 | end 51 | 52 | it 'fail if the user is not authorized to create a review' do 53 | allow(controller).to receive(:authorize!).and_raise(RuntimeError) 54 | 55 | expect { 56 | post :new, params: { product_id: product.slug } 57 | assert_match 'ryanbig', response.body 58 | }.to raise_error RuntimeError 59 | end 60 | 61 | it 'render the new template' do 62 | get :new, params: { product_id: product.slug } 63 | expect(response.status).to eq(200) 64 | expect(response).to render_template(:new) 65 | end 66 | end 67 | 68 | describe '#edit' do 69 | context 'for a product that does not exist' do 70 | it 'responds with a 404' do 71 | expect { 72 | get :edit, params: { id: review.id, product_id: 'not_real' } 73 | }.to raise_error(ActiveRecord::RecordNotFound) 74 | end 75 | end 76 | 77 | it 'fail if the user is not authorized to edit a review' do 78 | allow(controller).to receive(:authorize!).and_raise(RuntimeError) 79 | 80 | expect { 81 | post :edit, params: { id: review.id, product_id: product.slug } 82 | assert_match 'ryanbig', response.body 83 | }.to raise_error RuntimeError 84 | end 85 | 86 | it 'render the edit template' do 87 | get :edit, params: { id: review.id, product_id: product.slug } 88 | expect(response.status).to eq(200) 89 | expect(response).to render_template(:edit) 90 | end 91 | 92 | it 'doesn\'t allow another user to update a users review' do 93 | other_user = create(:user) 94 | allow(controller).to receive(:spree_current_user).and_return(other_user) 95 | get :edit, params: { id: review.id, product_id: product.slug } 96 | expect(response).not_to render_template(:edit) 97 | expect(flash[:error]).to eq "Authorization Failure" 98 | end 99 | end 100 | 101 | describe '#create' do 102 | before { allow(controller).to receive(:spree_current_user).and_return(user) } 103 | 104 | context 'for a product that does not exist' do 105 | it 'responds with a 404' do 106 | expect { 107 | post :create, params: { product_id: 'not_real' } 108 | }.to raise_error(ActiveRecord::RecordNotFound) 109 | end 110 | end 111 | 112 | it 'creates a new review' do 113 | expect { 114 | post :create, params: review_params 115 | }.to change(Spree::Review, :count).by(1) 116 | end 117 | 118 | it 'creates a rating only review' do 119 | review_params = { 120 | product_id: product.slug, 121 | review: { rating: 3 } 122 | } 123 | 124 | expect { 125 | post :create, params: review_params 126 | }.to change(Spree::Review, :count).by(1) 127 | end 128 | 129 | it 'sets the ip-address of the remote' do 130 | @request.env['REMOTE_ADDR'] = '127.0.0.1' 131 | post :create, params: review_params 132 | expect(assigns[:review].ip_address).to eq '127.0.0.1' 133 | end 134 | 135 | it 'attaches the image' do 136 | post :create, params: review_params 137 | expect(assigns[:review].images).to be_present 138 | end 139 | 140 | it 'fails if the user is not authorized to create a review' do 141 | allow(controller).to receive(:authorize!).and_raise(RuntimeError) 142 | 143 | expect{ 144 | post :create, params: review_params 145 | }.to raise_error RuntimeError 146 | end 147 | 148 | it 'flashes the notice' do 149 | post :create, params: review_params 150 | expect(flash[:notice]).to eq I18n.t('spree.review_successfully_submitted') 151 | end 152 | 153 | it 'redirects to product page' do 154 | post :create, params: review_params 155 | expect(response).to redirect_to spree.product_path(product) 156 | end 157 | 158 | it 'removes all non-numbers from ratings param' do 159 | post :create, params: review_params 160 | expect(controller.params[:review][:rating]).to eq '3' 161 | end 162 | 163 | it 'sets the current spree user as reviews user' do 164 | post :create, params: review_params 165 | review_params[:review][:user_id] = user.id 166 | assigns[:review][:user_id] = user.id 167 | expect(assigns[:review][:user_id]).to eq user.id 168 | end 169 | 170 | context 'with invalid params' do 171 | it 'renders new when review.save fails' do 172 | expect_any_instance_of(Spree::Review).to receive(:save).and_return(false) 173 | post :create, params: review_params 174 | expect(response).to render_template :new 175 | end 176 | 177 | it 'does not create a review' do 178 | expect(Spree::Review.count).to eq 0 179 | post :create, params: review_params.merge(review: { rating: 'not_a_number' }) 180 | expect(Spree::Review.count).to eq 0 181 | end 182 | end 183 | 184 | # It always sets the locale so preference pointless 185 | context 'when config requires locale tracking:' do 186 | it 'sets the locale' do 187 | stub_spree_preferences(Spree::Reviews::Config, track_locale: true) 188 | post :create, params: review_params 189 | expect(assigns[:review].locale).to eq I18n.locale.to_s 190 | end 191 | end 192 | end 193 | 194 | describe '#update' do 195 | before { 196 | allow(controller).to receive(:spree_current_user).and_return(user) 197 | @review_params = { 198 | product_id: product.slug, 199 | id: review.id, 200 | review: { title: 'Amazing Product' } 201 | } 202 | } 203 | 204 | context 'for a product that does not exist' do 205 | it 'responds with a 404' do 206 | expect { 207 | post :update, params: { id: review.id, product_id: 'not_real' } 208 | }.to raise_error(ActiveRecord::RecordNotFound) 209 | end 210 | end 211 | 212 | it 'updates a review' do 213 | post :update, params: @review_params 214 | 215 | expect(assigns[:review].title).to eq 'Amazing Product' 216 | expect(assigns[:review].product).to eq product 217 | expect(assigns[:review].user).to eq user 218 | end 219 | 220 | it 'updates a review to be a rating only review' do 221 | post :update, params: { 222 | product_id: product.slug, 223 | id: review.id, 224 | review: { title: '', review: '', rating: 5 } 225 | } 226 | 227 | expect(assigns[:review].title).to eq '' 228 | expect(assigns[:review].review).to eq '' 229 | expect(assigns[:review].rating).to eq 5 230 | end 231 | 232 | it 'updates the attached image' do 233 | post :update, params: { 234 | product_id: product.slug, 235 | id: review.id, 236 | review: { 237 | images: [ 238 | fixture_file_upload(File.new('spec/fixtures/thinking-cat.jpg')), 239 | ] 240 | } 241 | } 242 | expect(assigns[:review].images.count).to eq 1 243 | end 244 | 245 | it 'fails if the user is not authorized to create a review' do 246 | allow(controller).to receive(:authorize!).and_raise(RuntimeError) 247 | 248 | expect{ 249 | post :update, params: @review_params 250 | }.to raise_error RuntimeError 251 | end 252 | 253 | it 'flashes the notice' do 254 | post :update, params: @review_params 255 | 256 | expect(flash[:notice]).to eq I18n.t('spree.review_successfully_submitted') 257 | end 258 | 259 | it 'redirects to product page' do 260 | post :update, params: @review_params 261 | review.reload 262 | review.valid? 263 | expect(response).to redirect_to spree.product_path(product) 264 | end 265 | 266 | it 'removes all non-numbers from ratings param' do 267 | @review_params[:review][:rating] = 5 268 | post :update, params: @review_params 269 | expect(controller.params[:review][:rating]).to eq '5' 270 | end 271 | 272 | it 'doesnt change the current spree user as reviews user' do 273 | post :update, params: @review_params 274 | expect(assigns[:review].user_id).to eq user.id 275 | end 276 | 277 | context 'with invalid params' do 278 | it 'renders edit when review.save fails' do 279 | expect_any_instance_of(Spree::Review).to receive(:update).and_return(false) 280 | post :update, params: @review_params 281 | expect(response).to render_template :edit 282 | end 283 | 284 | it 'does not update a review' do 285 | original_rating = review.rating 286 | original_title = review.title 287 | @review_params[:review][:rating] = 'not_a_number' 288 | @review_params[:review][:title] = true 289 | post :update, params: @review_params 290 | 291 | review.reload 292 | expect(review.rating).to eq original_rating 293 | expect(review.title).to eq original_title 294 | end 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /OLD_CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.5.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.5.0) (2021-09-20) 4 | 5 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.4.1...v1.5.0) 6 | 7 | **Closed issues:** 8 | 9 | - Dependabot can't resolve your Ruby dependency files [\#105](https://github.com/solidusio-contrib/solidus_reviews/issues/105) 10 | - Prepare Solidus Reviews for Solidus 3.0 [\#101](https://github.com/solidusio-contrib/solidus_reviews/issues/101) 11 | - Installing fresh solidus and solidus\_reviews, rails g solidus\_reviews:install fails [\#100](https://github.com/solidusio-contrib/solidus_reviews/issues/100) 12 | - Dependabot can't resolve your Ruby dependency files [\#97](https://github.com/solidusio-contrib/solidus_reviews/issues/97) 13 | - Dependabot can't resolve your Ruby dependency files [\#96](https://github.com/solidusio-contrib/solidus_reviews/issues/96) 14 | 15 | **Merged pull requests:** 16 | 17 | - bump to support ruby 3 and solidus 3 [\#103](https://github.com/solidusio-contrib/solidus_reviews/pull/103) ([peterberkenbosch](https://github.com/peterberkenbosch)) 18 | - Fix specs to allow support for solidus 3 [\#102](https://github.com/solidusio-contrib/solidus_reviews/pull/102) ([seriousammy](https://github.com/seriousammy)) 19 | - Update gem to latest solidus\_dev\_support [\#99](https://github.com/solidusio-contrib/solidus_reviews/pull/99) ([nirebu](https://github.com/nirebu)) 20 | - Make reviews:config like the config in solidus-frontend [\#98](https://github.com/solidusio-contrib/solidus_reviews/pull/98) ([marcrohloff](https://github.com/marcrohloff)) 21 | 22 | ## [v1.4.1](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.4.1) (2020-08-10) 23 | 24 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.4.0...v1.4.1) 25 | 26 | ## [v1.4.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.4.0) (2020-08-06) 27 | 28 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.3.0...v1.4.0) 29 | 30 | **Closed issues:** 31 | 32 | - Cannot select star rating when creating or editing a review [\#88](https://github.com/solidusio-contrib/solidus_reviews/issues/88) 33 | - uninitialized constant SolidusReviews::Spree::Admin \(NameError\) [\#77](https://github.com/solidusio-contrib/solidus_reviews/issues/77) 34 | - Dependabot can't resolve your Ruby dependency files [\#75](https://github.com/solidusio-contrib/solidus_reviews/issues/75) 35 | - Dependabot can't resolve your Ruby dependency files [\#74](https://github.com/solidusio-contrib/solidus_reviews/issues/74) 36 | - Dependabot can't resolve your Ruby dependency files [\#73](https://github.com/solidusio-contrib/solidus_reviews/issues/73) 37 | - Dependabot can't resolve your Ruby dependency files [\#71](https://github.com/solidusio-contrib/solidus_reviews/issues/71) 38 | - Dependabot can't resolve your Ruby dependency files [\#70](https://github.com/solidusio-contrib/solidus_reviews/issues/70) 39 | - Dependabot can't resolve your Ruby dependency files [\#69](https://github.com/solidusio-contrib/solidus_reviews/issues/69) 40 | - Users should be able to upload images as part of their review [\#67](https://github.com/solidusio-contrib/solidus_reviews/issues/67) 41 | 42 | **Merged pull requests:** 43 | 44 | - Replace deprected ` SolidusSupport::EngineExtensions::Decorators` with ` SolidusSupport::EngineExtensions` [\#93](https://github.com/solidusio-contrib/solidus_reviews/pull/93) ([marcrohloff](https://github.com/marcrohloff)) 45 | - Allow solidus\_support 0.4 [\#92](https://github.com/solidusio-contrib/solidus_reviews/pull/92) ([mamhoff](https://github.com/mamhoff)) 46 | - Fixes review title [\#91](https://github.com/solidusio-contrib/solidus_reviews/pull/91) ([memotoro](https://github.com/memotoro)) 47 | - remove extraneous .css file [\#90](https://github.com/solidusio-contrib/solidus_reviews/pull/90) ([dhughesbc](https://github.com/dhughesbc)) 48 | - Relax solidus\_support dependency [\#86](https://github.com/solidusio-contrib/solidus_reviews/pull/86) ([kennyadsl](https://github.com/kennyadsl)) 49 | - Adds PermissionSets classes and checks for Reviews [\#85](https://github.com/solidusio-contrib/solidus_reviews/pull/85) ([memotoro](https://github.com/memotoro)) 50 | - Adds controls for image uploads [\#83](https://github.com/solidusio-contrib/solidus_reviews/pull/83) ([memotoro](https://github.com/memotoro)) 51 | - Add option to auto approve star only reviews [\#82](https://github.com/solidusio-contrib/solidus_reviews/pull/82) ([KaemonIsland](https://github.com/KaemonIsland)) 52 | - Add touch to feedback review model [\#81](https://github.com/solidusio-contrib/solidus_reviews/pull/81) ([KaemonIsland](https://github.com/KaemonIsland)) 53 | - Add feedback reviews to api [\#79](https://github.com/solidusio-contrib/solidus_reviews/pull/79) ([KaemonIsland](https://github.com/KaemonIsland)) 54 | - Fix path for Spree::ProductsControllerDecorator [\#78](https://github.com/solidusio-contrib/solidus_reviews/pull/78) ([aldesantis](https://github.com/aldesantis)) 55 | - Add edit and update methods to reviews controller [\#76](https://github.com/solidusio-contrib/solidus_reviews/pull/76) ([KaemonIsland](https://github.com/KaemonIsland)) 56 | - Update to solidus\_dev\_support [\#72](https://github.com/solidusio-contrib/solidus_reviews/pull/72) ([aldesantis](https://github.com/aldesantis)) 57 | 58 | ## [v1.3.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.3.0) (2019-11-27) 59 | 60 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.2.0...v1.3.0) 61 | 62 | **Closed issues:** 63 | 64 | - sass/rails is deprecated. Please update to `require 'sassc/rails'` [\#48](https://github.com/solidusio-contrib/solidus_reviews/issues/48) 65 | - Class variable access from toplevel [\#47](https://github.com/solidusio-contrib/solidus_reviews/issues/47) 66 | 67 | **Merged pull requests:** 68 | 69 | - Prepare v1.3.0 [\#68](https://github.com/solidusio-contrib/solidus_reviews/pull/68) ([aldesantis](https://github.com/aldesantis)) 70 | - Adopt solidus\_extension\_dev\_tools [\#66](https://github.com/solidusio-contrib/solidus_reviews/pull/66) ([aldesantis](https://github.com/aldesantis)) 71 | - Switch to sassc/rails [\#65](https://github.com/solidusio-contrib/solidus_reviews/pull/65) ([pelargir](https://github.com/pelargir)) 72 | - Fix flaky specs caused by bad preference stubbing [\#54](https://github.com/solidusio-contrib/solidus_reviews/pull/54) ([aldesantis](https://github.com/aldesantis)) 73 | 74 | ## [v1.2.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.2.0) (2019-09-17) 75 | 76 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.1.1...v1.2.0) 77 | 78 | **Merged pull requests:** 79 | 80 | - Remove solidus\_auth\_devise as dependency [\#53](https://github.com/solidusio-contrib/solidus_reviews/pull/53) ([kennyadsl](https://github.com/kennyadsl)) 81 | - Adopt CircleCI instead of Travis [\#51](https://github.com/solidusio-contrib/solidus_reviews/pull/51) ([aldesantis](https://github.com/aldesantis)) 82 | 83 | ## [v1.1.1](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.1.1) (2019-08-15) 84 | 85 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.1.0...v1.1.1) 86 | 87 | **Closed issues:** 88 | 89 | - Rubygems release [\#22](https://github.com/solidusio-contrib/solidus_reviews/issues/22) 90 | 91 | **Merged pull requests:** 92 | 93 | - Adds missing API features to the index and show endpoints [\#46](https://github.com/solidusio-contrib/solidus_reviews/pull/46) ([ericsaupe](https://github.com/ericsaupe)) 94 | 95 | ## [v1.1.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.1.0) (2019-08-14) 96 | 97 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/v1.0.0...v1.1.0) 98 | 99 | **Closed issues:** 100 | 101 | - Support rating only reviews [\#41](https://github.com/solidusio-contrib/solidus_reviews/issues/41) 102 | - Settings \> Reviews styling is broken [\#35](https://github.com/solidusio-contrib/solidus_reviews/issues/35) 103 | - Need add key to translate file ru.yml [\#23](https://github.com/solidusio-contrib/solidus_reviews/issues/23) 104 | - Error in solidus 2.1 due to add\_routes method [\#5](https://github.com/solidusio-contrib/solidus_reviews/issues/5) 105 | 106 | **Merged pull requests:** 107 | 108 | - General Typos [\#45](https://github.com/solidusio-contrib/solidus_reviews/pull/45) ([ericsaupe](https://github.com/ericsaupe)) 109 | - Updated API to include new fields [\#44](https://github.com/solidusio-contrib/solidus_reviews/pull/44) ([ericsaupe](https://github.com/ericsaupe)) 110 | - Fix specs [\#43](https://github.com/solidusio-contrib/solidus_reviews/pull/43) ([ericsaupe](https://github.com/ericsaupe)) 111 | - Rating only reviews [\#42](https://github.com/solidusio-contrib/solidus_reviews/pull/42) ([ericsaupe](https://github.com/ericsaupe)) 112 | - Adds verified purchaser [\#40](https://github.com/solidusio-contrib/solidus_reviews/pull/40) ([ericsaupe](https://github.com/ericsaupe)) 113 | - Cleanup [\#39](https://github.com/solidusio-contrib/solidus_reviews/pull/39) ([ericsaupe](https://github.com/ericsaupe)) 114 | - Added Images to Reviews [\#38](https://github.com/solidusio-contrib/solidus_reviews/pull/38) ([ericsaupe](https://github.com/ericsaupe)) 115 | - Add missing admin translation [\#37](https://github.com/solidusio-contrib/solidus_reviews/pull/37) ([jtapia](https://github.com/jtapia)) 116 | - Fix wrong locale id for require login in admin [\#36](https://github.com/solidusio-contrib/solidus_reviews/pull/36) ([kevinnio](https://github.com/kevinnio)) 117 | - Gem maintenance [\#34](https://github.com/solidusio-contrib/solidus_reviews/pull/34) ([spaghetticode](https://github.com/spaghetticode)) 118 | - Remove Solidus v2.3 from Travis config \(EOL\) [\#33](https://github.com/solidusio-contrib/solidus_reviews/pull/33) ([aitbw](https://github.com/aitbw)) 119 | - Add Solidus v2.7 and v2.8 to Travis config [\#32](https://github.com/solidusio-contrib/solidus_reviews/pull/32) ([aitbw](https://github.com/aitbw)) 120 | - Fix deprecation warnings [\#31](https://github.com/solidusio-contrib/solidus_reviews/pull/31) ([aitbw](https://github.com/aitbw)) 121 | - Fix Travis build [\#30](https://github.com/solidusio-contrib/solidus_reviews/pull/30) ([aitbw](https://github.com/aitbw)) 122 | - Fix validation i18n [\#29](https://github.com/solidusio-contrib/solidus_reviews/pull/29) ([mamhoff](https://github.com/mamhoff)) 123 | - Avoid conflicts when same rating HTML is repeated on page [\#28](https://github.com/solidusio-contrib/solidus_reviews/pull/28) ([spaghetticode](https://github.com/spaghetticode)) 124 | - Add missing TH tag [\#27](https://github.com/solidusio-contrib/solidus_reviews/pull/27) ([spaghetticode](https://github.com/spaghetticode)) 125 | - Use default translation key structure for validation message [\#26](https://github.com/solidusio-contrib/solidus_reviews/pull/26) ([aldesantis](https://github.com/aldesantis)) 126 | - Remove redundant rescue\_from from api controller [\#24](https://github.com/solidusio-contrib/solidus_reviews/pull/24) ([dgra](https://github.com/dgra)) 127 | - Improve specs [\#21](https://github.com/solidusio-contrib/solidus_reviews/pull/21) ([kennyadsl](https://github.com/kennyadsl)) 128 | - Refactor spec helper to let specs pass [\#20](https://github.com/solidusio-contrib/solidus_reviews/pull/20) ([kennyadsl](https://github.com/kennyadsl)) 129 | - Fix Trevis CI badge to use master branch status [\#19](https://github.com/solidusio-contrib/solidus_reviews/pull/19) ([kennyadsl](https://github.com/kennyadsl)) 130 | - Fix bundle install issue with solidus\_auth\_devise 2.0 [\#18](https://github.com/solidusio-contrib/solidus_reviews/pull/18) ([kennyadsl](https://github.com/kennyadsl)) 131 | - Fix resetting preferences in specs [\#17](https://github.com/solidusio-contrib/solidus_reviews/pull/17) ([kennyadsl](https://github.com/kennyadsl)) 132 | - Add italian translations [\#16](https://github.com/solidusio-contrib/solidus_reviews/pull/16) ([vassalloandrea](https://github.com/vassalloandrea)) 133 | - Add user email to admin review editing page [\#15](https://github.com/solidusio-contrib/solidus_reviews/pull/15) ([pelargir](https://github.com/pelargir)) 134 | - Api interface [\#14](https://github.com/solidusio-contrib/solidus_reviews/pull/14) ([dgra](https://github.com/dgra)) 135 | - General Solidus 2.3 updates [\#13](https://github.com/solidusio-contrib/solidus_reviews/pull/13) ([dgra](https://github.com/dgra)) 136 | - Prefix named route [\#11](https://github.com/solidusio-contrib/solidus_reviews/pull/11) ([pelargir](https://github.com/pelargir)) 137 | - Add the has\_many association for reviews from a user [\#10](https://github.com/solidusio-contrib/solidus_reviews/pull/10) ([dgra](https://github.com/dgra)) 138 | - Updates to Solidus admin UI [\#9](https://github.com/solidusio-contrib/solidus_reviews/pull/9) ([tvdeyen](https://github.com/tvdeyen)) 139 | - Update for Solidus 2.1 [\#6](https://github.com/solidusio-contrib/solidus_reviews/pull/6) ([kennyadsl](https://github.com/kennyadsl)) 140 | 141 | ## [v1.0.0](https://github.com/solidusio-contrib/solidus_reviews/tree/v1.0.0) (2017-02-03) 142 | 143 | [Full Changelog](https://github.com/solidusio-contrib/solidus_reviews/compare/8640958dc42f9472cb5cbb85cab981a44f4c45db...v1.0.0) 144 | 145 | **Merged pull requests:** 146 | 147 | - Allow solidus\_auth\_devise \< 1.5 [\#8](https://github.com/solidusio-contrib/solidus_reviews/pull/8) ([tvdeyen](https://github.com/tvdeyen)) 148 | - Test against multiple Solidus 1.x versions [\#7](https://github.com/solidusio-contrib/solidus_reviews/pull/7) ([tvdeyen](https://github.com/tvdeyen)) 149 | - Fix install generator [\#3](https://github.com/solidusio-contrib/solidus_reviews/pull/3) ([andrewjwu](https://github.com/andrewjwu)) 150 | - Do not add CRUD product routes to frontend [\#2](https://github.com/solidusio-contrib/solidus_reviews/pull/2) ([mamhoff](https://github.com/mamhoff)) 151 | - Move factories into `lib` [\#1](https://github.com/solidusio-contrib/solidus_reviews/pull/1) ([alexblackie](https://github.com/alexblackie)) 152 | 153 | 154 | 155 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 156 | --------------------------------------------------------------------------------