is_reviewable 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|