voting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +229 -0
  5. data/lib/generators/voting/install_generator.rb +19 -0
  6. data/lib/generators/voting/templates/db/migrate/create_voting_tables.rb +35 -0
  7. data/lib/voting.rb +10 -0
  8. data/lib/voting/models/voting/extension.rb +85 -0
  9. data/lib/voting/models/voting/vote.rb +45 -0
  10. data/lib/voting/models/voting/voting.rb +73 -0
  11. data/lib/voting/version.rb +5 -0
  12. data/spec/factories/article.rb +5 -0
  13. data/spec/factories/author.rb +5 -0
  14. data/spec/factories/category.rb +5 -0
  15. data/spec/factories/comment.rb +5 -0
  16. data/spec/factories/voting/vote.rb +8 -0
  17. data/spec/factories/voting/voting.rb +9 -0
  18. data/spec/models/extension/after_save_spec.rb +37 -0
  19. data/spec/models/extension/as_spec.rb +26 -0
  20. data/spec/models/extension/down_spec.rb +26 -0
  21. data/spec/models/extension/order_by_voting_spec.rb +94 -0
  22. data/spec/models/extension/up_spec.rb +26 -0
  23. data/spec/models/extension/vote_for_spec.rb +26 -0
  24. data/spec/models/extension/vote_spec.rb +26 -0
  25. data/spec/models/extension/voted_question_spec.rb +38 -0
  26. data/spec/models/extension/voted_records_spec.rb +12 -0
  27. data/spec/models/extension/voted_spec.rb +40 -0
  28. data/spec/models/extension/votes_records_spec.rb +12 -0
  29. data/spec/models/extension/votes_spec.rb +40 -0
  30. data/spec/models/extension/voting_records_spec.rb +12 -0
  31. data/spec/models/extension/voting_spec.rb +40 -0
  32. data/spec/models/extension/voting_warm_up_spec.rb +115 -0
  33. data/spec/models/vote/create_spec.rb +273 -0
  34. data/spec/models/vote/vote_for_spec.rb +40 -0
  35. data/spec/models/vote_spec.rb +27 -0
  36. data/spec/models/voting/update_voting_spec.rb +28 -0
  37. data/spec/models/voting/values_data_spec.rb +24 -0
  38. data/spec/models/voting_spec.rb +26 -0
  39. data/spec/rails_helper.rb +11 -0
  40. data/spec/support/common.rb +22 -0
  41. data/spec/support/database_cleaner.rb +19 -0
  42. data/spec/support/db/migrate/create_articles_table.rb +7 -0
  43. data/spec/support/db/migrate/create_authors_table.rb +7 -0
  44. data/spec/support/db/migrate/create_categories_table.rb +9 -0
  45. data/spec/support/db/migrate/create_comments_table.rb +7 -0
  46. data/spec/support/factory_bot.rb +9 -0
  47. data/spec/support/migrate.rb +11 -0
  48. data/spec/support/models/article.rb +7 -0
  49. data/spec/support/models/author.rb +5 -0
  50. data/spec/support/models/category.rb +5 -0
  51. data/spec/support/models/comment.rb +5 -0
  52. data/spec/support/shared_context/with_database_records.rb +22 -0
  53. data/spec/support/shoulda.rb +10 -0
  54. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Voting
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :article
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :author
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :category
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :comment
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :voting_vote, class: Voting::Vote do
5
+ author { create :author }
6
+ resource { create :comment }
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :voting_voting, class: Voting::Voting do
5
+ estimate 100
6
+
7
+ association :resource, factory: :comment, strategy: :build
8
+ end
9
+ 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