thumbsy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +299 -0
- data/Rakefile +24 -0
- data/config/database.yml +47 -0
- data/lib/generators/thumbsy/api_generator.rb +59 -0
- data/lib/generators/thumbsy/install_generator.rb +53 -0
- data/lib/generators/thumbsy/templates/README +56 -0
- data/lib/generators/thumbsy/templates/create_thumbsy_votes.rb +20 -0
- data/lib/generators/thumbsy/templates/thumbsy.rb +24 -0
- data/lib/generators/thumbsy/templates/thumbsy_api.rb +75 -0
- data/lib/thumbsy/api/configuration.rb +19 -0
- data/lib/thumbsy/api/controllers/application_controller.rb +70 -0
- data/lib/thumbsy/api/controllers/votes_controller.rb +135 -0
- data/lib/thumbsy/api/engine.rb +14 -0
- data/lib/thumbsy/api/routes.rb +26 -0
- data/lib/thumbsy/api/serializers/vote_serializer.rb +36 -0
- data/lib/thumbsy/api.rb +57 -0
- data/lib/thumbsy/configuration.rb +24 -0
- data/lib/thumbsy/engine.rb +20 -0
- data/lib/thumbsy/extension.rb +13 -0
- data/lib/thumbsy/models/thumbsy_vote.rb +75 -0
- data/lib/thumbsy/validators/array_inclusion_validator.rb +20 -0
- data/lib/thumbsy/version.rb +5 -0
- data/lib/thumbsy/votable.rb +82 -0
- data/lib/thumbsy/voter.rb +65 -0
- data/lib/thumbsy.rb +58 -0
- metadata +125 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :require_authentication, :authentication_method, :current_voter_method, :require_authorization,
|
|
7
|
+
:authorization_method, :voter_serializer
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@require_authentication = true
|
|
11
|
+
@require_authorization = false
|
|
12
|
+
@authentication_method = nil
|
|
13
|
+
@current_voter_method = nil
|
|
14
|
+
@authorization_method = nil
|
|
15
|
+
@voter_serializer = nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
class ApplicationController < ActionController::API
|
|
6
|
+
before_action :authenticate_voter!, if: :authentication_required?
|
|
7
|
+
before_action :set_current_voter
|
|
8
|
+
|
|
9
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def authenticate_voter!
|
|
14
|
+
if Thumbsy::Api.authentication_method
|
|
15
|
+
instance_eval(&Thumbsy::Api.authentication_method)
|
|
16
|
+
else
|
|
17
|
+
head :unauthorized unless current_voter.present?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_current_voter
|
|
22
|
+
if Thumbsy::Api.current_voter_method
|
|
23
|
+
@current_voter = instance_eval(&Thumbsy::Api.current_voter_method)
|
|
24
|
+
elsif respond_to?(:current_user)
|
|
25
|
+
@current_voter = current_user
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :current_voter
|
|
30
|
+
|
|
31
|
+
def authentication_required?
|
|
32
|
+
Thumbsy::Api.require_authentication
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_success(data = {}, status = :ok)
|
|
36
|
+
render json: { data: data }, status: status
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_error(message, status = :bad_request, errors = {})
|
|
40
|
+
# Convert errors to array format
|
|
41
|
+
error_array = if errors.is_a?(Hash) && errors.any?
|
|
42
|
+
errors.values.flatten
|
|
43
|
+
else
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
render json: { error: message, errors: error_array }, status: status
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_not_found(_exception)
|
|
50
|
+
render_error("Resource not found", :not_found)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def render_unauthorized(message = "Authentication required")
|
|
54
|
+
render_error(message, :unauthorized)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_forbidden(message = "Access denied")
|
|
58
|
+
render_error(message, :forbidden)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_unprocessable_entity(message = "Validation failed", errors = {})
|
|
62
|
+
render_error(message, :unprocessable_entity, errors)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_bad_request(message = "Bad request")
|
|
66
|
+
render_error(message, :bad_request)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
class VotesController < Thumbsy::Api::ApplicationController
|
|
6
|
+
before_action :find_votable
|
|
7
|
+
before_action :check_votable_permissions, if: :authorization_required?
|
|
8
|
+
|
|
9
|
+
# POST /votes/vote_up
|
|
10
|
+
def vote_up
|
|
11
|
+
vote = @votable.vote_up(current_voter, comment: vote_params[:comment],
|
|
12
|
+
feedback_options: vote_params[:feedback_options])
|
|
13
|
+
|
|
14
|
+
if vote&.persisted? && vote.errors.empty?
|
|
15
|
+
render_success(Thumbsy::Api::Serializers::VoteSerializer.new(vote).as_json, :created)
|
|
16
|
+
else
|
|
17
|
+
render_unprocessable_entity("Failed to create vote", vote&.errors&.full_messages || [])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# POST /votes/vote_down
|
|
22
|
+
def vote_down
|
|
23
|
+
vote = @votable.vote_down(current_voter, comment: vote_params[:comment],
|
|
24
|
+
feedback_options: vote_params[:feedback_options])
|
|
25
|
+
|
|
26
|
+
if vote&.persisted? && vote.errors.empty?
|
|
27
|
+
render_success(Thumbsy::Api::Serializers::VoteSerializer.new(vote).as_json, :created)
|
|
28
|
+
else
|
|
29
|
+
render_unprocessable_entity("Failed to create vote", vote&.errors&.full_messages || [])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DELETE /votes/remove
|
|
34
|
+
def remove
|
|
35
|
+
removed = @votable.remove_vote(current_voter)
|
|
36
|
+
|
|
37
|
+
if removed
|
|
38
|
+
render_success({ message: "Vote removed" })
|
|
39
|
+
else
|
|
40
|
+
render_not_found(nil)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# GET /votes/status
|
|
45
|
+
def status
|
|
46
|
+
vote = @votable.vote_by(current_voter)
|
|
47
|
+
|
|
48
|
+
data = {
|
|
49
|
+
voted: @votable.voted_by?(current_voter),
|
|
50
|
+
vote_type: if vote
|
|
51
|
+
vote.up_vote? ? "up" : "down"
|
|
52
|
+
end,
|
|
53
|
+
comment: vote&.comment,
|
|
54
|
+
vote_counts: {
|
|
55
|
+
total: @votable.votes_count,
|
|
56
|
+
up: @votable.up_votes_count,
|
|
57
|
+
down: @votable.down_votes_count,
|
|
58
|
+
score: @votable.votes_score,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render_success(data)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# GET /votes/vote
|
|
66
|
+
def show
|
|
67
|
+
vote = @votable.vote_by(current_voter)
|
|
68
|
+
if vote
|
|
69
|
+
render_success(Thumbsy::Api::Serializers::VoteSerializer.new(vote).as_json)
|
|
70
|
+
else
|
|
71
|
+
render_not_found(nil)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# GET /votes
|
|
76
|
+
def index
|
|
77
|
+
votes = filtered_votes
|
|
78
|
+
data = {
|
|
79
|
+
votes: votes.map { |vote| Thumbsy::Api::Serializers::VoteSerializer.new(vote).as_json },
|
|
80
|
+
summary: vote_summary,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
render_success(data)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def filtered_votes
|
|
89
|
+
votes = @votable.thumbsy_votes.includes(:voter)
|
|
90
|
+
votes = votes.with_comments if params[:with_comments] == "true"
|
|
91
|
+
votes = votes.up_votes if params[:vote_type] == "up"
|
|
92
|
+
votes = votes.down_votes if params[:vote_type] == "down"
|
|
93
|
+
votes
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def vote_summary
|
|
97
|
+
{
|
|
98
|
+
total: @votable.votes_count,
|
|
99
|
+
up: @votable.up_votes_count,
|
|
100
|
+
down: @votable.down_votes_count,
|
|
101
|
+
score: @votable.votes_score,
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def find_votable
|
|
106
|
+
votable_class = params[:votable_type].constantize
|
|
107
|
+
@votable = votable_class.find(params[:votable_id])
|
|
108
|
+
rescue NameError
|
|
109
|
+
render_bad_request("Invalid votable type")
|
|
110
|
+
rescue ActiveRecord::RecordNotFound
|
|
111
|
+
render_not_found(nil)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def check_votable_permissions
|
|
115
|
+
return unless Thumbsy::Api.authorization_method
|
|
116
|
+
|
|
117
|
+
authorized = instance_exec(@votable, current_voter, &Thumbsy::Api.authorization_method)
|
|
118
|
+
render_forbidden unless authorized
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def authorization_required?
|
|
122
|
+
Thumbsy::Api.require_authorization
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def vote_params
|
|
126
|
+
permitted = params.permit(:comment, { feedback_options: [] }, :feedback_option, :votable_type, :votable_id)
|
|
127
|
+
# Normalize feedback_option (string) to feedback_options (array)
|
|
128
|
+
if permitted[:feedback_options].blank? && permitted[:feedback_option].present?
|
|
129
|
+
permitted[:feedback_options] = [permitted.delete(:feedback_option)]
|
|
130
|
+
end
|
|
131
|
+
permitted
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Thumbsy::Api
|
|
7
|
+
|
|
8
|
+
# Load API routes from routes.rb file
|
|
9
|
+
config.after_initialize do
|
|
10
|
+
require "thumbsy/api/routes"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Thumbsy::Api::Engine.routes.draw do
|
|
4
|
+
# Flexible routes that can be mounted anywhere
|
|
5
|
+
scope ":votable_type/:votable_id" do
|
|
6
|
+
post "votes/vote_up", to: "votes#vote_up"
|
|
7
|
+
post "votes/vote_down", to: "votes#vote_down"
|
|
8
|
+
delete "votes/remove", to: "votes#remove"
|
|
9
|
+
get "votes/status", to: "votes#status"
|
|
10
|
+
get "votes/vote", to: "votes#show"
|
|
11
|
+
get "votes", to: "votes#index"
|
|
12
|
+
|
|
13
|
+
# Alternative shorter routes
|
|
14
|
+
post "vote_up", to: "votes#vote_up"
|
|
15
|
+
post "vote_down", to: "votes#vote_down"
|
|
16
|
+
delete "vote", to: "votes#remove"
|
|
17
|
+
get "vote", to: "votes#show"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Bulk operations (optional)
|
|
21
|
+
resources :votes, only: %i[index show] do
|
|
22
|
+
collection do
|
|
23
|
+
get :summary
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
module Serializers
|
|
6
|
+
class VoteSerializer
|
|
7
|
+
def initialize(vote)
|
|
8
|
+
@vote = vote
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def as_json
|
|
12
|
+
{
|
|
13
|
+
id: @vote.id,
|
|
14
|
+
vote_type: @vote.up_vote? ? "up" : "down",
|
|
15
|
+
comment: @vote.comment,
|
|
16
|
+
feedback_options: @vote.feedback_options || [],
|
|
17
|
+
voter: voter_data(@vote.voter),
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def voter_data(voter)
|
|
24
|
+
if Thumbsy::Api.voter_serializer
|
|
25
|
+
instance_exec(voter, &Thumbsy::Api.voter_serializer)
|
|
26
|
+
else
|
|
27
|
+
{
|
|
28
|
+
id: voter.id,
|
|
29
|
+
type: voter.class.name,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/thumbsy/api.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Api
|
|
5
|
+
def self.require_authentication
|
|
6
|
+
Thumbsy.api_config.require_authentication
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.authentication_method
|
|
10
|
+
Thumbsy.api_config.authentication_method
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.current_voter_method
|
|
14
|
+
Thumbsy.api_config.current_voter_method
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.require_authorization
|
|
18
|
+
Thumbsy.api_config.require_authorization
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.require_authorization=(value)
|
|
22
|
+
Thumbsy.api_config.require_authorization = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.authorization_method
|
|
26
|
+
Thumbsy.api_config.authorization_method
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.authorization_method=(value)
|
|
30
|
+
Thumbsy.api_config.authorization_method = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.voter_serializer
|
|
34
|
+
Thumbsy.api_config.voter_serializer
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.voter_serializer=(value)
|
|
38
|
+
Thumbsy.api_config.voter_serializer = value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.configure
|
|
42
|
+
yield(Thumbsy.api_config)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Load API components
|
|
46
|
+
def self.load!
|
|
47
|
+
require "thumbsy/api/engine"
|
|
48
|
+
require "thumbsy/api/controllers/application_controller"
|
|
49
|
+
require "thumbsy/api/controllers/votes_controller"
|
|
50
|
+
require "thumbsy/api/serializers/vote_serializer"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Custom exceptions
|
|
54
|
+
class AuthenticationError < StandardError; end
|
|
55
|
+
class AuthorizationError < StandardError; end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_config
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@api_config = Thumbsy::Api::Configuration.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def api
|
|
12
|
+
yield @api_config if block_given?
|
|
13
|
+
@api_config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def feedback_options=(options)
|
|
17
|
+
@feedback_options = options
|
|
18
|
+
# Automatically set up validation if ThumbsyVote is loaded
|
|
19
|
+
ThumbsyVote.setup_feedback_options_validation! if defined?(ThumbsyVote)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_reader :feedback_options
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
# Only define Rails engine when Rails is available
|
|
5
|
+
if defined?(Rails)
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Thumbsy
|
|
8
|
+
|
|
9
|
+
config.generators do |g|
|
|
10
|
+
g.test_framework :rspec
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer "thumbsy.active_record" do
|
|
14
|
+
ActiveSupport.on_load(:active_record) do
|
|
15
|
+
extend Thumbsy::Extension
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require_relative "../validators/array_inclusion_validator"
|
|
5
|
+
|
|
6
|
+
class ThumbsyVote < ActiveRecord::Base
|
|
7
|
+
serialize :feedback_options
|
|
8
|
+
|
|
9
|
+
after_initialize :set_default_feedback_options
|
|
10
|
+
|
|
11
|
+
def feedback_options=(value)
|
|
12
|
+
super(value.nil? ? [] : value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Dynamically (re)setup feedback_options validation
|
|
16
|
+
def self.setup_feedback_options_validation!
|
|
17
|
+
return unless Thumbsy.feedback_options.present?
|
|
18
|
+
|
|
19
|
+
_validators.delete(:feedback_options)
|
|
20
|
+
validates :feedback_options, array_inclusion: { in: Thumbsy.feedback_options }, allow_nil: false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
belongs_to :votable, polymorphic: true
|
|
24
|
+
belongs_to :voter, polymorphic: true
|
|
25
|
+
|
|
26
|
+
validates :votable, presence: true
|
|
27
|
+
validates :voter, presence: true
|
|
28
|
+
validates :vote, inclusion: { in: [true, false] }
|
|
29
|
+
validates :voter_id, uniqueness: { scope: %i[voter_type votable_type votable_id] }
|
|
30
|
+
|
|
31
|
+
scope :up_votes, -> { where(vote: true) }
|
|
32
|
+
scope :down_votes, -> { where(vote: false) }
|
|
33
|
+
scope :with_comments, -> { where.not(comment: [nil, ""]) }
|
|
34
|
+
|
|
35
|
+
def up_vote?
|
|
36
|
+
vote == true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def down_vote?
|
|
40
|
+
vote == false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.vote_for(votable, voter, vote_value, comment: nil, feedback_options: nil)
|
|
44
|
+
raise ArgumentError, "Voter cannot be nil" if voter.nil?
|
|
45
|
+
raise ArgumentError, "Votable cannot be nil" if votable.nil?
|
|
46
|
+
|
|
47
|
+
existing_vote = find_by(
|
|
48
|
+
votable: votable,
|
|
49
|
+
voter: voter,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if existing_vote
|
|
53
|
+
existing_vote.update!(
|
|
54
|
+
vote: vote_value,
|
|
55
|
+
comment: comment,
|
|
56
|
+
feedback_options: feedback_options,
|
|
57
|
+
)
|
|
58
|
+
existing_vote
|
|
59
|
+
else
|
|
60
|
+
create!(
|
|
61
|
+
votable: votable,
|
|
62
|
+
voter: voter,
|
|
63
|
+
vote: vote_value,
|
|
64
|
+
comment: comment,
|
|
65
|
+
feedback_options: feedback_options,
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def set_default_feedback_options
|
|
73
|
+
self.feedback_options = [] if feedback_options.nil?
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
class InvalidFeedbackOptionError < ArgumentError; end
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ActiveModel
|
|
8
|
+
module Validations
|
|
9
|
+
class ArrayInclusionValidator < EachValidator
|
|
10
|
+
def validate_each(record, attribute, value)
|
|
11
|
+
return if value.nil?
|
|
12
|
+
|
|
13
|
+
allowed = options[:in] || []
|
|
14
|
+
return if value.is_a?(Array) && value.all? { |v| allowed.include?(v) }
|
|
15
|
+
|
|
16
|
+
record.errors.add(attribute, "contains invalid feedback option(s)")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Votable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :thumbsy_votes, as: :votable, class_name: "ThumbsyVote", dependent: :destroy
|
|
9
|
+
scope :with_votes, -> { joins(:thumbsy_votes) }
|
|
10
|
+
scope :with_up_votes, -> { joins(:thumbsy_votes).where(thumbsy_votes: { vote: true }) }
|
|
11
|
+
scope :with_down_votes, -> { joins(:thumbsy_votes).where(thumbsy_votes: { vote: false }) }
|
|
12
|
+
scope :with_comments, -> { joins(:thumbsy_votes).where.not(thumbsy_votes: { comment: [nil, ""] }) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def vote_up(voter, comment: nil, feedback_options: nil)
|
|
16
|
+
return false if voter && !voter.respond_to?(:thumbsy_votes)
|
|
17
|
+
|
|
18
|
+
ThumbsyVote.vote_for(self, voter, true, comment: comment, feedback_options: feedback_options)
|
|
19
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
20
|
+
e.record # Return the invalid vote instance with errors
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def vote_down(voter, comment: nil, feedback_options: nil)
|
|
24
|
+
raise ArgumentError, "Voter is invalid" if voter && !voter.respond_to?(:thumbsy_votes)
|
|
25
|
+
|
|
26
|
+
ThumbsyVote.vote_for(self, voter, false, comment: comment, feedback_options: feedback_options)
|
|
27
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
28
|
+
e.record # Return the invalid vote instance with errors
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Naming/PredicateMethod
|
|
32
|
+
def remove_vote(voter)
|
|
33
|
+
destroyed = thumbsy_votes.where(voter: voter).destroy_all
|
|
34
|
+
destroyed.any?
|
|
35
|
+
end
|
|
36
|
+
# rubocop:enable Naming/PredicateMethod
|
|
37
|
+
|
|
38
|
+
def voted_by?(voter)
|
|
39
|
+
thumbsy_votes.exists?(voter: voter)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def up_voted_by?(voter)
|
|
43
|
+
thumbsy_votes.exists?(voter: voter, vote: true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def down_voted_by?(voter)
|
|
47
|
+
thumbsy_votes.exists?(voter: voter, vote: false)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def vote_by(voter)
|
|
51
|
+
thumbsy_votes.find_by(voter: voter)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def votes_count
|
|
55
|
+
thumbsy_votes.count
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def up_votes_count
|
|
59
|
+
thumbsy_votes.where(vote: true).count
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def down_votes_count
|
|
63
|
+
thumbsy_votes.where(vote: false).count
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def votes_score
|
|
67
|
+
up_votes_count - down_votes_count
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def votes_with_comments
|
|
71
|
+
thumbsy_votes.where.not(comment: [nil, ""])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def up_votes_with_comments
|
|
75
|
+
thumbsy_votes.where(vote: true).where.not(comment: [nil, ""])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def down_votes_with_comments
|
|
79
|
+
thumbsy_votes.where(vote: false).where.not(comment: [nil, ""])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thumbsy
|
|
4
|
+
module Voter
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :thumbsy_votes, as: :voter, class_name: "ThumbsyVote", dependent: :destroy
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def vote_up_for(votable, comment: nil)
|
|
12
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
13
|
+
|
|
14
|
+
votable.vote_up(self, comment: comment)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def vote_down_for(votable, comment: nil)
|
|
18
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
19
|
+
|
|
20
|
+
votable.vote_down(self, comment: comment)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def remove_vote_for(votable)
|
|
24
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
25
|
+
|
|
26
|
+
votable.remove_vote(self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def voted_for?(votable)
|
|
30
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
31
|
+
|
|
32
|
+
votable.voted_by?(self)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def up_voted_for?(votable)
|
|
36
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
37
|
+
|
|
38
|
+
votable.up_voted_by?(self)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def down_voted_for?(votable)
|
|
42
|
+
return false unless votable.respond_to?(:thumbsy_votes)
|
|
43
|
+
|
|
44
|
+
votable.down_voted_by?(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def voted_for(votable_class)
|
|
48
|
+
vote_records = thumbsy_votes.where(votable_type: votable_class.name)
|
|
49
|
+
votable_ids = vote_records.pluck(:votable_id)
|
|
50
|
+
votable_class.where(id: votable_ids)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def up_voted_for_class(votable_class)
|
|
54
|
+
vote_records = thumbsy_votes.where(votable_type: votable_class.name, vote: true)
|
|
55
|
+
votable_ids = vote_records.pluck(:votable_id)
|
|
56
|
+
votable_class.where(id: votable_ids)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def down_voted_for_class(votable_class)
|
|
60
|
+
vote_records = thumbsy_votes.where(votable_type: votable_class.name, vote: false)
|
|
61
|
+
votable_ids = vote_records.pluck(:votable_id)
|
|
62
|
+
votable_class.where(id: votable_ids)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|