acts_as_nps_rateable 0.0.4 → 0.0.5
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.
- checksums.yaml +6 -14
- data/.ruby-version +1 -1
- data/Gemfile +5 -0
- data/README.md +95 -3
- data/acts_as_nps_rateable.gemspec +2 -2
- data/lib/acts_as_nps_rateable.rb +2 -1
- data/lib/acts_as_nps_rateable/hook.rb +8 -2
- data/lib/acts_as_nps_rateable/railtie.rb +1 -0
- data/lib/acts_as_nps_rateable/rateable.rb +156 -0
- data/lib/acts_as_nps_rateable/rater.rb +80 -0
- data/lib/acts_as_nps_rateable/version.rb +1 -1
- data/lib/generators/acts_as_nps_rateable/install_generator.rb +19 -4
- data/lib/generators/acts_as_nps_rateable/templates/01-migration.rb +1 -0
- data/lib/generators/acts_as_nps_rateable/templates/02-migration.rb +2 -1
- data/lib/generators/acts_as_nps_rateable/templates/03-migration.rb +11 -0
- data/lib/generators/acts_as_nps_rateable/templates/04-migration.rb +11 -0
- data/lib/nps_rating.rb +55 -15
- metadata +21 -17
- data/lib/acts_as_nps_rateable/instance_methods.rb +0 -63
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
OGI3YmVlNmNmM2I2Mzc1NDczMjJmYmYwMTUwMGRkM2E0ZjMxNTRkY2MxMGI5
|
10
|
-
NmI5YmYzN2NhYmQ2OGQwZmIzY2U3ODgxNGZkMjk0MjhiYTc3YmNmODkxYTcz
|
11
|
-
MTg0NDBhN2ExNzQ4ZGZjZjA3OWVkZmUxZjJkMDMyZmIyNDNkN2Y=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZTU4YzNmY2M2ZmJjODY4NmNhYWUyYWUxNjUxOTM1OGFjMWI3OTFlOGZhYTY2
|
14
|
-
OGExY2I2ZTkyZDlmNTkwMDRkMmE4MjFjOTZiNDhkMWMwZWIyMWIzMzQ3N2Y2
|
15
|
-
NjljOGY3MTU5YzVjMmE5ZWE0MTU4YzBkYjkyYjU2ODUyYTM4MTE=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6e58be26857db542d7dd276514b5788241047d96
|
4
|
+
data.tar.gz: 1e6fc6ba2fbdfa813d845211d70c893cf4b5bdd3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6257642935cd4dc8173ebfa8575b6c1f3009a283b5abce8c86d5b094bbb0f9daf738af366fd88171ed5e324ea23ba925d4b953d729216a21f338beacbf3ad3ec
|
7
|
+
data.tar.gz: 1626017a39016b48797e6299bf3385dda58c400fa6e4831df7362237b3598bd73f5ddbb653442c59aec96198a173e8c9e789c9354b1ca5117afcffc6cf1119eb
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
ruby-2.2.2
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# acts_as_nps_rateable
|
2
2
|
|
3
|
-
|
3
|
+
acts_as_nps_rateable provides Net Promoter Score (NPS) ratings and analysis for your ActiveRecord-based models. Net
|
4
|
+
Promoter Score is a measurement of customer satisfaction; it is the ratio of the percentage of customers who would
|
5
|
+
recommend your product/service to the percentage of customers who would not recommend it.
|
6
|
+
|
7
|
+
NPS is documented in more detail at Wikipedia: http://en.wikipedia.org/wiki/Net_Promoter
|
4
8
|
|
5
9
|
## Installation
|
6
10
|
|
@@ -8,6 +12,10 @@ Add this line to your application's Gemfile:
|
|
8
12
|
|
9
13
|
gem 'acts_as_nps_rateable'
|
10
14
|
|
15
|
+
If you're adding this gem to a rails 3 application, you should change the line to be:
|
16
|
+
|
17
|
+
gem 'acts_as_nps_rateable', '=0.0.4'
|
18
|
+
|
11
19
|
And then execute:
|
12
20
|
|
13
21
|
$ bundle
|
@@ -16,9 +24,93 @@ Or install it yourself as:
|
|
16
24
|
|
17
25
|
$ gem install acts_as_nps_rateable
|
18
26
|
|
27
|
+
Once you've installed the gem, you'll need to install the migrations required by acts_as_nps_rateable into your project
|
28
|
+
and run all pending migrations:
|
29
|
+
|
30
|
+
rails generate acts_as_nps_rateable:install
|
31
|
+
rake db:migrate
|
32
|
+
|
33
|
+
Now you're ready to use acts_as_nps_rateable.
|
34
|
+
|
35
|
+
## Upgrading
|
36
|
+
|
37
|
+
If you're upgrading from a previous version of acts_as_nps_rateable, the major change here is that v0.0.5 is now only
|
38
|
+
compatible with rails 4.2. Making it rails 3 compatible shouldn't be a problem but I've moved on and don't have any
|
39
|
+
rails 3 applications to add it to.
|
40
|
+
|
41
|
+
I welcome a pull request that adds back rails 3 compatiblity.
|
42
|
+
|
19
43
|
## Usage
|
20
44
|
|
21
|
-
|
45
|
+
### Setting up the Models
|
46
|
+
|
47
|
+
acts_as_nps_rateable relies on the concept of rateables and raters.
|
48
|
+
|
49
|
+
A rateable is any model which can be given a rating from 0 to 10 (inclusive) and an optional review. An example of a
|
50
|
+
rateable might be a Restaurant model. Set up a rateable by adding the following line to the model you wish to be a
|
51
|
+
rateable:
|
52
|
+
|
53
|
+
acts_as_nps_rateable
|
54
|
+
|
55
|
+
e.g.
|
56
|
+
|
57
|
+
class Restaurant < ActiveRecord::Base
|
58
|
+
acts_as_nps_rateable
|
59
|
+
end
|
60
|
+
|
61
|
+
A rater is any model which can give a rateable that rating and is essentially the user to attribute that rating to. An
|
62
|
+
example of a rater will usually be a User model it's possible to have multiple raters in your system e.g. an Employee
|
63
|
+
and a Manager, both of which can rate any rateable. Set up a rater by adding the following line to the model you wish
|
64
|
+
to be a rater:
|
65
|
+
|
66
|
+
acts_as_nps_rater
|
67
|
+
|
68
|
+
e.g.
|
69
|
+
|
70
|
+
class User < ActiveRecord::Base
|
71
|
+
acts_as_nps_rater
|
72
|
+
end
|
73
|
+
|
74
|
+
### Rating
|
75
|
+
|
76
|
+
Let's assume we have the User and Restaurant models in the system with specific instances of each in our code as follows:
|
77
|
+
|
78
|
+
snob = User.find(31337)
|
79
|
+
rateotu = Restaurant.find(42)
|
80
|
+
|
81
|
+
Our rater has the following methods available to her:
|
82
|
+
|
83
|
+
snob.rate(rateotu, 5) # Rates the restaurant a 5 on a scale from 0 to 10 inclusive
|
84
|
+
snob.rate(rateotu, 3) # Overwrites the restaurant's rating to a 3
|
85
|
+
snob.average_rating # Returns the average score of all the ratings snob has given
|
86
|
+
snob.rated?(rateotu) # Returns true if snob has rated this restaurant before. False otherwise.
|
87
|
+
snob.rating_for(rateotu) # Returns the score snob gave this restaurant before. This could be nil.
|
88
|
+
|
89
|
+
snob.review("It was OK", rateotu) # Adds a review to an existing rating by snob for this restaurant.
|
90
|
+
|
91
|
+
Our rateable has the following methods available:
|
92
|
+
|
93
|
+
rateotu.rate(9, snob) # Adds a rating for the restaurant by a given rater
|
94
|
+
rateotu.rate(10, snob) # Overwrites the rating given by snob with a better number
|
95
|
+
rateotu.average_rating # Returns the average score of all the ratings this restaurant has received
|
96
|
+
rateotu.rated_by?(snob) # Returns true if snob has rated this restaurant before. False otherwise.
|
97
|
+
rateotu.rating_by(snob) # Returns the score snob gave this restaurant before. This could be nil.
|
98
|
+
|
99
|
+
# We will use this recent_ratings variable in some examples below. It's meant to be all ratings that were recorded
|
100
|
+
# in the last month.
|
101
|
+
recent_ratings = rateotu.nps_ratings.where("created_at > ?", 1.month.ago)
|
102
|
+
|
103
|
+
rateotu.promoters # Returns a count of the number of all ratings considered promoters in the NPS sense
|
104
|
+
rateotu.promoters(recent_ratings) # Returns a count of the number of recent ratings considered promoters
|
105
|
+
rateotu.passives # Returns a count of the number of all ratings considered passives in the NPS sense
|
106
|
+
rateotu.passives(recent_ratings) # Returns a count of the number of recent ratings considered passives
|
107
|
+
rateotu.detractors # Returns a count of the number of all ratings considered detractors in the NPS sense
|
108
|
+
rateotu.detractors(recent_ratings) # Returns a count of the number of recent ratings considered detractors
|
109
|
+
|
110
|
+
rateotu.net_promoter_score # Returns the Net Promoter Score based on all ratings for this restaurant
|
111
|
+
rateotu.net_promoter_score(recent_ratings) # Returns the Net Promoter Score based on recent ratings only
|
112
|
+
|
113
|
+
rateotu.review("It was Great!", snob) # Adds a review to an existing rating by snob for this restaurant
|
22
114
|
|
23
115
|
## Contributing
|
24
116
|
|
@@ -12,8 +12,8 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.summary = %q{Net Promoter Score ratings and analysis for ActiveRecord models}
|
13
13
|
gem.homepage = 'https://github.com/sjaveed/acts_as_nps_rateable'
|
14
14
|
|
15
|
-
gem.add_dependency 'activerecord', '
|
16
|
-
gem.add_dependency 'rails', '
|
15
|
+
gem.add_dependency 'activerecord', '>= 4.2.0'
|
16
|
+
gem.add_dependency 'rails', '>= 4.2.0'
|
17
17
|
|
18
18
|
gem.files = `git ls-files`.split($/)
|
19
19
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
data/lib/acts_as_nps_rateable.rb
CHANGED
@@ -2,7 +2,8 @@ require File.join(File.dirname(__FILE__), "acts_as_nps_rateable/railtie")
|
|
2
2
|
|
3
3
|
module ActsAsNpsRateable
|
4
4
|
autoload :Hook, File.join(File.dirname(__FILE__), "acts_as_nps_rateable/hook")
|
5
|
-
autoload :
|
5
|
+
autoload :Rateable, File.join(File.dirname(__FILE__), "acts_as_nps_rateable/rateable")
|
6
|
+
autoload :Rater, File.join(File.dirname(__FILE__), "acts_as_nps_rateable/rater")
|
6
7
|
end
|
7
8
|
|
8
9
|
require 'nps_rating'
|
@@ -1,10 +1,16 @@
|
|
1
|
+
# @private
|
1
2
|
# This is the hook that implements the acts_as_nps_rateable method
|
2
3
|
# and includes all acts_as_nps_rateable instance methods as needed
|
3
|
-
|
4
4
|
module ActsAsNpsRateable::Hook
|
5
5
|
def acts_as_nps_rateable
|
6
6
|
has_many :nps_ratings, :as => :nps_rateable, class_name: 'ActsAsNpsRateable::NpsRating', :dependent => :destroy
|
7
7
|
|
8
|
-
include ActsAsNpsRateable::
|
8
|
+
include ActsAsNpsRateable::Rateable
|
9
|
+
end
|
10
|
+
|
11
|
+
def acts_as_nps_rater
|
12
|
+
has_many :nps_ratings, :as => :rater, class_name: 'ActsAsNpsRateable::NpsRating', :dependent => :destroy
|
13
|
+
|
14
|
+
include ActsAsNpsRateable::Rater
|
9
15
|
end
|
10
16
|
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
##
|
2
|
+
# acts_as_nps_rateable is a library that provides the following functionality:
|
3
|
+
# * Net Promoter Score recording and analysis functionality.
|
4
|
+
# * Storing comments in addition to a rating
|
5
|
+
# * One response per comment from the entity being reviewed
|
6
|
+
# * Upvoting or downvoting comments (as many times as someone wants)
|
7
|
+
#
|
8
|
+
# These are all the instance methods that <code>acts_as_nps_rateable</code> includes into a model
|
9
|
+
module ActsAsNpsRateable::Rateable
|
10
|
+
##
|
11
|
+
# Rate this rateable ensuring there's at most only one rating per rater per rateable
|
12
|
+
#
|
13
|
+
# @param [Integer] score
|
14
|
+
# This is the actual rating this rateable is getting
|
15
|
+
# @param [ActsAsNpsRateable::Rater] rater
|
16
|
+
# This is the rater who is rating this particular rateable. Nothing happens if this is absent.
|
17
|
+
def rate(score, rater)
|
18
|
+
return unless rater.present?
|
19
|
+
|
20
|
+
ActiveRecord::Base.transaction do
|
21
|
+
ratings_by(rater).delete_all
|
22
|
+
nps_ratings.create(score: score.to_i, rater: rater)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns the average of all ratings for this rateable
|
28
|
+
#
|
29
|
+
# @return [Float] the average of all ratings for this rateable
|
30
|
+
# This uses the database average aggregation
|
31
|
+
def average_rating
|
32
|
+
nps_ratings.average(:score)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Checks whether a rater has already rated this rateable
|
37
|
+
#
|
38
|
+
# @param [ActsAsNpsRateable::Rater] rater
|
39
|
+
# The rater who might have rated this rateable before
|
40
|
+
#
|
41
|
+
# @return [Boolean] if a rater was specified
|
42
|
+
# @return nil if rater is nil
|
43
|
+
def rated_by?(rater)
|
44
|
+
return unless rater.present?
|
45
|
+
|
46
|
+
ratings_by(rater).any?
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Get the rating given to this rateable by a rater
|
51
|
+
#
|
52
|
+
# @param [ActsAsNpsRateable::Rater] rater
|
53
|
+
# The rater who might have rated this rateable before.
|
54
|
+
#
|
55
|
+
# @return [Boolean] if a rater was specified
|
56
|
+
# @return nil if rater is nil
|
57
|
+
def rating_by(rater)
|
58
|
+
return unless rater.present?
|
59
|
+
|
60
|
+
ratings_by(rater).first
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the number of ratings which are classified as promoters. Promoters are any ratings where the score is
|
65
|
+
# greater than 8
|
66
|
+
#
|
67
|
+
# @param [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>] ratings
|
68
|
+
# These are optional ratings - typically a subset of the whole - which you want to calculate the promoter count from
|
69
|
+
# @return [Integer] the number of promoter ratings among all the ratings for this rateable if ratings is nil
|
70
|
+
# @return [Integer] the number of promoter ratings among the given ratings if ratings is not nil
|
71
|
+
def promoters ratings = nil
|
72
|
+
if ratings.nil?
|
73
|
+
nps_ratings.promoters.size
|
74
|
+
else
|
75
|
+
ratings.promoters.size
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Returns the number of ratings which are classified as passives. Passives are any ratings where the score is
|
81
|
+
# either 7 or 8
|
82
|
+
#
|
83
|
+
# @param [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>] ratings
|
84
|
+
# These are optional ratings - typically a subset of the whole - which you want to calculate the passive count from
|
85
|
+
# @return [Integer] the number of passive ratings among all the ratings for this rateable if ratings is nil
|
86
|
+
# @return [Integer] the number of passive ratings among the given ratings if ratings is not nil
|
87
|
+
def passives ratings = nil
|
88
|
+
if ratings.nil?
|
89
|
+
nps_ratings.passives.size
|
90
|
+
else
|
91
|
+
ratings.passives.size
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Returns the number of ratings which are classified as detractors. Passives are any ratings where the score is
|
97
|
+
# less than 7
|
98
|
+
#
|
99
|
+
# @param [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>] ratings
|
100
|
+
# These are optional ratings - typically a subset of the whole - which you want to calculate the detractor count from
|
101
|
+
# @return [Integer] the number of detractor ratings among all the ratings for this rateable if ratings is nil
|
102
|
+
# @return [Integer] the number of detractor ratings among the given ratings if ratings is not nil
|
103
|
+
def detractors ratings = nil
|
104
|
+
if ratings.nil?
|
105
|
+
nps_ratings.detractors.size
|
106
|
+
else
|
107
|
+
ratings.detractors.size
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Calculates the Net Promoter Score as defined at http://www.checkmarket.com/2011/06/net-promoter-score/
|
113
|
+
#
|
114
|
+
# @param [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>] ratings
|
115
|
+
# These are optional ratings - typically a subset of the whole - which you want to calculate the Net Promoter Score
|
116
|
+
# from
|
117
|
+
# @return [Integer] the Net Promoter Score based on all the ratings for this rateable if ratings is nil
|
118
|
+
# @return [Integer] the Net Promoter Score based on the given ratings if ratings is not nil
|
119
|
+
def net_promoter_score ratings = nil
|
120
|
+
total_ratings = ratings.nil? ? nps_ratings.size : ratings.size
|
121
|
+
return 0 if total_ratings.zero?
|
122
|
+
|
123
|
+
if ratings.nil?
|
124
|
+
(promoters - detractors) * 100 / total_ratings
|
125
|
+
else
|
126
|
+
(promoters(ratings) - detractors(ratings)) * 100 / total_ratings
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Add an optional review to this rateable only if this rater has previously rated this rateable.
|
132
|
+
#
|
133
|
+
# @param [String] comment
|
134
|
+
# The actual comment
|
135
|
+
# @param [ActsAsNpsRateable::Rater] rater
|
136
|
+
# This is the rater who is rating this particular rateable. Nothing happens if this is absent.
|
137
|
+
def review(comment, rater)
|
138
|
+
return unless rater.present?
|
139
|
+
|
140
|
+
ratings_by(rater).update_all(comments: comment)
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
##
|
146
|
+
# A convenience method to DRY up the logic of fetching all ratings for this rateable by a given rater
|
147
|
+
#
|
148
|
+
# @param [ActsAsNpsRateable::Rater] rater
|
149
|
+
# This is the rater for whom you want ratings retrieved
|
150
|
+
# @return [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>]
|
151
|
+
# the list of ratings by the rater for this rateable
|
152
|
+
def ratings_by(rater)
|
153
|
+
nps_ratings.where(rater: rater)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# These are all the instance methods that acts_as_nps_rater includes into an ActiveModel
|
2
|
+
|
3
|
+
module ActsAsNpsRateable::Rater
|
4
|
+
##
|
5
|
+
# As this rater, rate the specified rateable ensuring there's at most only one rating per rater per rateable
|
6
|
+
#
|
7
|
+
# @param [ActsAsNpsRateable::Rateable] rateable
|
8
|
+
# This is the rateable which this rater is rating. Nothing happens if this is absent.
|
9
|
+
# @param [Integer] score
|
10
|
+
# This is the actual rating this rater is giving
|
11
|
+
def rate(rateable, score)
|
12
|
+
ActiveRecord::Base.transaction do
|
13
|
+
ratings_for(rateable).delete_all
|
14
|
+
nps_ratings.create(nps_rateable: rateable, score: score.to_i)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Returns the average of all ratings this rater has ever given
|
20
|
+
#
|
21
|
+
# @return [Float] the average of all ratings this rater has given
|
22
|
+
# This uses the database average aggregation
|
23
|
+
def average_rating
|
24
|
+
nps_ratings.average(:score)
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Checks whether this rater has already rated a rateable
|
29
|
+
#
|
30
|
+
# @param [ActsAsNpsRateable::Rateable] rateable
|
31
|
+
# The rateable that this rater might have rated before
|
32
|
+
#
|
33
|
+
# @return [Boolean] if a rateable was specified
|
34
|
+
# @return nil if rater is nil
|
35
|
+
def rated?(rateable)
|
36
|
+
return unless rateable.present?
|
37
|
+
|
38
|
+
ratings_for(rateable).any?
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Get the rating this rater has given to a rateable
|
43
|
+
#
|
44
|
+
# @param [ActsAsNpsRateable::Rateable] rateable
|
45
|
+
# The rateable that this rater might have rated before.
|
46
|
+
#
|
47
|
+
# @return [Boolean] if a rateable was specified
|
48
|
+
# @return nil if rater is nil
|
49
|
+
def rating_for(rateable)
|
50
|
+
return unless rateable.present?
|
51
|
+
|
52
|
+
ratings_for(rateable).first
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Add an optional review by this rater to a rateable only if this rater has previously rated that rateable
|
57
|
+
#
|
58
|
+
# @param [String] comment
|
59
|
+
# The actual comment
|
60
|
+
# @param [ActsAsNpsRateable::Rateable] rateable
|
61
|
+
# This is the rateable for which this rater is adding a review. Nothing happens if this is absent.
|
62
|
+
def review(comment, rateable)
|
63
|
+
return unless rateable.present?
|
64
|
+
|
65
|
+
ratings_for(rateable).update_all(comments: comment)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
##
|
71
|
+
# A convenience method to DRY up the logic of fetching all ratings by this rater for a given rateable
|
72
|
+
#
|
73
|
+
# @param [ActsAsNpsRateable::Rateable] rateable
|
74
|
+
# This is the rateable for which you want ratings retrieved
|
75
|
+
# @return [ActiveRecord::Relation<ActsAsNpsRateable::NpsRating>]
|
76
|
+
# the list of ratings for the rateable by this rater
|
77
|
+
def ratings_for(rateable)
|
78
|
+
nps_ratings.where(nps_rateable: rateable)
|
79
|
+
end
|
80
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rails/generators/migration'
|
2
2
|
|
3
3
|
module ActsAsNpsRateable
|
4
|
+
# @private
|
4
5
|
class InstallGenerator < Rails::Generators::Base
|
5
6
|
include Rails::Generators::Migration
|
6
7
|
|
@@ -10,13 +11,27 @@ module ActsAsNpsRateable
|
|
10
11
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
14
|
+
def create_nps_rateable_migrations
|
15
|
+
migration_mapping = {
|
16
|
+
'01-migration' => 'acts_as_nps_rateable_migration',
|
17
|
+
'02-migration' => 'acts_as_nps_rateable_migration_upgrade_0_0_2',
|
18
|
+
'03-migration' => 'acts_as_nps_rateable_migration_upgrade_0_0_5',
|
19
|
+
'04-migration' => 'acts_as_nps_rateable_migration_usefulness_upgrade'
|
20
|
+
}
|
21
|
+
|
22
|
+
migration_mapping.keys.sort.each do |src|
|
23
|
+
tgt = migration_mapping[src]
|
24
|
+
|
25
|
+
if self.class.migration_exists? 'db/migrate', tgt
|
26
|
+
puts "Not over-writing existing migration: #{tgt}"
|
27
|
+
else
|
28
|
+
migration_template "#{src}.rb", "db/migrate/#{tgt}.rb"
|
29
|
+
sleep 1
|
30
|
+
end
|
31
|
+
end
|
16
32
|
end
|
17
33
|
|
18
34
|
def install_models
|
19
|
-
#template 'nps_rating.rb', 'app/models/nps_rating.rb'
|
20
35
|
end
|
21
36
|
end
|
22
37
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# @private
|
2
|
+
class ActsAsNpsRateableMigrationUpgrade005 < ActiveRecord::Migration
|
3
|
+
def change
|
4
|
+
remove_index :nps_ratings, column: [:nps_rateable_type, :nps_rateable_id, :user_id], unique: true, name: "acts_as_nps_rateable_unique_index"
|
5
|
+
|
6
|
+
rename_column :nps_ratings, :user_id, :rater_id
|
7
|
+
add_column :nps_ratings, :rater_type, :string, default: 'User'
|
8
|
+
|
9
|
+
add_index :nps_ratings, [:nps_rateable_type, :nps_rateable_id, :rater_type, :rater_id], unique: true, name: "acts_as_nps_rateable_unique_index"
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# @private
|
2
|
+
class ActsAsNpsRateableMigrationUsefulnessUpgrade < ActiveRecord::Migration
|
3
|
+
def change
|
4
|
+
add_column :nps_ratings, :up_votes, :integer, default: 0, null: false
|
5
|
+
add_column :nps_ratings, :down_votes, :integer, default: 0, null: false
|
6
|
+
add_column :nps_ratings, :net_votes, :integer, default: 0, null: false
|
7
|
+
|
8
|
+
add_column :nps_ratings, :response, :text
|
9
|
+
add_column :nps_ratings, :responded_at, :timestamp
|
10
|
+
end
|
11
|
+
end
|
data/lib/nps_rating.rb
CHANGED
@@ -1,30 +1,47 @@
|
|
1
|
+
##
|
2
|
+
# acts_as_nps_rateable is a library that provides the following functionality:
|
3
|
+
# * Net Promoter Score recording and analysis functionality.
|
4
|
+
# * Storing comments in addition to a rating
|
5
|
+
# * One response per comment from the entity being reviewed
|
6
|
+
# * Upvoting or downvoting comments (as many times as someone wants)
|
1
7
|
module ActsAsNpsRateable
|
8
|
+
##
|
9
|
+
# This is the actual model that stores a rating, a review (if any), its response (if any) and a count of upvotes and
|
10
|
+
# downvotes
|
2
11
|
class NpsRating < ActiveRecord::Base
|
3
12
|
self.table_name = 'nps_ratings'
|
4
13
|
|
5
14
|
belongs_to :nps_rateable, polymorphic: true
|
6
|
-
belongs_to :
|
15
|
+
belongs_to :rater, polymorphic: true
|
7
16
|
|
8
17
|
validates_presence_of :score
|
9
18
|
validates_numericality_of :score, only_integer: true
|
10
19
|
validates_inclusion_of :score, in: (0..10).to_a, message: 'must be between 0 and 10'
|
11
20
|
|
12
|
-
validates_presence_of :
|
13
|
-
validates_presence_of :
|
21
|
+
validates_presence_of :nps_rateable
|
22
|
+
validates_presence_of :rater
|
23
|
+
validates_uniqueness_of :rater_id, scope: [:rater_type, :nps_rateable_type, :nps_rateable_id], message: 'has already rated'
|
14
24
|
|
15
|
-
|
16
|
-
|
25
|
+
scope :promoters, lambda { where(score: [9, 10]) }
|
26
|
+
scope :passives, lambda { where(score: [7, 8]) }
|
27
|
+
scope :detractors, lambda { where(NpsRating.arel_table[:score].lteq(6)) }
|
17
28
|
|
18
|
-
|
19
|
-
|
20
|
-
scope :promoters, where(score: [9, 10])
|
21
|
-
scope :passives, where(score: [7, 8])
|
22
|
-
scope :detractors, where('score <= 6')
|
23
|
-
scope :with_comments, where('comments IS NOT NULL')
|
29
|
+
scope :with_comments, lambda { where.not(comments: nil) }
|
24
30
|
|
31
|
+
##
|
32
|
+
# Returns the Net Promoter Score for the given collection of ratings.
|
33
|
+
#
|
34
|
+
# @param [ActiveRecord::Relation<NpsRating>] relevant_ratings
|
35
|
+
# This is essentially the result of an ActiveRecord query for nps_ratings
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# NpsRating.calculate_for(fancy_product.nps_ratings)
|
39
|
+
#
|
40
|
+
# @return [Float] The calculated Net Promoter Score.
|
41
|
+
# This is 0 if <code>relevant_ratings.size</code> is 0
|
25
42
|
def self.calculate_for relevant_ratings
|
26
43
|
total_ratings = relevant_ratings.size
|
27
|
-
return 0 if total_ratings
|
44
|
+
return 0 if total_ratings.zero?
|
28
45
|
|
29
46
|
promoters = relevant_ratings.promoters.size
|
30
47
|
detractors = relevant_ratings.detractors.size
|
@@ -32,10 +49,33 @@ module ActsAsNpsRateable
|
|
32
49
|
((promoters - detractors) * 100.0 / total_ratings).round
|
33
50
|
end
|
34
51
|
|
35
|
-
|
52
|
+
##
|
53
|
+
# @!attribute [w] comments
|
54
|
+
# Ensures that any comments that are recorded are non-blank
|
55
|
+
def comments= comments
|
56
|
+
write_attribute(:comments, comments.present? ? comments : nil)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# @!attribute [w] response
|
61
|
+
# Ensures that when a non-blank response is recorded, the responded_at time is also updated to the current time
|
62
|
+
def response= response
|
63
|
+
if response.present?
|
64
|
+
write_attribute(:response, response)
|
65
|
+
write_attribute(:responded_at, Time.now)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Upvotes a rating atomically. Duplicate votes are possible.
|
71
|
+
def upvote!
|
72
|
+
ActsAsNpsRateable::NpsRating.update_counters(id, up_votes: 1, net_votes: 1)
|
73
|
+
end
|
36
74
|
|
37
|
-
|
38
|
-
|
75
|
+
##
|
76
|
+
# Downvotes a rating atomically. Duplicate votes are possible.
|
77
|
+
def downvote!
|
78
|
+
ActsAsNpsRateable::NpsRating.update_counters(id, down_votes: 1, net_votes: -1)
|
39
79
|
end
|
40
80
|
end
|
41
81
|
end
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_nps_rateable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shahbaz Javeed
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 4.2.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 4.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rails
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 4.2.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 4.2.0
|
41
41
|
description: Rails gem that provides Net Promoter Score (NPS) ratings and analysis
|
42
42
|
for ActiveRecord models. It can be used as a regular 0 to 10 rating scale and you
|
43
43
|
can just ignore the NPS analysis methods.
|
@@ -47,9 +47,9 @@ executables: []
|
|
47
47
|
extensions: []
|
48
48
|
extra_rdoc_files: []
|
49
49
|
files:
|
50
|
-
- .gitignore
|
51
|
-
- .ruby-gemset
|
52
|
-
- .ruby-version
|
50
|
+
- ".gitignore"
|
51
|
+
- ".ruby-gemset"
|
52
|
+
- ".ruby-version"
|
53
53
|
- Gemfile
|
54
54
|
- LICENSE.txt
|
55
55
|
- README.md
|
@@ -57,12 +57,15 @@ files:
|
|
57
57
|
- acts_as_nps_rateable.gemspec
|
58
58
|
- lib/acts_as_nps_rateable.rb
|
59
59
|
- lib/acts_as_nps_rateable/hook.rb
|
60
|
-
- lib/acts_as_nps_rateable/instance_methods.rb
|
61
60
|
- lib/acts_as_nps_rateable/railtie.rb
|
61
|
+
- lib/acts_as_nps_rateable/rateable.rb
|
62
|
+
- lib/acts_as_nps_rateable/rater.rb
|
62
63
|
- lib/acts_as_nps_rateable/version.rb
|
63
64
|
- lib/generators/acts_as_nps_rateable/install_generator.rb
|
64
65
|
- lib/generators/acts_as_nps_rateable/templates/01-migration.rb
|
65
66
|
- lib/generators/acts_as_nps_rateable/templates/02-migration.rb
|
67
|
+
- lib/generators/acts_as_nps_rateable/templates/03-migration.rb
|
68
|
+
- lib/generators/acts_as_nps_rateable/templates/04-migration.rb
|
66
69
|
- lib/nps_rating.rb
|
67
70
|
homepage: https://github.com/sjaveed/acts_as_nps_rateable
|
68
71
|
licenses: []
|
@@ -73,18 +76,19 @@ require_paths:
|
|
73
76
|
- lib
|
74
77
|
required_ruby_version: !ruby/object:Gem::Requirement
|
75
78
|
requirements:
|
76
|
-
- -
|
79
|
+
- - ">="
|
77
80
|
- !ruby/object:Gem::Version
|
78
81
|
version: '0'
|
79
82
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
83
|
requirements:
|
81
|
-
- -
|
84
|
+
- - ">="
|
82
85
|
- !ruby/object:Gem::Version
|
83
86
|
version: '0'
|
84
87
|
requirements: []
|
85
88
|
rubyforge_project:
|
86
|
-
rubygems_version: 2.
|
89
|
+
rubygems_version: 2.4.6
|
87
90
|
signing_key:
|
88
91
|
specification_version: 4
|
89
92
|
summary: Net Promoter Score ratings and analysis for ActiveRecord models
|
90
93
|
test_files: []
|
94
|
+
has_rdoc:
|
@@ -1,63 +0,0 @@
|
|
1
|
-
# These are all the instance methods that acts_as_nps_rateable includes into an ActiveModel
|
2
|
-
|
3
|
-
module ActsAsNpsRateable::InstanceMethods
|
4
|
-
# rate this rateable ensuring there's at most only one rating per user per rateable
|
5
|
-
def rate(score, user)
|
6
|
-
ActiveRecord::Base.transaction do
|
7
|
-
nps_ratings.where(user_id: user.id).delete_all if user.present?
|
8
|
-
nps_ratings.create(score: score.to_i, user_id: user.id)
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
# the average of all ratings for this rateable
|
13
|
-
def average_rating
|
14
|
-
nps_ratings.average(:score)
|
15
|
-
end
|
16
|
-
|
17
|
-
# a check to see if the specified user has already rated this rateable
|
18
|
-
def rated_by?(user)
|
19
|
-
return unless user.present?
|
20
|
-
|
21
|
-
nps_ratings.where(user_id: user.id).size > 0
|
22
|
-
end
|
23
|
-
|
24
|
-
#
|
25
|
-
# Net Promoter Score Calculations: http://www.checkmarket.com/2011/06/net-promoter-score/
|
26
|
-
#
|
27
|
-
|
28
|
-
def promoters ratings = nil
|
29
|
-
if ratings.nil?
|
30
|
-
nps_ratings.promoters.size
|
31
|
-
else
|
32
|
-
ratings.promoters.size
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def passives ratings = nil
|
37
|
-
if ratings.nil?
|
38
|
-
nps_ratings.passives.size
|
39
|
-
else
|
40
|
-
ratings.passives.size
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def detractors ratings = nil
|
45
|
-
if ratings.nil?
|
46
|
-
nps_ratings.detractors.size
|
47
|
-
else
|
48
|
-
ratings.detractors.size
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def net_promoter_score ratings = nil
|
53
|
-
total_ratings = ratings.nil? ? nps_ratings.size : ratings.size
|
54
|
-
return 0 if total_ratings == 0
|
55
|
-
|
56
|
-
if ratings.nil?
|
57
|
-
(promoters - detractors) * 100 / total_ratings
|
58
|
-
else
|
59
|
-
(promoters(ratings) - detractors(ratings)) * 100 / total_ratings
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|