interest 0.0.1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +329 -0
  7. data/Rakefile +2 -0
  8. data/interest.gemspec +29 -0
  9. data/lib/generators/interest_generator.rb +18 -0
  10. data/lib/generators/templates/migrations/blockings.rb +14 -0
  11. data/lib/generators/templates/migrations/followings.rb +15 -0
  12. data/lib/generators/templates/models/blocking.rb +3 -0
  13. data/lib/generators/templates/models/following.rb +4 -0
  14. data/lib/interest.rb +49 -0
  15. data/lib/interest/base.rb +49 -0
  16. data/lib/interest/blockable.rb +25 -0
  17. data/lib/interest/blockable/blockee.rb +40 -0
  18. data/lib/interest/blockable/blocker.rb +69 -0
  19. data/lib/interest/blockable/blocking.rb +48 -0
  20. data/lib/interest/blockable/exceptions.rb +12 -0
  21. data/lib/interest/definition.rb +45 -0
  22. data/lib/interest/exception.rb +10 -0
  23. data/lib/interest/follow_requestable.rb +25 -0
  24. data/lib/interest/follow_requestable/exceptions.rb +12 -0
  25. data/lib/interest/follow_requestable/follow_request.rb +45 -0
  26. data/lib/interest/follow_requestable/follow_requestee.rb +45 -0
  27. data/lib/interest/follow_requestable/follow_requester.rb +92 -0
  28. data/lib/interest/followable.rb +25 -0
  29. data/lib/interest/followable/exceptions.rb +12 -0
  30. data/lib/interest/followable/followee.rb +41 -0
  31. data/lib/interest/followable/follower.rb +68 -0
  32. data/lib/interest/followable/following.rb +62 -0
  33. data/lib/interest/utils.rb +29 -0
  34. data/lib/interest/version.rb +3 -0
  35. data/spec/blockable_spec.rb +127 -0
  36. data/spec/database.yml.example +3 -0
  37. data/spec/follow_requestable_spec.rb +126 -0
  38. data/spec/followable_spec.rb +171 -0
  39. data/spec/models/blocking_spec.rb +42 -0
  40. data/spec/models/following_spec.rb +120 -0
  41. data/spec/spec_helper.rb +103 -0
  42. data/spec/support/database.rb +10 -0
  43. data/spec/support/models/blockable_user.rb +8 -0
  44. data/spec/support/models/blocking.rb +4 -0
  45. data/spec/support/models/collection.rb +6 -0
  46. data/spec/support/models/follow_requestable_and_blockable_user.rb +8 -0
  47. data/spec/support/models/follow_requestable_user.rb +9 -0
  48. data/spec/support/models/followable_and_blockable_user.rb +9 -0
  49. data/spec/support/models/followable_user.rb +8 -0
  50. data/spec/support/models/following.rb +4 -0
  51. data/spec/support/models/stuff.rb +6 -0
  52. data/spec/support/schema.rb +26 -0
  53. metadata +211 -0
