interest 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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