acts_as_network 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.
- data/.gitignore +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +97 -0
- data/LICENSE +22 -0
- data/README.md +275 -0
- data/Rakefile +10 -0
- data/acts_as_network.gemspec +22 -0
- data/lib/acts_as_network.rb +324 -0
- data/lib/acts_as_network/version.rb +3 -0
- data/test/database.yml +3 -0
- data/test/fixtures/channels.yml +15 -0
- data/test/fixtures/invites.yml +27 -0
- data/test/fixtures/people.yml +29 -0
- data/test/fixtures/people_people.yml +20 -0
- data/test/fixtures/shows.yml +47 -0
- data/test/network_test.rb +323 -0
- data/test/schema.rb +34 -0
- data/test/test_helper.rb +39 -0
- metadata +104 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rvm --create use 1.9.3@acts_as_network
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
acts_as_network (0.1.0)
|
|
5
|
+
rails (~> 3.2.0)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
actionmailer (3.2.1)
|
|
11
|
+
actionpack (= 3.2.1)
|
|
12
|
+
mail (~> 2.4.0)
|
|
13
|
+
actionpack (3.2.1)
|
|
14
|
+
activemodel (= 3.2.1)
|
|
15
|
+
activesupport (= 3.2.1)
|
|
16
|
+
builder (~> 3.0.0)
|
|
17
|
+
erubis (~> 2.7.0)
|
|
18
|
+
journey (~> 1.0.1)
|
|
19
|
+
rack (~> 1.4.0)
|
|
20
|
+
rack-cache (~> 1.1)
|
|
21
|
+
rack-test (~> 0.6.1)
|
|
22
|
+
sprockets (~> 2.1.2)
|
|
23
|
+
activemodel (3.2.1)
|
|
24
|
+
activesupport (= 3.2.1)
|
|
25
|
+
builder (~> 3.0.0)
|
|
26
|
+
activerecord (3.2.1)
|
|
27
|
+
activemodel (= 3.2.1)
|
|
28
|
+
activesupport (= 3.2.1)
|
|
29
|
+
arel (~> 3.0.0)
|
|
30
|
+
tzinfo (~> 0.3.29)
|
|
31
|
+
activeresource (3.2.1)
|
|
32
|
+
activemodel (= 3.2.1)
|
|
33
|
+
activesupport (= 3.2.1)
|
|
34
|
+
activesupport (3.2.1)
|
|
35
|
+
i18n (~> 0.6)
|
|
36
|
+
multi_json (~> 1.0)
|
|
37
|
+
arel (3.0.0)
|
|
38
|
+
builder (3.0.0)
|
|
39
|
+
erubis (2.7.0)
|
|
40
|
+
hike (1.2.1)
|
|
41
|
+
i18n (0.6.0)
|
|
42
|
+
journey (1.0.1)
|
|
43
|
+
json (1.6.5)
|
|
44
|
+
mail (2.4.1)
|
|
45
|
+
i18n (>= 0.4.0)
|
|
46
|
+
mime-types (~> 1.16)
|
|
47
|
+
treetop (~> 1.4.8)
|
|
48
|
+
mime-types (1.17.2)
|
|
49
|
+
minitest (2.11.1)
|
|
50
|
+
multi_json (1.0.4)
|
|
51
|
+
polyglot (0.3.3)
|
|
52
|
+
rack (1.4.1)
|
|
53
|
+
rack-cache (1.1)
|
|
54
|
+
rack (>= 0.4)
|
|
55
|
+
rack-ssl (1.3.2)
|
|
56
|
+
rack
|
|
57
|
+
rack-test (0.6.1)
|
|
58
|
+
rack (>= 1.0)
|
|
59
|
+
rails (3.2.1)
|
|
60
|
+
actionmailer (= 3.2.1)
|
|
61
|
+
actionpack (= 3.2.1)
|
|
62
|
+
activerecord (= 3.2.1)
|
|
63
|
+
activeresource (= 3.2.1)
|
|
64
|
+
activesupport (= 3.2.1)
|
|
65
|
+
bundler (~> 1.0)
|
|
66
|
+
railties (= 3.2.1)
|
|
67
|
+
railties (3.2.1)
|
|
68
|
+
actionpack (= 3.2.1)
|
|
69
|
+
activesupport (= 3.2.1)
|
|
70
|
+
rack-ssl (~> 1.3.2)
|
|
71
|
+
rake (>= 0.8.7)
|
|
72
|
+
rdoc (~> 3.4)
|
|
73
|
+
thor (~> 0.14.6)
|
|
74
|
+
rake (0.9.2.2)
|
|
75
|
+
rdoc (3.12)
|
|
76
|
+
json (~> 1.4)
|
|
77
|
+
sprockets (2.1.2)
|
|
78
|
+
hike (~> 1.2)
|
|
79
|
+
rack (~> 1.0)
|
|
80
|
+
tilt (~> 1.1, != 1.3.0)
|
|
81
|
+
sqlite3 (1.3.5)
|
|
82
|
+
sqlite3-ruby (1.3.3)
|
|
83
|
+
sqlite3 (>= 1.3.3)
|
|
84
|
+
thor (0.14.6)
|
|
85
|
+
tilt (1.3.3)
|
|
86
|
+
treetop (1.4.10)
|
|
87
|
+
polyglot
|
|
88
|
+
polyglot (>= 0.3.1)
|
|
89
|
+
tzinfo (0.3.31)
|
|
90
|
+
|
|
91
|
+
PLATFORMS
|
|
92
|
+
ruby
|
|
93
|
+
|
|
94
|
+
DEPENDENCIES
|
|
95
|
+
acts_as_network!
|
|
96
|
+
minitest
|
|
97
|
+
sqlite3-ruby
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2007 Zetetic LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# acts_as_network
|
|
2
|
+
|
|
3
|
+
This gem is intended to simplify the definition
|
|
4
|
+
and storage of reciprocal relationships between entities using
|
|
5
|
+
`ActiveRecord`, exposing a "network" of two-way connections between
|
|
6
|
+
records. It does this in DRY way using only **a single record**
|
|
7
|
+
in a `has_and_belongs_to_many` join table or `has_many :through`
|
|
8
|
+
join model. Thus, there is no redundancy and you need only one instance of
|
|
9
|
+
an association or join model to represent both directions of the relationship.
|
|
10
|
+
|
|
11
|
+
This is especially useful for social networks where
|
|
12
|
+
a *friend* relationship in one direction implies the reverse
|
|
13
|
+
relationship (when Jack is a friend of Jane, Jane should also
|
|
14
|
+
be a friend of Jack).
|
|
15
|
+
|
|
16
|
+
## History
|
|
17
|
+
|
|
18
|
+
[Zetetic LLC](http://www.zetetic.net) extracted `acts_as_network` from
|
|
19
|
+
[PingMe](http://www.gopingme.com) where it drives the social
|
|
20
|
+
networking features of the site.
|
|
21
|
+
|
|
22
|
+
[ExamTime](http://www.examtime.com) forked the project in February 2012
|
|
23
|
+
to repackage it from a Rails 2 plugin to a Rails 3 gem. Minimal code
|
|
24
|
+
changes have been made. Significant changes were pulled in from
|
|
25
|
+
[Erik Hollensbe's
|
|
26
|
+
fork](https://github.com/erikh/acts_as_network/commits/rails3/lib/zetetic/acts)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Add this line to your application's `Gemfile`:
|
|
31
|
+
|
|
32
|
+
gem 'acts_as_network'
|
|
33
|
+
|
|
34
|
+
And then execute:
|
|
35
|
+
|
|
36
|
+
$ bundle
|
|
37
|
+
|
|
38
|
+
Or install it yourself as:
|
|
39
|
+
|
|
40
|
+
$ gem install acts_as_network
|
|
41
|
+
|
|
42
|
+
## Contributing
|
|
43
|
+
|
|
44
|
+
This fork is maintained on GitHub:
|
|
45
|
+
git@github.com:ExamTime/acts_as_network.git
|
|
46
|
+
|
|
47
|
+
The original project is here:
|
|
48
|
+
http://github.com/sjlombardo/acts_as_network/tree/master
|
|
49
|
+
|
|
50
|
+
1. Fork it
|
|
51
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
52
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
53
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
54
|
+
5. Create new Pull Request
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
The usual way of representing network relationships in a database is
|
|
59
|
+
to use an intermediate, often self-referential, join table (HABTM).
|
|
60
|
+
For example one might define a simple `Person` type
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
create_table :people, :force => true do |t|
|
|
64
|
+
t.column :name, :string
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
and then a join table to store the friendship relation
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
create_table :friends, {:id => false} do |t|
|
|
72
|
+
t.column :person_id, :integer, :null => false
|
|
73
|
+
t.column :person_id_friend, :integer, :null => false # target of the relationship
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Unfortunately this model requires *two* rows in the intermediate table to
|
|
78
|
+
make a relationship bi-directional
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
jane = Person.create(:name => 'Jane')
|
|
82
|
+
jack = Person.create(:name => 'Jack')
|
|
83
|
+
|
|
84
|
+
jane.friends << jack # Jack is Jane's friend
|
|
85
|
+
jane.friends.include?(jack) # => true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Clearly Jack is Jane's friend, yet Jane is *not* Jack's friend
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
jack.friends.include?(jane) # => false
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
unless you need to explicitly define the reverse relation
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
jack.friends << jane
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Of course, this isn't horrible, and can in fact be implemented
|
|
101
|
+
in a fairly DRY way using association callbacks. However, things get
|
|
102
|
+
more complicated when you consider disassociation (what to do when Jane
|
|
103
|
+
doesn't want to be friends with Jack any more), or the very common
|
|
104
|
+
case where you want to express the relationship through a more complicated
|
|
105
|
+
join model via `has_many :through`
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
create_table :invites do |t|
|
|
109
|
+
t.column :person_id, :integer, :null => false # source of the relationship
|
|
110
|
+
t.column :person_id_friend, :integer, :null => false # target of the relationship
|
|
111
|
+
t.column :code, :string # random invitation code
|
|
112
|
+
t.column :message, :text # invitation message
|
|
113
|
+
t.column :is_accepted, :boolean
|
|
114
|
+
t.column :accepted_at, :timestamp # when did they accept?
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
In this case creating a reverse relationship is painful, and depending on
|
|
119
|
+
validations might require the duplication of multiple values, making the
|
|
120
|
+
data model decidedly un-DRY.
|
|
121
|
+
|
|
122
|
+
### Using acts_as_network
|
|
123
|
+
|
|
124
|
+
Acts As Network DRYs things up by representing only a single record
|
|
125
|
+
in a `has_and_belongs_to_many` join table or `has_many :through`
|
|
126
|
+
join model. Thus, you only need one instance of an association or join model to
|
|
127
|
+
represent both directions of the relationship.
|
|
128
|
+
|
|
129
|
+
### With HABTM
|
|
130
|
+
|
|
131
|
+
For a HABTM style relationship, it's as simple as
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
class Person < ActiveRecord::Base
|
|
135
|
+
acts_as_network :friends, :join_table => :friends
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
In this case `acts_as_network` will expose three new properies
|
|
140
|
+
on the Person model
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
me.friends_out # friends where I have originated the friendship
|
|
144
|
+
# (people I consider friends)
|
|
145
|
+
|
|
146
|
+
me.friends_in # friends where they originated the friendship
|
|
147
|
+
# (people who consider me a friend)
|
|
148
|
+
|
|
149
|
+
me.friends # the union of the two sets, that is all people who I consider
|
|
150
|
+
# friends and all those who consider me a friend
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Thus
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
jane = Person.create(:name => 'Jane')
|
|
157
|
+
jack = Person.create(:name => 'Jack')
|
|
158
|
+
|
|
159
|
+
jane.friends_out << jack # Jane adds Jack as a friend
|
|
160
|
+
jane.friends.include?(jack) => true # Jack is Janes friend
|
|
161
|
+
jack.friends.include?(jane) => true # Jane is also Jack's friend!
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### With a join model
|
|
165
|
+
|
|
166
|
+
This may seem more natural when considering a join style with a proper Invite model. In this case
|
|
167
|
+
one person will "invite" another person to be friends.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class Invite < ActiveRecord::Base
|
|
171
|
+
belongs_to :person
|
|
172
|
+
belongs_to :person_target, :class_name => 'Person', :foreign_key => 'person_id_target' # the target of the friend relationship
|
|
173
|
+
validates_presence_of :person, :person_target
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class Person < ActiveRecord::Base
|
|
177
|
+
acts_as_network :friends, :through => :invites, [:conditions => "is_accepted = ?", true]
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
In this case `acts_as_network` implicitly defines five new properties on
|
|
182
|
+
the `Person` model:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
person.invites_out # has_many invites originating from me to others
|
|
186
|
+
person.invites_in # has_many invites orginiating from others to me
|
|
187
|
+
person.friends_out # has_many friends :through outbound accepted invites from me to others
|
|
188
|
+
person.friends_in # has_many friends :through inbound accepted invites from others to me
|
|
189
|
+
person.friends # the union of the two friend sets - all people who I have
|
|
190
|
+
# invited and all the people who have invited me
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Thus
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
jane = Person.create(:name => 'Jane')
|
|
197
|
+
jack = Person.create(:name => 'Jack')
|
|
198
|
+
|
|
199
|
+
# Jane invites Jack to be friends
|
|
200
|
+
invite = Invite.create(:person => jane, :person_target => jack, :message => "let's be friends!")
|
|
201
|
+
|
|
202
|
+
jane.friends.include?(jack) => false # Jack is not yet Jane's friend
|
|
203
|
+
jack.friends.include?(jane) => false # Jane is not yet Jack's friend either
|
|
204
|
+
|
|
205
|
+
invite.is_accepted = true # Now Jack accepts the invite
|
|
206
|
+
invite.save and jane.reload and jack.reload
|
|
207
|
+
|
|
208
|
+
jane.friends.include?(jack) => true # Jack is Jane's friend now
|
|
209
|
+
jack.friends.include?(jane) => true # Jane is also Jack's friend
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
For more details and specific options see `ActsAsNetwork::Network::ClassMethods`
|
|
213
|
+
|
|
214
|
+
The applications of this plugin to social network situations are fairly obvious,
|
|
215
|
+
but it should also be usable in the general case to represent inherant
|
|
216
|
+
bi-directional relationships between entities.
|
|
217
|
+
|
|
218
|
+
#### Migrations
|
|
219
|
+
|
|
220
|
+
This Gem does not attempt to help you write your migrations. For the
|
|
221
|
+
join example above, the changes to the model and the corresponding
|
|
222
|
+
migrations would be:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
class Person < ActiveRecord::Base
|
|
226
|
+
...
|
|
227
|
+
acts_as_network :friends,
|
|
228
|
+
:through => :invites,
|
|
229
|
+
:conditions => [ "is_accepted = ?", true ],
|
|
230
|
+
:association_foreign_key => "person_target_id"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
class CreateInvite < ActiveRecord::Migration
|
|
235
|
+
def change
|
|
236
|
+
create_table :invites do |t|
|
|
237
|
+
t.integer :person_id
|
|
238
|
+
t.integer :person_target_id
|
|
239
|
+
t.text :message
|
|
240
|
+
t.boolean :is_accepted
|
|
241
|
+
t.timestamps
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class CreateFriends < ActiveRecord::Migration
|
|
247
|
+
def change
|
|
248
|
+
create_table :friends do |t|
|
|
249
|
+
t.integer :person_id
|
|
250
|
+
t.integer :person_id_friend
|
|
251
|
+
t.timestamps
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Tests
|
|
258
|
+
|
|
259
|
+
The plugin's unit tests are located in `test` directory under
|
|
260
|
+
`vendor/plugins/acts_as_network`. Run:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
[%] cd vendor/plugins/acts_as_network
|
|
264
|
+
[%] ruby test/network_test.rb
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
This will create a temporary `sqlite3` database, a number of tables,
|
|
268
|
+
fixture data, and run the tests. You can delete the sqlite database
|
|
269
|
+
when you are done.
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
[%] rm acts_as_network.test.db
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The test suite requires `sqlite3`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/acts_as_network/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Zetetic LLC (Stephen Lombardo), David Kennedy"]
|
|
6
|
+
gem.email = ["david.kennedy@examtime.com"]
|
|
7
|
+
gem.description = %q{Simplify the definition and storage of "network" relationships, especially useful for social networks.}
|
|
8
|
+
gem.summary = %q{Simplify social network relationships}
|
|
9
|
+
gem.homepage = "https://github.com/ExamTime/acts_as_network"
|
|
10
|
+
|
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
14
|
+
gem.name = "acts_as_network"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = ActsAsNetwork::VERSION
|
|
17
|
+
|
|
18
|
+
gem.add_development_dependency "minitest"
|
|
19
|
+
gem.add_development_dependency "sqlite3-ruby"
|
|
20
|
+
gem.add_dependency "rails", "~> 3.2.0"
|
|
21
|
+
|
|
22
|
+
end
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
require "acts_as_network/version"
|
|
2
|
+
require "active_record"
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# ActsAsNetwork contains
|
|
6
|
+
# * ActsAsNetwork::Network::ClassMethods - provides the actual acts_as_network ActiveRecord functionality
|
|
7
|
+
# * ActsAsNetwork::UnionCollection - the basis for the "union" capability that allows acts_as_network
|
|
8
|
+
# to expose both inbound and outbound relationships in a single collection
|
|
9
|
+
#
|
|
10
|
+
module ActsAsNetwork
|
|
11
|
+
# = UnionCollection
|
|
12
|
+
# UnionCollection provides useful application-space functionality
|
|
13
|
+
# for emulating set unions acrosss ActiveRecord collections.
|
|
14
|
+
#
|
|
15
|
+
# A UnionCollection can be initialized with zero or more sets,
|
|
16
|
+
# although generally it must contain at least two to do anything
|
|
17
|
+
# useful. Once initialized, the UnionCollection itself will
|
|
18
|
+
# act as an array containing all of the records from each of its
|
|
19
|
+
# member sets. The following will create a union object containing
|
|
20
|
+
# the unique results of each individual find
|
|
21
|
+
#
|
|
22
|
+
# union = ActsAsNetwork::UnionCollection.new(
|
|
23
|
+
# Person.find(:all, :conditions => "id <= 1"), # set 0
|
|
24
|
+
# Person.find(:all, :conditions => "id >= 10 AND id <= 15"), # set 1
|
|
25
|
+
# Person.find(:all, :conditions => "id >= 20") # set 2
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# UnionCollection's more interesting feature is how it will
|
|
29
|
+
# intelligently forward ActiveRecord method calls to its member
|
|
30
|
+
# sets. This allows you to execute find operations directly on a
|
|
31
|
+
# UnionCollection, that will be executed on one or more
|
|
32
|
+
# of the member sets. Given the prior definition calling
|
|
33
|
+
#
|
|
34
|
+
# union.find(:all, :conditions => "id <= 1 OR id >= 20")
|
|
35
|
+
#
|
|
36
|
+
# would return an array containing all the records from set 0
|
|
37
|
+
# and set 2 (set 1 would be implicity excluded by the <tt>:conditions</tt>),
|
|
38
|
+
#
|
|
39
|
+
# union.find_by_name('george')
|
|
40
|
+
#
|
|
41
|
+
# would return a single entry fetched from set 2 if george's id was >= 20,
|
|
42
|
+
#
|
|
43
|
+
# union.find(30)
|
|
44
|
+
#
|
|
45
|
+
# would retrieve the record from set 2 with id == 30, and
|
|
46
|
+
#
|
|
47
|
+
# union.find(9)
|
|
48
|
+
#
|
|
49
|
+
# would throw an #ActiveRecord::RecordNotFound exception because that id
|
|
50
|
+
# is specifically excluded from the union's member sets.
|
|
51
|
+
#
|
|
52
|
+
# UnionCollection operates according to the following rules:
|
|
53
|
+
#
|
|
54
|
+
# * <tt>find :first</tt> - will search the sets in order and return the
|
|
55
|
+
# first record that matches the find criteria.
|
|
56
|
+
# * <tt>find :all</tt> - will search the sets, returning a
|
|
57
|
+
# UnionCollection containing the all matching results. This UnionCollection
|
|
58
|
+
# can, of course, be searched further
|
|
59
|
+
# * <tt>find(ids)</tt> - will look through all member sets in search
|
|
60
|
+
# of records with the given ids. #ActiveRecord::RecordNotFound will
|
|
61
|
+
# be raised unless all the IDs are located.
|
|
62
|
+
# * <tt>find_by_*</tt> - works as expected, behaving like <tt>find :first</tt>
|
|
63
|
+
# * <tt>find_all_by_*</tt> - works as expected like <tt>find :all</tt>
|
|
64
|
+
#
|
|
65
|
+
class UnionCollection
|
|
66
|
+
|
|
67
|
+
# UnionCollection should be initialized with a list of ActiveRecord collections
|
|
68
|
+
#
|
|
69
|
+
# union = ActsAsNetwork::UnionCollection.new(
|
|
70
|
+
# Person.find(:all, :conditions => "id <= 1"), # dynamic find set
|
|
71
|
+
# Person.managers # an model association
|
|
72
|
+
# )
|
|
73
|
+
#
|
|
74
|
+
def initialize(*sets)
|
|
75
|
+
@sets = sets || []
|
|
76
|
+
@sets.compact! # remove nil elements
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Emulates the ActiveRecord::base.find method.
|
|
80
|
+
# Accepts all the same arguments and options
|
|
81
|
+
#
|
|
82
|
+
# union.find(:first, :conditions => ["name = ?", "George"])
|
|
83
|
+
#
|
|
84
|
+
def find(*args)
|
|
85
|
+
case args.first
|
|
86
|
+
when :first then find_initial(:find, *args)
|
|
87
|
+
when :all then find_all(:find, *args)
|
|
88
|
+
else find_from_ids(:find, *args)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_a
|
|
93
|
+
load_sets
|
|
94
|
+
@arr
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def load_sets
|
|
100
|
+
@arr = []
|
|
101
|
+
@sets.each{|set| @arr.concat set unless set.nil?} unless @sets.nil?
|
|
102
|
+
@arr.uniq!
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# start by passing the find to set 0. If no results are returned
|
|
106
|
+
# pass the find on to set 1, and so on.
|
|
107
|
+
def find_initial(method_id, *args)
|
|
108
|
+
# conditions get ANDed together on subequent runs in this scope
|
|
109
|
+
# by ActiveRecord. We'lls separate the conditions out, save a copy
|
|
110
|
+
# of the initial state, and pass it to subsequent runs
|
|
111
|
+
conditions = args[1][:conditions] if args.size > 1 and args[1].kind_of?(Hash)
|
|
112
|
+
|
|
113
|
+
# this iteration is a great opportunity for future optimization -
|
|
114
|
+
# with find initial there is no need to continue processing once we
|
|
115
|
+
# find a match
|
|
116
|
+
results = @sets.collect { |set|
|
|
117
|
+
args[1][:conditions] = conditions unless conditions.nil?
|
|
118
|
+
set.empty? ? nil : set.send(method_id, *args)
|
|
119
|
+
}.compact
|
|
120
|
+
results.size > 0 ? results[0] : nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def find_all(method_id, *args)
|
|
124
|
+
# create a new UnionCollection with new member sets containing the
|
|
125
|
+
# results of the find accross the current member sets
|
|
126
|
+
UnionCollection.new(*@sets.collect{|set| set.empty? ? nil : set.send(method_id, *Marshal::load(Marshal.dump(args))) })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Invokes method against set1, catching ActiveRecord::RecordNotFound.
|
|
130
|
+
# if exception is raised try the method execution against set2
|
|
131
|
+
def find_from_ids(method_id, *args)
|
|
132
|
+
res = []
|
|
133
|
+
|
|
134
|
+
# another good target for future optimization - if only
|
|
135
|
+
# one id is presented for the search there is no need to proxy
|
|
136
|
+
# the call out to ever set - we can stop when we hit a match
|
|
137
|
+
args.each do |id|
|
|
138
|
+
@sets.each do |set|
|
|
139
|
+
begin
|
|
140
|
+
res << set.send(method_id, id) unless set.empty?
|
|
141
|
+
rescue ActiveRecord::RecordNotFound
|
|
142
|
+
# rethrow later
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
res.uniq!
|
|
148
|
+
if args.uniq.size != res.size
|
|
149
|
+
#FIXME
|
|
150
|
+
raise ActiveRecord::RecordNotFound.new "Couldn't find all records with IDs (#{args.join ','})"
|
|
151
|
+
end
|
|
152
|
+
args.size == 1 ? res[0] : res
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Handle find_by convenience methods
|
|
156
|
+
def method_missing(method_id, *args, &block)
|
|
157
|
+
if method_id.to_s =~ /^find_all_by/
|
|
158
|
+
find_all method_id, *args, &block
|
|
159
|
+
elsif method_id.to_s =~ /^find_by/
|
|
160
|
+
find_initial method_id, *args, &block
|
|
161
|
+
else
|
|
162
|
+
load_sets
|
|
163
|
+
@arr.send method_id, *args, &block
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
module Network #:nodoc:
|
|
169
|
+
def self.included(base)
|
|
170
|
+
base.extend ClassMethods
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
module ClassMethods
|
|
174
|
+
# = acts_as_network
|
|
175
|
+
#
|
|
176
|
+
# ActsAsNetwork expects a few things to be present before it is
|
|
177
|
+
# called. Namely, you need to establish the existance of either
|
|
178
|
+
# 1. a HABTM join table; or
|
|
179
|
+
# 2. an intermediate Join model
|
|
180
|
+
#
|
|
181
|
+
# == HABTM
|
|
182
|
+
#
|
|
183
|
+
# In the first case, +acts_as_network+ will assume that your HABTM table is named
|
|
184
|
+
# in a self-referential manner based on the model name. i.e. if your model is called
|
|
185
|
+
# +Person+ it will assume the HABTM join table is called +people_people+.
|
|
186
|
+
# It will also default the +foreign_key+ column to be named after the model: +person_id+.
|
|
187
|
+
# The default +association_foreign_key+ column will be the +foreign_key+ name with +_target+
|
|
188
|
+
# appended.
|
|
189
|
+
#
|
|
190
|
+
# acts_as_network :friends
|
|
191
|
+
#
|
|
192
|
+
# You can override any of these options in your call to +acts_as_network+. The
|
|
193
|
+
# following will use a join table named +friends+ with a foreign key of +person_id+
|
|
194
|
+
# and an association foreign key of +friend_id+
|
|
195
|
+
#
|
|
196
|
+
# acts_as_network :friends, :join_table => :friends, :foreign_key => 'person_id', :association_foreign_key => 'friend_id'
|
|
197
|
+
#
|
|
198
|
+
# == Join Model
|
|
199
|
+
#
|
|
200
|
+
# In the second case +acts_as_network+ will need to be told which model to use to perform the join - this is
|
|
201
|
+
# accomplished by passing a symbol for the join model to the <tt>:through</tt> option. So, with a join model called invites
|
|
202
|
+
# use:
|
|
203
|
+
#
|
|
204
|
+
# acts_as_network :friends, :through => :invites
|
|
205
|
+
#
|
|
206
|
+
# The same assumptions are made relative to the foreign_key and association_foreign_key columns, which can be overriden using
|
|
207
|
+
# the same options. It may be useful to include <tt>:conditions</tt> as well depending on the specific requirements of the
|
|
208
|
+
# join model. The following will create a network relation using a join model named +Invite+ with a foreign_key of
|
|
209
|
+
# +person_id+, an association_foreign_key of +friend_id+, where the Invite's +is_accepted+ field
|
|
210
|
+
# is true.
|
|
211
|
+
#
|
|
212
|
+
# acts_as_network :friends, :through => :invites, :foreign_key => 'person_id',
|
|
213
|
+
# :association_foreign_key => 'friend_id', [:conditions => "is_accepted = ?", true]
|
|
214
|
+
#
|
|
215
|
+
# The valid configuration options that can be passed to +acts_as_network+ follow:
|
|
216
|
+
#
|
|
217
|
+
# * <tt>:through</tt> - class to use for has_many :through relationship. If omitted acts_as_network
|
|
218
|
+
# will fall back on a HABTM relation
|
|
219
|
+
# * <tt>:join_table</tt> - when using a simple HABTM relation, this allows you to override the
|
|
220
|
+
# name of the join table. Defaults to <tt>model_model</tt> format, i.e. people_people
|
|
221
|
+
# * <tt>:foreign_key</tt> - name of the foreign key for the origin side of relation -
|
|
222
|
+
# i.e. person_id.
|
|
223
|
+
# * <tt>:association_foreign_key</tt> - name of the foreign key for the target side,
|
|
224
|
+
# i.e. person_id_target. Defaults to the same value as +foreign_key+ with a <tt>_target</tt> suffix
|
|
225
|
+
# * <tt>:conditions</tt> - optional, standard ActiveRecord SQL contition clause
|
|
226
|
+
#
|
|
227
|
+
def acts_as_network(relationship, options = {})
|
|
228
|
+
configuration = {
|
|
229
|
+
:foreign_key => name.foreign_key,
|
|
230
|
+
:association_foreign_key => "#{name.foreign_key}_target",
|
|
231
|
+
:join_table => "#{name.tableize}_#{name.tableize}"
|
|
232
|
+
}
|
|
233
|
+
configuration.update(options) if options.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
if configuration[:through].nil?
|
|
236
|
+
has_and_belongs_to_many "#{relationship}_out".to_sym, :class_name => name,
|
|
237
|
+
:foreign_key => configuration[:foreign_key], :association_foreign_key => configuration[:association_foreign_key],
|
|
238
|
+
:join_table => configuration[:join_table], :conditions => configuration[:conditions]
|
|
239
|
+
|
|
240
|
+
has_and_belongs_to_many "#{relationship}_in".to_sym, :class_name => name,
|
|
241
|
+
:foreign_key => configuration[:association_foreign_key], :association_foreign_key => configuration[:foreign_key],
|
|
242
|
+
:join_table => configuration[:join_table], :conditions => configuration[:conditions]
|
|
243
|
+
|
|
244
|
+
else
|
|
245
|
+
|
|
246
|
+
through_class = configuration[:through].to_s.classify
|
|
247
|
+
through_sym = configuration[:through]
|
|
248
|
+
|
|
249
|
+
# a node has many outbound relationships
|
|
250
|
+
has_many "#{through_sym}_out".to_sym, :class_name => through_class,
|
|
251
|
+
:foreign_key => configuration[:foreign_key]
|
|
252
|
+
has_many "#{relationship}_out".to_sym, :through => "#{through_sym}_out".to_sym,
|
|
253
|
+
:source => "#{name.tableize.singularize}_target", :foreign_key => configuration[:foreign_key],
|
|
254
|
+
:conditions => configuration[:conditions]
|
|
255
|
+
|
|
256
|
+
# a node has many inbound relationships
|
|
257
|
+
has_many "#{through_sym}_in".to_sym, :class_name => through_class,
|
|
258
|
+
:foreign_key => configuration[:association_foreign_key]
|
|
259
|
+
has_many "#{relationship}_in".to_sym, :through => "#{through_sym}_in".to_sym,
|
|
260
|
+
:source => name.tableize.singularize, :foreign_key => configuration[:association_foreign_key],
|
|
261
|
+
:conditions => configuration[:conditions]
|
|
262
|
+
|
|
263
|
+
# when using a join model, define a method providing a unioned view of all the join
|
|
264
|
+
# records. i.e. if People acts_as_network :contacts :through => :invites, this method
|
|
265
|
+
# is defined as def invites
|
|
266
|
+
class_eval <<-EOV
|
|
267
|
+
acts_as_union :#{through_sym}, [ :#{through_sym}_in, :#{through_sym}_out ]
|
|
268
|
+
EOV
|
|
269
|
+
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# define the accessor method for the reciprocal network relationship view itself.
|
|
273
|
+
# i.e. if People acts_as_network :contacts, this method is defind as def contacts
|
|
274
|
+
class_eval <<-EOV
|
|
275
|
+
acts_as_union :#{relationship}, [ :#{relationship}_in, :#{relationship}_out ]
|
|
276
|
+
EOV
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
module Union
|
|
282
|
+
def self.included(base)
|
|
283
|
+
base.extend ClassMethods
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
module ClassMethods
|
|
287
|
+
# = acts_as_union
|
|
288
|
+
# acts_as_union simply presents a union'ed view of one or more ActiveRecord
|
|
289
|
+
# relationships (has_many or has_and_belongs_to_many, acts_as_network, etc).
|
|
290
|
+
#
|
|
291
|
+
# class Person < ActiveRecord::Base
|
|
292
|
+
# acts_as_network :friends
|
|
293
|
+
# acts_as_network :colleagues, :through => :invites, :foreign_key => 'person_id',
|
|
294
|
+
# :conditions => ["is_accepted = ?", true]
|
|
295
|
+
# acts_as_union :aquantainces, [:friends, :colleagues]
|
|
296
|
+
# end
|
|
297
|
+
#
|
|
298
|
+
# In this case a call to the +aquantainces+ method will return a UnionCollection on both
|
|
299
|
+
# a person's +friends+ and their +colleagues+. Likewise, finder operations will work accross
|
|
300
|
+
# the two distinct sets as if they were one. Thus, for the following code
|
|
301
|
+
#
|
|
302
|
+
# stephen = Person.find_by_name('Stephen')
|
|
303
|
+
# # search for user by login
|
|
304
|
+
# billy = stephen.aquantainces.find_by_name('Billy')
|
|
305
|
+
#
|
|
306
|
+
# both Stephen's +friends+ and +colleagues+ collections would be searched for someone named Billy.
|
|
307
|
+
#
|
|
308
|
+
# +acts_as_union+ doesn't accept any options.
|
|
309
|
+
#
|
|
310
|
+
def acts_as_union(relationship, methods)
|
|
311
|
+
# define the accessor method for the union.
|
|
312
|
+
# i.e. if People acts_as_union :jobs, this method is defined as def jobs
|
|
313
|
+
class_eval <<-EOV
|
|
314
|
+
def #{relationship}
|
|
315
|
+
UnionCollection.new(#{methods.collect{|m| "self.#{m.to_s}"}.join(',')})
|
|
316
|
+
end
|
|
317
|
+
EOV
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
ActiveRecord::Base.send :include, ActsAsNetwork::Network
|
|
324
|
+
ActiveRecord::Base.send :include, ActsAsNetwork::Union
|