voting 0.1.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.
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