@@ -0,0 +1,45 @@
1
+ require "active_support"
2
+
3
+ module Interest
4
+ module FollowRequestable
5
+ module FollowRequest
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ validate :validate_follow_request_relationships, if: :should_validate_follow_request_relationships?
10
+ end
11
+
12
+ def accept
13
+ update status: "accepted"
14
+ end
15
+
16
+ def accept!
17
+ update! status: "accepted"
18
+ end
19
+
20
+ def accept_mutual_follow
21
+ transaction { accept and mutual }
22
+ end
23
+
24
+ def accept_mutual_follow!
25
+ transaction { accept! and mutual! }
26
+ end
27
+
28
+ def reject
29
+ destroy
30
+ end
31
+
32
+ alias_method :reject!, :reject
33
+
34
+ def should_validate_follow_request_relationships?
35
+ pending? and follower.is_a?(ActiveRecord::Base) and followee.is_a?(ActiveRecord::Base)
36
+ end
37
+
38
+ def validate_follow_request_relationships
39
+ errors.add :follower, :invalid unless follower.follow_requester?
40
+ errors.add :followee, :invalid unless followee.follow_requestee?
41
+ errors.add :followee, :rejected if follower.follow_requester? and not follower.valid_follow_request_for?(followee)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ require "active_support"
2
+ require "interest/utils"
3
+ require "interest/definition"
4
+
5
+ module Interest
6
+ module FollowRequestable
7
+ module FollowRequestee
8
+ extend ActiveSupport::Concern
9
+
10
+ include Interest::Definition.instance_methods_for(:follow_requestee, :follow_requester)
11
+
12
+ def requires_request_to_follow?(requester)
13
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
14
+ end
15
+
16
+ def has_been_requested_to_follow?(requester)
17
+ follow_requestee_collection_for(requester).include? requester
18
+ end
19
+
20
+ module ClassMethods
21
+ include Interest::Definition.class_methods_for(:follow_requestee, :follow_requester)
22
+
23
+ def define_follow_requestee_association_methods
24
+ has_many :incoming_follow_requests,
25
+ -> { where(followings: {status: "pending"}).uniq },
26
+ as: :followee,
27
+ dependent: :destroy,
28
+ class_name: "Following" do
29
+ include Interest::Definition.collection_methods_for(:follower)
30
+ end
31
+ end
32
+
33
+ def define_follow_requestee_association_method(source_type)
34
+ association_method_name = follow_requestee_association_method_name_for source_type
35
+
36
+ has_many association_method_name,
37
+ -> { uniq },
38
+ through: :incoming_follow_requests,
39
+ source: :follower,
40
+ source_type: source_type
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,92 @@
1
+ require "active_support"
2
+ require "active_record"
3
+ require "interest/utils"
4
+ require "interest/definition"
5
+ require "interest/follow_requestable/exceptions"
6
+ require "interest/follow_requestable/follow_requestee"
7
+
8
+ module Interest
9
+ module FollowRequestable
10
+ module FollowRequester
11
+ extend ActiveSupport::Concern
12
+
13
+ include Interest::Definition.instance_methods_for(:follow_requester, :follow_requestee)
14
+
15
+ def required_request_to_follow?(requestee)
16
+ requestee.follow_requestee? and requestee.requires_request_to_follow?(self)
17
+ end
18
+
19
+ def request_to_follow(requestee, raise_record_invalid = false)
20
+ outgoing_follow_requests.create!(followee: requestee)
21
+ rescue ActiveRecord::RecordInvalid => exception
22
+ raise_record_invalid ? (raise exception) : nil
23
+ rescue ActiveRecord::RecordNotUnique
24
+ outgoing_follow_requests.find_by(followee: requestee)
25
+ end
26
+
27
+ def request_to_follow!(requestee)
28
+ request_to_follow(requestee, true)
29
+ rescue ActiveRecord::RecordInvalid => exception
30
+ raise Interest::FollowRequestable::Rejected.new(exception)
31
+ end
32
+
33
+ def cancel_request_to_follow(requestee)
34
+ follow_requester_collection_for(requestee).delete requestee
35
+ end
36
+
37
+ def has_requested_to_follow?(requestee)
38
+ follow_requester_collection_for(requestee).include? requestee
39
+ end
40
+
41
+ def valid_follow_request_for?(requestee)
42
+ not (self == requestee or not follow_requestable?(requestee) or (follower? and following? requestee))
43
+ end
44
+
45
+ def follow_or_request_to_follow!(other)
46
+ if required_request_to_follow? other
47
+ returned = request_to_follow! other
48
+ which = :request_to_follow
49
+ else
50
+ returned = follow! other
51
+ which = :follow
52
+ end
53
+
54
+ FollowOrRequestToFollow.new which, returned, other
55
+ end
56
+
57
+ class FollowOrRequestToFollow < Struct.new(:which, :returned, :target)
58
+ def followed?
59
+ which == :follow
60
+ end
61
+
62
+ def requested_to_follow?
63
+ which == :request_to_follow
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+ include Interest::Definition.class_methods_for(:follow_requester, :follow_requestee)
69
+
70
+ def define_follow_requester_association_methods
71
+ has_many :outgoing_follow_requests,
72
+ -> { where(followings: {status: "pending"}).uniq },
73
+ as: :follower,
74
+ dependent: :destroy,
75
+ class_name: "Following" do
76
+ include Interest::Definition.collection_methods_for(:followee)
77
+ end
78
+ end
79
+
80
+ def define_follow_requester_association_method(source_type)
81
+ association_method_name = follow_requester_association_method_name_for source_type
82
+
83
+ has_many association_method_name,
84
+ -> { uniq },
85
+ through: :outgoing_follow_requests,
86
+ source: :followee,
87
+ source_type: source_type
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ require "interest/followable/exceptions"
2
+ require "interest/followable/following"
3
+ require "interest/followable/follower"
4
+ require "interest/followable/followee"
5
+
6
+ module Interest
7
+ module Followable
8
+ module Extension
9
+ def acts_as_follower
10
+ include Follower
11
+ define_follower_association_methods
12
+ end
13
+
14
+ def acts_as_followee
15
+ include Followee
16
+ define_followee_association_methods
17
+ end
18
+
19
+ def acts_as_followable
20
+ acts_as_follower
21
+ acts_as_followee
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ require "interest/exception"
2
+
3
+ module Interest
4
+ module Followable
5
+ module Exceptions
6
+ class Exception < Interest::Exception; end
7
+ class Rejected < Exception; end
8
+ end
9
+
10
+ include Exceptions
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require "active_support"
2
+ require "interest/utils"
3
+ require "interest/definition"
4
+
5
+ module Interest
6
+ module Followable
7
+ module Followee
8
+ extend ActiveSupport::Concern
9
+
10
+ include Interest::Definition.instance_methods_for(:followee, :follower)
11
+
12
+ def followed_by?(follower)
13
+ followee_collection_for(follower).include? follower
14
+ end
15
+
16
+ module ClassMethods
17
+ include Interest::Definition.class_methods_for(:followee, :follower)
18
+
19
+ def define_followee_association_methods
20
+ has_many :follower_relationships,
21
+ -> { where(Interest.following_class.table_name.to_sym => {status: "accepted"}).uniq },
22
+ as: :followee,
23
+ dependent: :destroy,
24
+ class_name: Interest.following_class_name do
25
+ include Interest::Definition.collection_methods_for(:follower)
26
+ end
27
+ end
28
+
29
+ def define_followee_association_method(source_type)
30
+ association_method_name = followee_association_method_name_for source_type
31
+
32
+ has_many association_method_name,
33
+ -> { uniq },
34
+ through: :follower_relationships,
35
+ source: :follower,
36
+ source_type: source_type
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ require "active_support"
2
+ require "active_record"
3
+ require "interest/utils"
4
+ require "interest/definition"
5
+ require "interest/followable/exceptions"
6
+ require "interest/followable/followee"
7
+
8
+ module Interest
9
+ module Followable
10
+ module Follower
11
+ extend ActiveSupport::Concern
12
+
13
+ include Interest::Definition.instance_methods_for(:follower, :following)
14
+
15
+ def following?(followee)
16
+ follower_collection_for(followee).include? followee
17
+ end
18
+
19
+ def follow(followee, raise_record_invalid = false)
20
+ following_relationships.create!(followee: followee)
21
+ rescue ActiveRecord::RecordInvalid => exception
22
+ raise_record_invalid ? (raise exception) : nil
23
+ rescue ActiveRecord::RecordNotUnique
24
+ if follow_requester? and followee.follow_requestee?
25
+ outgoing_follow_requests.find_by(followee: followee).try(:accept!)
26
+ end or following_relationships.find_by(followee: followee)
27
+ end
28
+
29
+ def follow!(followee)
30
+ follow(followee, true)
31
+ rescue ActiveRecord::RecordInvalid => exception
32
+ raise Interest::Followable::Rejected.new(exception)
33
+ end
34
+
35
+ def unfollow(followee)
36
+ follower_collection_for(followee).delete followee
37
+ end
38
+
39
+ def valid_following_for?(followee)
40
+ not (followee == self or not followable?(followee))
41
+ end
42
+
43
+ module ClassMethods
44
+ include Interest::Definition.class_methods_for(:follower, :following)
45
+
46
+ def define_follower_association_methods
47
+ has_many :following_relationships,
48
+ -> { where(Interest.following_class.table_name.to_sym => {status: "accepted"}).uniq },
49
+ as: :follower,
50
+ dependent: :destroy,
51
+ class_name: Interest.following_class_name do
52
+ include Interest::Definition.collection_methods_for(:followee)
53
+ end
54
+ end
55
+
56
+ def define_follower_association_method(source_type)
57
+ association_method_name = follower_association_method_name_for source_type
58
+
59
+ has_many association_method_name,
60
+ -> { uniq },
61
+ through: :following_relationships,
62
+ source: :followee,
63
+ source_type: source_type
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,62 @@
1
+ require "active_support"
2
+
3
+ module Interest
4
+ module Followable
5
+ module Following
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ belongs_to :follower, polymorphic: true
10
+ belongs_to :followee, polymorphic: true
11
+
12
+ validates :follower, presence: true
13
+ validates :followee, presence: true
14
+ validates :status, presence: true, inclusion: {in: %w(pending accepted)}
15
+
16
+ validate :validate_following_relationships, if: :should_validate_following_relationships?
17
+
18
+ scope :accepted, -> { where(status: "accepted") }
19
+ scope :pending, -> { where(status: "pending") }
20
+
21
+ scope :between, ->(a, b) {
22
+ a_to_b = where(follower: a, followee: b).where_values.reduce(:and)
23
+ b_to_a = where(follower: b, followee: a).where_values.reduce(:and)
24
+
25
+ where a_to_b.or(b_to_a)
26
+ }
27
+ end
28
+
29
+ def accepted?
30
+ status == "accepted"
31
+ end
32
+
33
+ def pending?
34
+ status == "pending"
35
+ end
36
+
37
+ def mutual
38
+ followee.follow(follower)
39
+ end
40
+
41
+ def mutual!
42
+ followee.follow!(follower)
43
+ end
44
+
45
+ def should_validate_following_relationships?
46
+ accepted? and follower.is_a?(ActiveRecord::Base) and followee.is_a?(ActiveRecord::Base)
47
+ end
48
+
49
+ def validate_following_relationships
50
+ errors.add :follower, :invalid unless follower.follower?
51
+ errors.add :followee, :invalid unless followee.followee?
52
+ errors.add :followee, :rejected if follower.follower? and not follower.valid_following_for?(followee)
53
+ end
54
+
55
+ module ClassMethods
56
+ def destroy_relationships_between(a, b)
57
+ between(a, b).destroy_all
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,29 @@
1
+ require "active_support"
2
+ require "active_support/core_ext/string/inflections"
3
+ require "active_record"
4
+
5
+ module Interest
6
+ module Utils
7
+ class << self
8
+ def symbolic_name_of(object)
9
+ if object.is_a?(ActiveRecord::Base)
10
+ object.class.name
11
+ elsif object.is_a?(Class) and object < ActiveRecord::Base
12
+ object.name
13
+ else
14
+ object.to_s
15
+ end.underscore.pluralize
16
+ end
17
+
18
+ def source_type_of(object)
19
+ if object.is_a?(ActiveRecord::Base)
20
+ object.class.name
21
+ elsif object.is_a?(Class) and object < ActiveRecord::Base
22
+ object.name
23
+ else
24
+ object.to_s.classify
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Interest
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,127 @@
1
+ require "spec_helper"
2
+
3
+ describe "Blockable" do
4
+ it "should block other" do
5
+ user = BlockableUser.create!
6
+ other_user = BlockableUser.create!
7
+
8
+ user.block(other_user)
9
+
10
+ expect(user).to be_blocking(other_user)
11
+ end
12
+
13
+ it "should not block self" do
14
+ user = BlockableUser.create!
15
+
16
+ user.block(user)
17
+
18
+ expect(user).not_to be_blocking(user)
19
+ end
20
+
21
+ it "should not duplicate blockee" do
22
+ user = BlockableUser.create!
23
+ other_user = BlockableUser.create!
24
+
25
+ user.block(other_user)
26
+ user.block(other_user)
27
+
28
+ expect(user.blocking_relationships.count).to eq(1)
29
+ expect(user.blocking_blockable_users.count).to eq(1)
30
+ end
31
+
32
+ it "should be a Blocking" do
33
+ user = BlockableUser.create!
34
+ other_user = BlockableUser.create!
35
+
36
+ user.block(other_user)
37
+
38
+ expect(user.blocking_relationships.of(BlockableUser)).to be_present.and all be_a Blocking
39
+ end
40
+
41
+ it "should be a model is blocked" do
42
+ user = BlockableUser.create!
43
+ other_user = BlockableUser.create!
44
+ collection = Collection.create!
45
+
46
+ user.block(other_user)
47
+ user.block(collection)
48
+
49
+ expect(user.blocking_blockable_users).to be_present.and all be_a BlockableUser
50
+ end
51
+
52
+ it "should not block self and raise exception" do
53
+ user = BlockableUser.create!
54
+
55
+ expect { user.block!(user) }.to raise_error(Interest::Blockable::Rejected)
56
+ end
57
+
58
+ it "should be followed by other user if user is not blocking one" do
59
+ user = FollowableAndBlockableUser.create!
60
+ other_user = FollowableAndBlockableUser.create!
61
+
62
+ expect { other_user.follow!(user) }.not_to raise_error
63
+ end
64
+
65
+ it "should not be followed by other user if user is blocking one" do
66
+ user = FollowableAndBlockableUser.create!
67
+ other_user = FollowableAndBlockableUser.create!
68
+
69
+ user.block(other_user)
70
+
71
+ expect { other_user.follow!(user) }.to raise_error(Interest::Followable::Rejected)
72
+ end
73
+
74
+ it "should destroy their followship when either follower or followee blocks other" do
75
+ user = FollowableAndBlockableUser.create!
76
+ other_user = FollowableAndBlockableUser.create!
77
+
78
+ user.follow(other_user)
79
+ other_user.follow(user)
80
+
81
+ user.block(other_user)
82
+
83
+ expect(user).not_to be_following(other_user)
84
+ expect(other_user).not_to be_following(user)
85
+ end
86
+
87
+ it "should destroy their follow requests when either requester or requestee blocks other" do
88
+ user = FollowRequestableAndBlockableUser.create!
89
+ other_user = FollowRequestableAndBlockableUser.create!
90
+
91
+ user.request_to_follow(other_user)
92
+ other_user.request_to_follow(user)
93
+
94
+ user.block(other_user)
95
+
96
+ expect(user).not_to have_requested_to_follow(other_user)
97
+ expect(other_user).not_to have_been_requested_to_follow(user)
98
+ end
99
+
100
+ it "should find outgoing blocking of specified type" do
101
+ user = BlockableUser.create!
102
+ other_user1 = BlockableUser.create!
103
+ other_user2 = BlockableUser.create!
104
+ collection = Collection.create!
105
+
106
+ user.block(other_user1)
107
+ user.block(other_user2)
108
+ user.block(collection)
109
+
110
+ expect(user.blocking_relationships.of(BlockableUser)).to have_exactly(2).items
111
+ expect(user.blocking_relationships.of(Collection)).to have_exactly(1).items
112
+ end
113
+
114
+ it "should find incoming blocking of specified type" do
115
+ user = BlockableUser.create!
116
+ other_user1 = BlockableUser.create!
117
+ other_user2 = BlockableUser.create!
118
+ collection = Collection.create!
119
+
120
+ other_user1.block(user)
121
+ other_user2.block(user)
122
+ collection.block(user)
123
+
124
+ expect(user.blocker_relationships.of(BlockableUser)).to have_exactly(2).items
125
+ expect(user.blocker_relationships.of(Collection)).to have_exactly(1).items
126
+ end
127
+ end