voting 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +21 -0
- data/README.md +229 -0
- data/lib/generators/voting/install_generator.rb +19 -0
- data/lib/generators/voting/templates/db/migrate/create_voting_tables.rb +35 -0
- data/lib/voting.rb +10 -0
- data/lib/voting/models/voting/extension.rb +85 -0
- data/lib/voting/models/voting/vote.rb +45 -0
- data/lib/voting/models/voting/voting.rb +73 -0
- data/lib/voting/version.rb +5 -0
- data/spec/factories/article.rb +5 -0
- data/spec/factories/author.rb +5 -0
- data/spec/factories/category.rb +5 -0
- data/spec/factories/comment.rb +5 -0
- data/spec/factories/voting/vote.rb +8 -0
- data/spec/factories/voting/voting.rb +9 -0
- data/spec/models/extension/after_save_spec.rb +37 -0
- data/spec/models/extension/as_spec.rb +26 -0
- data/spec/models/extension/down_spec.rb +26 -0
- data/spec/models/extension/order_by_voting_spec.rb +94 -0
- data/spec/models/extension/up_spec.rb +26 -0
- data/spec/models/extension/vote_for_spec.rb +26 -0
- data/spec/models/extension/vote_spec.rb +26 -0
- data/spec/models/extension/voted_question_spec.rb +38 -0
- data/spec/models/extension/voted_records_spec.rb +12 -0
- data/spec/models/extension/voted_spec.rb +40 -0
- data/spec/models/extension/votes_records_spec.rb +12 -0
- data/spec/models/extension/votes_spec.rb +40 -0
- data/spec/models/extension/voting_records_spec.rb +12 -0
- data/spec/models/extension/voting_spec.rb +40 -0
- data/spec/models/extension/voting_warm_up_spec.rb +115 -0
- data/spec/models/vote/create_spec.rb +273 -0
- data/spec/models/vote/vote_for_spec.rb +40 -0
- data/spec/models/vote_spec.rb +27 -0
- data/spec/models/voting/update_voting_spec.rb +28 -0
- data/spec/models/voting/values_data_spec.rb +24 -0
- data/spec/models/voting_spec.rb +26 -0
- data/spec/rails_helper.rb +11 -0
- data/spec/support/common.rb +22 -0
- data/spec/support/database_cleaner.rb +19 -0
- data/spec/support/db/migrate/create_articles_table.rb +7 -0
- data/spec/support/db/migrate/create_authors_table.rb +7 -0
- data/spec/support/db/migrate/create_categories_table.rb +9 -0
- data/spec/support/db/migrate/create_comments_table.rb +7 -0
- data/spec/support/factory_bot.rb +9 -0
- data/spec/support/migrate.rb +11 -0
- data/spec/support/models/article.rb +7 -0
- data/spec/support/models/author.rb +5 -0
- data/spec/support/models/category.rb +5 -0
- data/spec/support/models/comment.rb +5 -0
- data/spec/support/shared_context/with_database_records.rb +22 -0
- data/spec/support/shoulda.rb +10 -0
- metadata +257 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Voting
|
4
|
+
class Voting < ActiveRecord::Base
|
5
|
+
self.table_name = 'voting_votings'
|
6
|
+
|
7
|
+
belongs_to :resource, polymorphic: true
|
8
|
+
belongs_to :scopeable, polymorphic: true
|
9
|
+
|
10
|
+
validates :estimate, :negative, :positive, :resource, presence: true
|
11
|
+
validates :estimate, numericality: true
|
12
|
+
validates :negative, :positive, numericality: { greater_than_or_equal_to: 0, only_integer: true }
|
13
|
+
|
14
|
+
validates :resource_id, uniqueness: {
|
15
|
+
case_sensitive: false,
|
16
|
+
scope: %i[resource_type scopeable_id scopeable_type]
|
17
|
+
}
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def values_data(resource, scopeable)
|
21
|
+
sql = %(
|
22
|
+
SELECT
|
23
|
+
COALESCE(SUM(negative), 0) voting_negative,
|
24
|
+
COALESCE(SUM(positive), 0) voting_positive
|
25
|
+
FROM #{vote_table_name}
|
26
|
+
WHERE resource_type = ? and resource_id = ? and #{scope_query(scopeable)}
|
27
|
+
).squish
|
28
|
+
|
29
|
+
values = [sql, resource.class.name, resource.id]
|
30
|
+
values += [scopeable.class.name, scopeable.id] unless scopeable.nil?
|
31
|
+
|
32
|
+
execute_sql values
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_voting(resource, scopeable)
|
36
|
+
record = find_or_initialize_by(resource: resource, scopeable: scopeable)
|
37
|
+
values = values_data(resource, scopeable)
|
38
|
+
|
39
|
+
record.estimate = estimate(values)
|
40
|
+
record.negative = values.voting_negative
|
41
|
+
record.positive = values.voting_positive
|
42
|
+
|
43
|
+
record.save!
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def estimate(values)
|
49
|
+
sum = values.voting_negative + values.voting_positive
|
50
|
+
|
51
|
+
return 0 if sum.zero?
|
52
|
+
|
53
|
+
square = Math.sqrt((values.voting_negative * values.voting_positive) / sum + 0.9604)
|
54
|
+
|
55
|
+
((values.voting_positive + 1.9208) / sum - 1.96 * square / sum) / (1 + 3.8416 / sum)
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute_sql(sql)
|
59
|
+
Vote.find_by_sql(sql).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def vote_table_name
|
63
|
+
@vote_table_name ||= Vote.table_name
|
64
|
+
end
|
65
|
+
|
66
|
+
def scope_query(scopeable)
|
67
|
+
return 'scopeable_type is NULL and scopeable_id is NULL' if scopeable.nil?
|
68
|
+
|
69
|
+
'scopeable_type = ? and scopeable_id = ?'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, 'after_save' do
|
6
|
+
context 'when record is author' do
|
7
|
+
let!(:record) { build :author }
|
8
|
+
|
9
|
+
it 'does warm up the cache' do
|
10
|
+
expect(record).not_to receive(:voting_warm_up)
|
11
|
+
|
12
|
+
record.save
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when record is not author' do
|
17
|
+
context 'when record has scoping' do
|
18
|
+
let!(:record) { build :article }
|
19
|
+
|
20
|
+
it 'warms up the cache' do
|
21
|
+
expect(record).to receive(:voting_warm_up).with(scoping: :categories)
|
22
|
+
|
23
|
+
record.save
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when record has no scoping' do
|
28
|
+
let!(:record) { build :comment }
|
29
|
+
|
30
|
+
it 'warms up the cache' do
|
31
|
+
expect(record).to receive(:voting_warm_up).with(scoping: nil)
|
32
|
+
|
33
|
+
record.save
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, ':as' do
|
6
|
+
context 'when is nil' do
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
it 'creates a voting record with values as zero to warm up the cache' do
|
10
|
+
voting = Voting::Voting.find_by(resource: comment)
|
11
|
+
|
12
|
+
expect(voting.negative).to eq 0
|
13
|
+
expect(voting.positive).to eq 0
|
14
|
+
expect(voting.resource).to eq comment
|
15
|
+
expect(voting.scopeable).to eq nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when is :author' do
|
20
|
+
let!(:author) { create :author }
|
21
|
+
|
22
|
+
it 'does not creates a voting record' do
|
23
|
+
expect(Voting::Voting.exists?(resource: author)).to eq false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, '.down' do
|
6
|
+
let!(:author) { create :author }
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
context 'with no scopeable' do
|
10
|
+
it 'delegates to create method' do
|
11
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: nil, value: -1
|
12
|
+
|
13
|
+
author.down comment
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with scopeable' do
|
18
|
+
let!(:category) { build :category }
|
19
|
+
|
20
|
+
it 'delegates to create method' do
|
21
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: category, value: -1
|
22
|
+
|
23
|
+
author.down comment, scope: category
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
require 'support/shared_context/with_database_records'
|
5
|
+
|
6
|
+
RSpec.describe Voting::Extension, ':order_by_voting' do
|
7
|
+
include_context 'with_database_records'
|
8
|
+
|
9
|
+
context 'with default filters' do
|
10
|
+
it 'sorts by :estimate :desc' do
|
11
|
+
expect(Comment.order_by_voting).to eq [
|
12
|
+
comment_1,
|
13
|
+
comment_2,
|
14
|
+
comment_3
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when filtering by :estimate' do
|
20
|
+
context 'with asc' do
|
21
|
+
it 'works' do
|
22
|
+
expect(Comment.order_by_voting(:estimate, :asc)).to eq [
|
23
|
+
comment_3,
|
24
|
+
comment_2,
|
25
|
+
comment_1
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with scope' do
|
30
|
+
it 'works' do
|
31
|
+
expect(Comment.order_by_voting(:estimate, :asc, scope: category)).to eq [
|
32
|
+
comment_1
|
33
|
+
]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with desc' do
|
39
|
+
it 'works' do
|
40
|
+
expect(Comment.order_by_voting(:estimate, :desc)).to eq [
|
41
|
+
comment_1,
|
42
|
+
comment_2,
|
43
|
+
comment_3
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with scope' do
|
48
|
+
it 'works' do
|
49
|
+
expect(Comment.order_by_voting(:estimate, :desc, scope: category)).to eq [
|
50
|
+
comment_1
|
51
|
+
]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when filtering by :negative' do
|
58
|
+
context 'with asc' do
|
59
|
+
it 'works' do
|
60
|
+
expect(Comment.order_by_voting(:negative, :asc)).to eq [
|
61
|
+
comment_3,
|
62
|
+
comment_1,
|
63
|
+
comment_2
|
64
|
+
]
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with scope' do
|
68
|
+
it 'works' do
|
69
|
+
expect(Comment.order_by_voting(:negative, :asc, scope: category)).to eq [
|
70
|
+
comment_1
|
71
|
+
]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with desc' do
|
77
|
+
it 'works' do
|
78
|
+
expect(Comment.order_by_voting(:negative, :desc)).to eq [
|
79
|
+
comment_2,
|
80
|
+
comment_1,
|
81
|
+
comment_3
|
82
|
+
]
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with scope' do
|
86
|
+
it 'works' do
|
87
|
+
expect(Comment.order_by_voting(:negative, :desc, scope: category)).to eq [
|
88
|
+
comment_1
|
89
|
+
]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, '.up' do
|
6
|
+
let!(:author) { create :author }
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
context 'with no scopeable' do
|
10
|
+
it 'delegates to create method' do
|
11
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: nil, value: 1
|
12
|
+
|
13
|
+
author.up comment
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with scopeable' do
|
18
|
+
let!(:category) { build :category }
|
19
|
+
|
20
|
+
it 'delegates to create method' do
|
21
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: category, value: 1
|
22
|
+
|
23
|
+
author.up comment, scope: category
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, ':vote_for' do
|
6
|
+
let!(:author) { create :author }
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
context 'with no scopeable' do
|
10
|
+
it 'delegates to vote object' do
|
11
|
+
expect(Voting::Vote).to receive(:vote_for).with author: author, resource: comment, scopeable: nil
|
12
|
+
|
13
|
+
author.vote_for comment
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with scopeable' do
|
18
|
+
let!(:category) { build :category }
|
19
|
+
|
20
|
+
it 'delegates to vote object' do
|
21
|
+
expect(Voting::Vote).to receive(:vote_for).with author: author, resource: comment, scopeable: category
|
22
|
+
|
23
|
+
author.vote_for comment, scope: category
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, ':vote' do
|
6
|
+
let!(:author) { create :author }
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
context 'with no scopeable' do
|
10
|
+
it 'delegates to vote object' do
|
11
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: nil, value: 1
|
12
|
+
|
13
|
+
author.vote comment, 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with scopeable' do
|
18
|
+
let!(:category) { build :category }
|
19
|
+
|
20
|
+
it 'delegates to vote object' do
|
21
|
+
expect(Voting::Vote).to receive(:create).with author: author, resource: comment, scopeable: category, value: 1
|
22
|
+
|
23
|
+
author.vote comment, 1, scope: category
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe Voting::Extension, ':voted?' do
|
6
|
+
let!(:author) { create :author }
|
7
|
+
let!(:comment) { create :comment }
|
8
|
+
|
9
|
+
context 'with no scopeable' do
|
10
|
+
context 'when has no vote for the given resource' do
|
11
|
+
before { allow(author).to receive(:vote_for).with(comment, scope: nil) { nil } }
|
12
|
+
|
13
|
+
specify { expect(author.voted?(comment)).to eq false }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when has vote for the given resource' do
|
17
|
+
before { allow(author).to receive(:vote_for).with(comment, scope: nil) { double } }
|
18
|
+
|
19
|
+
specify { expect(author.voted?(comment)).to eq true }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with scopeable' do
|
24
|
+
let!(:category) { build :category }
|
25
|
+
|
26
|
+
context 'when has no vote for the given resource' do
|
27
|
+
before { allow(author).to receive(:vote_for).with(comment, scope: category) { nil } }
|
28
|
+
|
29
|
+
specify { expect(author.voted?(comment, scope: category)).to eq false }
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when has vote for the given resource' do
|
33
|
+
before { allow(author).to receive(:vote_for).with(comment, scope: category) { double } }
|
34
|
+
|
35
|
+
specify { expect(author.voted?(comment, scope: category)).to eq true }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
require 'support/shared_context/with_database_records'
|
5
|
+
|
6
|
+
RSpec.describe Voting::Extension, '.voted_records' do
|
7
|
+
include_context 'with_database_records'
|
8
|
+
|
9
|
+
it 'returns all votes that this author gave' do
|
10
|
+
expect(author_1.voted_records).to match_array [vote_1, vote_4, vote_7]
|
11
|
+
end
|
12
|
+
end
|