is_reviewable 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.textile +421 -0
- data/Rakefile +51 -0
- data/generators/is_reviewable_migration/is_reviewable_migration_generator.rb +12 -0
- data/generators/is_reviewable_migration/templates/reviews_migration.rb +34 -0
- data/generators/is_reviewable_model/is_reviewable_model_generator.rb +11 -0
- data/generators/is_reviewable_model/templates/review_model.rb +5 -0
- data/lib/is_reviewable.rb +34 -0
- data/lib/is_reviewable/review.rb +38 -0
- data/lib/is_reviewable/reviewable.rb +430 -0
- data/lib/is_reviewable/reviewer.rb +17 -0
- data/lib/is_reviewable/support.rb +49 -0
- data/rails/init.rb +1 -0
- data/test/is_reviewable_test.rb +234 -0
- data/test/test_helper.rb +59 -0
- metadata +84 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
class IsReviewableMigrationGenerator < Rails::Generator::Base
|
4
|
+
|
5
|
+
def manifest
|
6
|
+
record do |m|
|
7
|
+
m.migration_template 'reviews_migration.rb',
|
8
|
+
File.join('db', 'migrate'), :migration_file_name => 'is_reviewable_migration'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
class IsReviewableMigration < ActiveRecord::Migration
|
4
|
+
def self.up
|
5
|
+
create_table :reviews do |t|
|
6
|
+
t.references :reviewable, :polymorphic => true
|
7
|
+
|
8
|
+
t.references :reviewer, :polymorphic => true
|
9
|
+
t.string :ip, :limit => 24
|
10
|
+
|
11
|
+
t.float :rating
|
12
|
+
t.text :body
|
13
|
+
|
14
|
+
#
|
15
|
+
# Custom fields goes here...
|
16
|
+
#
|
17
|
+
# t.string :title
|
18
|
+
# t.string :mood
|
19
|
+
# ...
|
20
|
+
#
|
21
|
+
|
22
|
+
t.timestamps
|
23
|
+
end
|
24
|
+
|
25
|
+
add_index :reviews, :reviewer_id
|
26
|
+
add_index :reviews, :reviewer_type
|
27
|
+
add_index :reviews, [:reviewer_id, :reviewer_type]
|
28
|
+
add_index :reviews, [:reviewable_id, :reviewable_type]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.down
|
32
|
+
drop_table :reviews
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), *%w[is_reviewable review])
|
3
|
+
require File.join(File.dirname(__FILE__), *%w[is_reviewable reviewable])
|
4
|
+
require File.join(File.dirname(__FILE__), *%w[is_reviewable reviewer])
|
5
|
+
require File.join(File.dirname(__FILE__), *%w[is_reviewable support])
|
6
|
+
|
7
|
+
module IsReviewable
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
class IsReviewableError < ::StandardError
|
12
|
+
def initialize(message)
|
13
|
+
::IsReviewable.log message, :debug
|
14
|
+
super message
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
InvalidConfigValueError = ::Class.new(IsReviewableError)
|
19
|
+
InvalidReviewerError = ::Class.new(IsReviewableError)
|
20
|
+
InvalidReviewValueError = ::Class.new(IsReviewableError)
|
21
|
+
RecordError = ::Class.new(IsReviewableError)
|
22
|
+
|
23
|
+
mattr_accessor :verbose
|
24
|
+
|
25
|
+
@@verbose = ::Object.const_defined?(:RAILS_ENV) ? (::RAILS_ENV.to_sym == :development) : true
|
26
|
+
|
27
|
+
def log(message, level = :info)
|
28
|
+
return unless @@verbose
|
29
|
+
level = :info if level.blank?
|
30
|
+
@@logger ||= ::Logger.new(::STDOUT)
|
31
|
+
@@logger.send(level.to_sym, message)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module IsReviewable
|
4
|
+
class Review < ::ActiveRecord::Base
|
5
|
+
|
6
|
+
belongs_to :reviewable, :polymorphic => true
|
7
|
+
belongs_to :reviewer, :polymorphic => true
|
8
|
+
|
9
|
+
# Order.
|
10
|
+
named_scope :in_order, :order => 'created_at ASC'
|
11
|
+
named_scope :most_recent, :order => 'created_at DESC'
|
12
|
+
named_scope :lowest_rating, :order => 'rating ASC'
|
13
|
+
named_scope :highest_rating, :order => 'rating DESC'
|
14
|
+
|
15
|
+
# Filters.
|
16
|
+
named_scope :limit, lambda { |number_of_items| {:limit => number_of_items} }
|
17
|
+
named_scope :since, lambda { |created_at_datetime| {:conditions => ['created_at >= ?', created_at_datetime]} }
|
18
|
+
named_scope :recent, lambda { |arg|
|
19
|
+
if [::ActiveSupport::TimeWithZone, ::DateTime].any? { |c| c.is_a?(arg) }
|
20
|
+
{:conditions => ['created_at >= ?', arg]}
|
21
|
+
else
|
22
|
+
{:limit => arg.to_i}
|
23
|
+
end
|
24
|
+
}
|
25
|
+
named_scope :between_dates, lambda { |from_date, to_date| {:conditions => {:created_at => (from_date..to_date)}} }
|
26
|
+
named_scope :with_rating, lambda { |rating_value_or_range| {:conditions => {:rating => rating_value_or_range}} }
|
27
|
+
named_scope :with_a_rating, :conditions => ['rating IS NOT NULL']
|
28
|
+
named_scope :without_a_rating, :conditions => ['rating IS NULL']
|
29
|
+
named_scope :with_a_body, :conditions => ['body IS NOT NULL AND LENGTH(body) > 0']
|
30
|
+
named_scope :without_a_body, :conditions => ['body IS NULL OR LENGTH(body) = 0']
|
31
|
+
named_scope :complete, :conditions => ['rating IS NOT NULL AND body IS NOT NULL AND LENGTH(body) > 0']
|
32
|
+
named_scope :of_reviewable_type, lambda { |type| {:conditions => Support.polymorphic_conditions_for(type, :reviewable, :type)} }
|
33
|
+
named_scope :by_reviewer_type, lambda { |type| {:conditions => Support.polymorphic_conditions_for(type, :reviewer, :type)} }
|
34
|
+
named_scope :on, lambda { |reviewable| {:conditions => Support.polymorphic_conditions_for(reviewable, :reviewable)} }
|
35
|
+
named_scope :by, lambda { |reviewer| {:conditions => Support.polymorphic_conditions_for(reviewer, :reviewer)} }
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,430 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
unless defined?(::Review)
|
4
|
+
class Review < ::IsReviewable::Review
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module IsReviewable
|
9
|
+
module Reviewable
|
10
|
+
|
11
|
+
REVIEW_CLASS = ::Review
|
12
|
+
DEFAULT_SCALE = 1..5
|
13
|
+
DEFAULT_ACCEPT_IP = false
|
14
|
+
CACHABLE_FIELDS = [
|
15
|
+
:reviews_count,
|
16
|
+
:average_rating
|
17
|
+
].freeze
|
18
|
+
ASSOCIATION_FIELDS = [
|
19
|
+
:reviewable_id,
|
20
|
+
:reviewable_type,
|
21
|
+
:reviewer_id,
|
22
|
+
:reviewer_type,
|
23
|
+
:ip
|
24
|
+
].freeze
|
25
|
+
CONTENT_FIELDS = [
|
26
|
+
:rating,
|
27
|
+
:body
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
def self.included(base) #:nodoc:
|
31
|
+
base.class_eval do
|
32
|
+
extend ClassMethods
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks if this object reviewable or not.
|
36
|
+
#
|
37
|
+
def reviewable?; false; end
|
38
|
+
alias :is_reviewable? :reviewable?
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
# TODO: Document this method...thoroughly.
|
44
|
+
#
|
45
|
+
# Examples:
|
46
|
+
#
|
47
|
+
# is_reviewable :by => :user, :scale => 0..5, :total_precision => 2
|
48
|
+
#
|
49
|
+
def is_reviewable(*args)
|
50
|
+
options = args.extract_options!
|
51
|
+
options.reverse_merge!(
|
52
|
+
:by => Reviewer::DEFAULT_CLASS_NAME,
|
53
|
+
:scale => options[:values] || options[:range] || DEFAULT_SCALE,
|
54
|
+
:accept_ip => options[:anonymous] || DEFAULT_ACCEPT_IP # i.e. also accepts unique IPs as reviewer
|
55
|
+
)
|
56
|
+
scale = options[:scale]
|
57
|
+
if options[:step].blank? && options[:steps].blank?
|
58
|
+
options[:steps] = scale.last - scale.first + 1
|
59
|
+
else
|
60
|
+
# use :step or :steps beneath
|
61
|
+
end
|
62
|
+
options[:total_precision] ||= options[:average_precision] || scale.first.to_s.split('.').last.size # == 1
|
63
|
+
|
64
|
+
# Check for incorrect input values, and handle ranges of floats with help of :step. E.g. :scale => 1.0..5.0.
|
65
|
+
|
66
|
+
if scale.is_a?(::Range) && scale.first.is_a?(::Float)
|
67
|
+
options[:step] = (scale.last - scale.first) / (options[:steps] - 1) if options[:step].blank?
|
68
|
+
options[:scale] = scale.first.step(scale.last, options[:step]).collect { |value| value }
|
69
|
+
else
|
70
|
+
options[:scale] = scale.to_a.collect! { |v| v.to_f }
|
71
|
+
end
|
72
|
+
raise InvalidConfigValueError, ":scale/:range/:values must consist of numeric values only." unless options[:scale].all? { |v| v.is_a?(::Numeric) }
|
73
|
+
raise InvalidConfigValueError, ":total_precision must be an integer." unless options[:total_precision].is_a?(::Fixnum)
|
74
|
+
|
75
|
+
# Assocations: Review class (e.g. Review).
|
76
|
+
options[:review_class] = REVIEW_CLASS
|
77
|
+
|
78
|
+
# Reviewer class(es).
|
79
|
+
options[:by] = [options[:by]] unless options[:by].is_a?(::Array)
|
80
|
+
options[:reviewer_class_names] = options[:by].collect { |class_name| class_name.to_s.singularize.classify }
|
81
|
+
options[:reviewer_classes] = options[:reviewer_class_names].collect do |class_name|
|
82
|
+
begin
|
83
|
+
class_name.constantize
|
84
|
+
rescue NameError => e
|
85
|
+
raise InvalidReviewerError, "Reviewer class #{options[:reviewer_class_name]} not defined, needs to be defined. #{e}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Had to do this here - not sure why. Subclassing Review be enough? =S
|
90
|
+
::Review.class_eval do
|
91
|
+
belongs_to :reviewable, :polymorphic => true unless self.respond_to?(:reviewable)
|
92
|
+
belongs_to :reviewer, :polymorphic => true unless self.respond_to?(:reviewer)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Assocations: Reviewer class(es) (e.g. User, Account, ...).
|
96
|
+
options[:reviewer_classes].each do |reviewer_class|
|
97
|
+
if ::Object.const_defined?(reviewer_class.name.to_sym)
|
98
|
+
reviewer_class.class_eval do
|
99
|
+
has_many :reviews, :as => :reviewer, :dependent => :delete_all
|
100
|
+
|
101
|
+
# Polymorphic has-many-through not supported (has_many :reviewables, :through => :reviews), so:
|
102
|
+
def reviewables(*args)
|
103
|
+
query_options = args.extract_options!
|
104
|
+
query_options[:include] = [:reviewable]
|
105
|
+
query_options.reverse_merge!(
|
106
|
+
:conditions => Support.polymorphic_conditions_for(self, :reviewer)
|
107
|
+
)
|
108
|
+
|
109
|
+
::Review.find(:all, query_options).collect! { |review| review.reviewable }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Assocations: Reviewable class (self) (e.g. Page).
|
116
|
+
self.class_eval do
|
117
|
+
has_many :reviews, :as => :reviewable, :dependent => :delete_all
|
118
|
+
|
119
|
+
# Polymorphic has-many-through not supported (has_many :reviewers, :through => :reviews), so:
|
120
|
+
def reviewers(*args)
|
121
|
+
query_options = args.extract_options!
|
122
|
+
query_options[:include] = [:reviewer]
|
123
|
+
query_options.reverse_merge!(
|
124
|
+
:conditions => Support.polymorphic_conditions_for(self, :reviewable)
|
125
|
+
)
|
126
|
+
|
127
|
+
::Review.find(:all, query_options).collect! { |review| review.reviewer }
|
128
|
+
end
|
129
|
+
|
130
|
+
before_create :init_reviewable_caching_fields
|
131
|
+
|
132
|
+
include ::IsReviewable::Reviewable::InstanceMethods
|
133
|
+
extend ::IsReviewable::Reviewable::Finders
|
134
|
+
end
|
135
|
+
|
136
|
+
# Save the initialized options for this class.
|
137
|
+
write_inheritable_attribute :is_reviewable_options, options
|
138
|
+
class_inheritable_reader :is_reviewable_options
|
139
|
+
end
|
140
|
+
|
141
|
+
# Checks if this object reviewable or not.
|
142
|
+
#
|
143
|
+
def reviewable?
|
144
|
+
@@reviewable ||= self.respond_to?(:is_reviewable_options, true)
|
145
|
+
end
|
146
|
+
alias :is_reviewable? :reviewable?
|
147
|
+
|
148
|
+
# The rating scale used for this reviewable class.
|
149
|
+
#
|
150
|
+
def reviewable_scale
|
151
|
+
self.is_reviewable_options[:scale]
|
152
|
+
end
|
153
|
+
alias :rating_scale :reviewable_scale
|
154
|
+
|
155
|
+
# The rating value precision used for this reviewable class.
|
156
|
+
#
|
157
|
+
# Using Rails default behaviour:
|
158
|
+
#
|
159
|
+
# Float#round(<precision>)
|
160
|
+
#
|
161
|
+
def reviewable_precision
|
162
|
+
self.is_reviewable_options[:total_precision]
|
163
|
+
end
|
164
|
+
alias :rating_precision :reviewable_precision
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
# Check if the requested reviewer object is a valid reviewer.
|
169
|
+
#
|
170
|
+
def validate_reviewer(identifiers)
|
171
|
+
raise InvalidReviewerError, "Argument can't be nil: no reviewer object or IP provided." if identifiers.blank?
|
172
|
+
reviewer = identifiers[:reviewer] || identifiers[:user] || identifiers[:account] || identifiers[:ip]
|
173
|
+
is_ip = Support.is_ip?(reviewer)
|
174
|
+
reviewer = reviewer.to_s.strip if is_ip
|
175
|
+
unless Support.is_active_record?(reviewer) || is_ip
|
176
|
+
raise InvalidReviewerError, "Reviewer is of wrong type: #{reviewer.inspect}."
|
177
|
+
end
|
178
|
+
raise InvalidReviewerError, "Reviewing based on IP is disabled." if is_ip && !self.is_reviewable_options[:accept_ip]
|
179
|
+
reviewer
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
module InstanceMethods
|
185
|
+
|
186
|
+
# Checks if this object reviewable or not.
|
187
|
+
#
|
188
|
+
def reviewable?
|
189
|
+
self.class.reviewable?
|
190
|
+
end
|
191
|
+
alias :is_reviewable? :reviewable?
|
192
|
+
|
193
|
+
# The rating scale used for this reviewable class.
|
194
|
+
#
|
195
|
+
def reviewable_scale
|
196
|
+
self.class.reviewable_scale
|
197
|
+
end
|
198
|
+
alias :rating_scale :reviewable_scale
|
199
|
+
|
200
|
+
# The rating value precision used for this reviewable class.
|
201
|
+
#
|
202
|
+
def reviewable_precision
|
203
|
+
self.class.reviewable_precision
|
204
|
+
end
|
205
|
+
alias :rating_precision :reviewable_precision
|
206
|
+
|
207
|
+
# Reviewed at datetime.
|
208
|
+
#
|
209
|
+
def reviewed_at
|
210
|
+
self.created_at if self.respond_to?(:created_at)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Calculate average rating for this reviewable object.
|
214
|
+
#
|
215
|
+
def average_rating(recalculate = false)
|
216
|
+
if !recalculate && self.reviewable_caching_fields?(:average_rating)
|
217
|
+
self.average_rating
|
218
|
+
else
|
219
|
+
conditions = self.reviewable_conditions(true)
|
220
|
+
conditions[0] << ' AND rating IS NOT NULL'
|
221
|
+
::Review.average(:rating,
|
222
|
+
:conditions => conditions).to_f.round(self.is_reviewable_options[:total_precision])
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Calculate average rating for this reviewable object within a domain of reviewers.
|
227
|
+
#
|
228
|
+
def average_rating_by(identifiers)
|
229
|
+
# FIXME: Only count non-nil ratings, i.e. See "average_rating".
|
230
|
+
::Review.average(:rating,
|
231
|
+
:conditions => self.reviewer_conditions(identifiers).merge(self.reviewable_conditions)
|
232
|
+
).to_f.round(self.is_reviewable_options[:total_precision])
|
233
|
+
end
|
234
|
+
|
235
|
+
# Get the total number of reviews for this object.
|
236
|
+
#
|
237
|
+
def total_reviews(recalculate = false)
|
238
|
+
if !recalculate && self.reviewable_caching_fields?(:total_reviews)
|
239
|
+
self.total_reviews
|
240
|
+
else
|
241
|
+
::Review.count(:conditions => self.reviewable_conditions)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
alias :number_of_reviews :total_reviews
|
245
|
+
|
246
|
+
# Is this object reviewed by anyone?
|
247
|
+
#
|
248
|
+
def reviewed?
|
249
|
+
self.total_reviews > 0
|
250
|
+
end
|
251
|
+
alias :is_reviewed? :reviewed?
|
252
|
+
|
253
|
+
# Check if an item was already reviewed by the given reviewer or ip.
|
254
|
+
#
|
255
|
+
# === identifiers hash:
|
256
|
+
# * <tt>:ip</tt> - identify with IP
|
257
|
+
# * <tt>:reviewer/:user/:account</tt> - identify with a reviewer-model (e.g. User, ...)
|
258
|
+
#
|
259
|
+
def reviewed_by?(identifiers)
|
260
|
+
self.reviews.exists?(:conditions => reviewer_conditions(identifiers))
|
261
|
+
end
|
262
|
+
alias :is_reviewed_by? :reviewed_by?
|
263
|
+
|
264
|
+
# Get review already reviewed by the given reviewer or ip.
|
265
|
+
#
|
266
|
+
def review_by(identifiers)
|
267
|
+
self.reviews.find(:first, :conditions => reviewer_conditions(identifiers))
|
268
|
+
end
|
269
|
+
|
270
|
+
# View the object with and identifier (user or ip) - create new if new reviewer.
|
271
|
+
#
|
272
|
+
# === identifiers_and_options hash:
|
273
|
+
# * <tt>:reviewer/:user/:account</tt> - identify with a reviewer-model or IP (e.g. User, Account, ..., "128.0.0.1")
|
274
|
+
# * <tt>:rating</tt> - Review rating value, e.g. 3.5, "3.5", ... (optional)
|
275
|
+
# * <tt>:body</tt> - Review text body, e.g. "Lorem *ipsum*..." (optional)
|
276
|
+
# * <tt>:*</tt> - Any custom review field, e.g. :reviewer_mood => "angry" (optional)
|
277
|
+
#
|
278
|
+
def review!(identifiers_and_options)
|
279
|
+
begin
|
280
|
+
reviewer = self.validate_reviewer(identifiers_and_options)
|
281
|
+
review = self.review_by(identifiers_and_options)
|
282
|
+
|
283
|
+
# Except for the reserved fields, any Review-fields should be be able to update.
|
284
|
+
review_values = identifiers_and_options.except(*ASSOCIATION_FIELDS)
|
285
|
+
review_values[:rating] = review_values[:rating].to_f if review_values[:rating].present?
|
286
|
+
|
287
|
+
if review_values[:rating].present? && !self.valid_rating_value?(review_values[:rating])
|
288
|
+
raise InvalidReviewValueError, "Invalid rating value: #{review_values[:rating]} not in [#{self.rating_scale.join(', ')}]."
|
289
|
+
end
|
290
|
+
|
291
|
+
unless review.present?
|
292
|
+
# An un-existing reviewer of this reviewable object => Create a new review.
|
293
|
+
review = ::Review.new do |r|
|
294
|
+
r.reviewable_id = self.id
|
295
|
+
r.reviewable_type = self.class.name
|
296
|
+
|
297
|
+
if Support.is_active_record?(reviewer)
|
298
|
+
r.reviewer_id = reviewer.id
|
299
|
+
r.reviewer_type = reviewer.class.name
|
300
|
+
else
|
301
|
+
r.ip = reviewer
|
302
|
+
end
|
303
|
+
end
|
304
|
+
self.reviews << review
|
305
|
+
else
|
306
|
+
# An existing reviewer of this reviewable object => Update the existing review.
|
307
|
+
end
|
308
|
+
|
309
|
+
# Update non-association attributes, such as rating, body (the review text), and any custom fields.
|
310
|
+
review.attributes = review_values.slice(*review.attribute_names.collect { |an| an.to_sym })
|
311
|
+
|
312
|
+
if self.reviewable_caching_fields?(:total_reviews)
|
313
|
+
begin
|
314
|
+
self.cached_total_reviews += 1 if review.new_record?
|
315
|
+
rescue
|
316
|
+
self.cached_total_reviews = self.total_reviews(true)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
if self.reviewable_caching_fields?(:average_rating)
|
321
|
+
self.cached_average_rating = self.average_rating(true)
|
322
|
+
# new_rating = review.rating - (old_rating || 0)
|
323
|
+
# self.cached_average_rating = (self.cached_average_rating + new_rating) / self.cached_total_reviews.to_f
|
324
|
+
end
|
325
|
+
|
326
|
+
review.save && self.save_without_validation
|
327
|
+
review
|
328
|
+
rescue InvalidReviewerError, InvalidReviewValueError => e
|
329
|
+
raise e
|
330
|
+
rescue Exception => e
|
331
|
+
raise RecordError, "Could not create/update review #{review.inspect} by #{reviewer.inspect}: #{e}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Remove the review of this reviewer from this object.
|
336
|
+
#
|
337
|
+
def unreview!(identifiers)
|
338
|
+
review = self.review_by(identifiers)
|
339
|
+
review_rating = review.rating if review.present?
|
340
|
+
|
341
|
+
if review && review.destroy
|
342
|
+
if self.reviewable_caching_fields?(:total_reviews)
|
343
|
+
begin
|
344
|
+
self.cached_total_reviews -= 1
|
345
|
+
rescue
|
346
|
+
self.cached_total_reviews = self.reviews.size
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
if self.reviewable_caching_fields?(:average_rating)
|
351
|
+
self.cached_average_rating = self.average_rating(true)
|
352
|
+
# self.cached_average_rating = (self.cached_average_rating - review_rating) / self.cached_total_reviews.to_f
|
353
|
+
end
|
354
|
+
|
355
|
+
self.save_without_validation
|
356
|
+
else
|
357
|
+
raise RecordError, "Could not un-review #{review.inspect} by #{reviewer.inspect}: #{e}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
protected
|
362
|
+
|
363
|
+
# Checks if a certain value is a valid rating value for this reviewable object.
|
364
|
+
#
|
365
|
+
def valid_rating_value?(value_or_values)
|
366
|
+
value_or_values = [*value_or_values]
|
367
|
+
value_or_values.size == (value_or_values & self.rating_scale).size
|
368
|
+
end
|
369
|
+
alias :valid_rating_values? :valid_rating_value?
|
370
|
+
|
371
|
+
# Cachable fields for this reviewable class.
|
372
|
+
#
|
373
|
+
def reviewable_caching_fields
|
374
|
+
CACHABLE_FIELDS
|
375
|
+
end
|
376
|
+
|
377
|
+
# Checks if there are any cached fields for this reviewable class.
|
378
|
+
#
|
379
|
+
def reviewable_caching_fields?(*fields)
|
380
|
+
fields = CACHABLE_FIELDS if fields.blank?
|
381
|
+
fields.all? { |field| self.attributes.has_key?(:"cached_#{field}") }
|
382
|
+
end
|
383
|
+
alias :has_reviewable_caching_fields? :reviewable_caching_fields?
|
384
|
+
|
385
|
+
# Initialize any cached fields.
|
386
|
+
#
|
387
|
+
def init_reviewable_caching_fields
|
388
|
+
self.cached_total_reviews = 0 if self.reviewable_caching_fields?(:cached_total_reviews)
|
389
|
+
self.cached_average_rating = 0.0 if self.reviewable_caching_fields?(:average_rating)
|
390
|
+
end
|
391
|
+
|
392
|
+
def reviewable_conditions(as_array = false)
|
393
|
+
conditions = {:reviewable_id => self.id, :reviewable_type => self.class.name}
|
394
|
+
as_array ? Support.hash_conditions_as_array(conditions) : conditions
|
395
|
+
end
|
396
|
+
|
397
|
+
# Generate query conditions.
|
398
|
+
#
|
399
|
+
def reviewer_conditions(identifiers, as_array = false)
|
400
|
+
reviewer = self.validate_reviewer(identifiers)
|
401
|
+
if Support.is_active_record?(reviewer)
|
402
|
+
conditions = {:reviewer_id => reviewer.id, :reviewer_type => reviewer.class.name}
|
403
|
+
else
|
404
|
+
conditions = {:ip => reviewer.to_s}
|
405
|
+
end
|
406
|
+
as_array ? Support.hash_conditions_as_array(conditions) : conditions
|
407
|
+
end
|
408
|
+
|
409
|
+
def validate_reviewer(identifiers)
|
410
|
+
self.class.send(:validate_reviewer, identifiers)
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
module Finders
|
416
|
+
|
417
|
+
# When the has-many-through-polymoprhic issue is solved:
|
418
|
+
#
|
419
|
+
# * users that reviewed this with rating X
|
420
|
+
# * users that reviewed this, also reviewed [...] with same rating
|
421
|
+
|
422
|
+
end
|
423
|
+
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Extend ActiveRecord.
|
428
|
+
::ActiveRecord::Base.class_eval do
|
429
|
+
include ::IsReviewable::Reviewable
|
430
|
+
end
|