blendris 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.
- data.tar.gz.sig +0 -0
- data/History.txt +4 -0
- data/Manifest +32 -0
- data/PostInstall.txt +1 -0
- data/README.rdoc +79 -0
- data/Rakefile +22 -0
- data/autotest/discover.rb +3 -0
- data/blendris.gemspec +32 -0
- data/lib/blendris.rb +25 -0
- data/lib/blendris/accessor.rb +62 -0
- data/lib/blendris/errors.rb +3 -0
- data/lib/blendris/integer.rb +19 -0
- data/lib/blendris/list.rb +37 -0
- data/lib/blendris/model.rb +141 -0
- data/lib/blendris/node.rb +46 -0
- data/lib/blendris/reference.rb +51 -0
- data/lib/blendris/reference_base.rb +68 -0
- data/lib/blendris/reference_set.rb +65 -0
- data/lib/blendris/set.rb +39 -0
- data/lib/blendris/string.rb +19 -0
- data/lib/blendris/types.rb +14 -0
- data/lib/blendris/utils.rb +39 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/list_spec.rb +17 -0
- data/spec/model_spec.rb +180 -0
- data/spec/redis-tools_spec.rb +15 -0
- data/spec/ref_spec.rb +40 -0
- data/spec/set_spec.rb +20 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +85 -0
- data/spec/string_spec.rb +20 -0
- data/tasks/rspec.rake +21 -0
- metadata +128 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
# RedisNode is used to compose all Redis value wrapper classes.
|
4
|
+
module RedisNode
|
5
|
+
|
6
|
+
include RedisAccessor
|
7
|
+
|
8
|
+
def initialize(key, options = {})
|
9
|
+
@key = sanitize_key(key)
|
10
|
+
@default = options[:default]
|
11
|
+
@options = options
|
12
|
+
|
13
|
+
set(@default) if @default && !redis.exists(self.key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(value)
|
17
|
+
if value
|
18
|
+
redis.set key, self.class.cast_to_redis(value, @options)
|
19
|
+
else
|
20
|
+
redis.del key
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get
|
25
|
+
self.class.cast_from_redis redis.get(self.key), @options
|
26
|
+
end
|
27
|
+
|
28
|
+
def key
|
29
|
+
prefix + @key
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
redis.del key
|
34
|
+
end
|
35
|
+
|
36
|
+
def type
|
37
|
+
redis.type key
|
38
|
+
end
|
39
|
+
|
40
|
+
def exists?
|
41
|
+
redis.exists key
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
class RedisReference < RedisReferenceBase
|
4
|
+
|
5
|
+
include RedisNode
|
6
|
+
|
7
|
+
def ref
|
8
|
+
@ref ||= RedisString.new(@key)
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(obj)
|
12
|
+
old_obj = self.get if @reverse
|
13
|
+
modified = false
|
14
|
+
refkey = self.class.cast_to_redis(obj, @options)
|
15
|
+
|
16
|
+
if refkey == nil
|
17
|
+
ref.set nil
|
18
|
+
modified = true
|
19
|
+
elsif refkey != ref.get
|
20
|
+
ref.set refkey
|
21
|
+
apply_reverse_add obj
|
22
|
+
modified = true
|
23
|
+
end
|
24
|
+
|
25
|
+
apply_reverse_delete(old_obj) if modified
|
26
|
+
|
27
|
+
obj
|
28
|
+
end
|
29
|
+
|
30
|
+
def get
|
31
|
+
self.class.cast_from_redis ref.get
|
32
|
+
end
|
33
|
+
|
34
|
+
def assign_ref(value)
|
35
|
+
self.set value
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_ref(value)
|
39
|
+
self.set nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def references(value)
|
43
|
+
refval = ref.get
|
44
|
+
|
45
|
+
return true if refval.nil? && value.nil?
|
46
|
+
return refval == value.key
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
class RedisReferenceBase
|
4
|
+
|
5
|
+
include RedisNode
|
6
|
+
extend RedisAccessor
|
7
|
+
|
8
|
+
def initialize(key, options = {})
|
9
|
+
@model = options[:model]
|
10
|
+
@key = sanitize_key(key)
|
11
|
+
@reverse = options[:reverse]
|
12
|
+
@options = options
|
13
|
+
|
14
|
+
@klass = options[:class] || Model
|
15
|
+
@klass = constantize(camelize @klass) if @klass.kind_of? String
|
16
|
+
|
17
|
+
unless @klass.ancestors.include? Model
|
18
|
+
raise ArgumentError.new("#{klass.name} is not a model")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_reverse_add(value)
|
23
|
+
if @reverse && value
|
24
|
+
reverse = value.redis_symbol(@reverse)
|
25
|
+
reverse.assign_ref(@model) if !reverse.references @model
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_reverse_delete(value)
|
30
|
+
if @reverse && value
|
31
|
+
reverse = value.redis_symbol(@reverse)
|
32
|
+
reverse.remove_ref(@model) if reverse.references @model
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.cast_to_redis(obj, options = {})
|
37
|
+
expect = options[:class] || Model
|
38
|
+
expect = constantize(expect) if expect.kind_of? String
|
39
|
+
expect = Model unless expect.ancestors.include? Model
|
40
|
+
|
41
|
+
if obj == nil
|
42
|
+
nil
|
43
|
+
elsif obj.kind_of? expect
|
44
|
+
obj.key
|
45
|
+
else
|
46
|
+
raise TypeError.new("#{obj.class.name} is not a #{expect.name}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.cast_from_redis(refkey, options = {})
|
51
|
+
expect = options[:class] || Model
|
52
|
+
expect = constantize(expect) if expect.kind_of? String
|
53
|
+
expect = Model unless expect.ancestors.include? Model
|
54
|
+
|
55
|
+
klass = constantize(redis.get(prefix + refkey)) if refkey
|
56
|
+
|
57
|
+
if klass == nil
|
58
|
+
nil
|
59
|
+
elsif klass.ancestors.include? expect
|
60
|
+
klass.new refkey
|
61
|
+
else
|
62
|
+
raise TypeError.new("#{klass.name} is not a #{expect.name}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
class RedisReferenceSet < RedisReferenceBase
|
4
|
+
|
5
|
+
include RedisNode
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def refs
|
9
|
+
@refs ||= RedisSet.new(@key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(*objs)
|
13
|
+
objs.flatten!
|
14
|
+
objs.compact!
|
15
|
+
|
16
|
+
objs.each do |obj|
|
17
|
+
if refkey = self.class.cast_to_redis(obj, @options)
|
18
|
+
refs << refkey
|
19
|
+
apply_reverse_add obj
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
alias :<< :set
|
26
|
+
|
27
|
+
def delete(obj)
|
28
|
+
if refkey = self.class.cast_to_redis(obj, @options)
|
29
|
+
deleted = refs.delete(refkey)
|
30
|
+
apply_reverse_delete(obj) if deleted
|
31
|
+
deleted
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def each
|
40
|
+
redis.smembers(key).each do |refkey|
|
41
|
+
yield self.class.cast_from_redis(refkey, @options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def include?(obj)
|
46
|
+
refkey = self.class.cast_to_redis(obj, @options)
|
47
|
+
|
48
|
+
refs.include? refkey
|
49
|
+
end
|
50
|
+
|
51
|
+
def assign_ref(*values)
|
52
|
+
self.set *values
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_ref(value)
|
56
|
+
self.delete value
|
57
|
+
end
|
58
|
+
|
59
|
+
def references(value)
|
60
|
+
self.include? value
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/blendris/set.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
class RedisSet
|
4
|
+
|
5
|
+
include RedisNode
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(key, options = {})
|
9
|
+
@key = key.to_s
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def each
|
14
|
+
redis.smembers(key).each do |value|
|
15
|
+
yield value
|
16
|
+
end
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(value)
|
22
|
+
[ value ].flatten.compact.each do |v|
|
23
|
+
redis.sadd key, v
|
24
|
+
end
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def get
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(value)
|
34
|
+
redis.srem key, value
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
class RedisString
|
4
|
+
|
5
|
+
include RedisNode
|
6
|
+
|
7
|
+
def self.cast_to_redis(value, options = {})
|
8
|
+
raise TypeError.new("#{value.class.name} is not a string") unless value.kind_of? String
|
9
|
+
|
10
|
+
value
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.cast_from_redis(value, options = {})
|
14
|
+
value
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Blendris
|
2
|
+
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
# Method lifted from Rails.
|
6
|
+
def constantize(camel_cased_word)
|
7
|
+
return if blank(camel_cased_word)
|
8
|
+
|
9
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
|
10
|
+
raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
|
11
|
+
end
|
12
|
+
|
13
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Method lifted from Rails.
|
17
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
18
|
+
if first_letter_in_uppercase
|
19
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
20
|
+
else
|
21
|
+
lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Tests if the given object is blank.
|
26
|
+
def blank(obj)
|
27
|
+
return true if obj.nil?
|
28
|
+
return obj.strip.empty? if obj.kind_of? String
|
29
|
+
return obj.empty? if obj.respond_to? :empty?
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
def sanitize_key(key)
|
34
|
+
key.to_s.gsub(/[\r\n\s]/, "_").gsub(/^:+|:+$/, "")
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# File: script/console
|
3
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
4
|
+
|
5
|
+
libs = " -r irb/completion"
|
6
|
+
# Perhaps use a console_lib to store any extra methods I may want available in the cosole
|
7
|
+
# libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
|
8
|
+
libs << " -r #{File.dirname(__FILE__) + '/../lib/blendris.rb'}"
|
9
|
+
puts "Loading blendris gem"
|
10
|
+
exec "#{irb} #{libs} --simple-prompt"
|
data/script/destroy
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/destroy'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Destroy.new.run(ARGV)
|
data/script/generate
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rubigen'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rubigen'
|
9
|
+
end
|
10
|
+
require 'rubigen/scripts/generate'
|
11
|
+
|
12
|
+
ARGV.shift if ['--help', '-h'].include?(ARGV[0])
|
13
|
+
RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
|
14
|
+
RubiGen::Scripts::Generate.new.run(ARGV)
|
data/spec/list_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe "redis lists" do
|
4
|
+
|
5
|
+
it "should read and write" do
|
6
|
+
@onion.sales.count.should == 0
|
7
|
+
|
8
|
+
@onion.sales << %w( to-bill to-tom to-bill to-bob to-tom )
|
9
|
+
@onion.sales.count.should == 5
|
10
|
+
@onion.sales.to_a.should == %w( to-bill to-tom to-bill to-bob to-tom )
|
11
|
+
|
12
|
+
@onion.sales.delete("to-bill").should == 2
|
13
|
+
@onion.sales.delete("to-bob").should == 1
|
14
|
+
@onion.sales.to_a.should == %w( to-tom to-tom )
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/spec/model_spec.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Model do
|
4
|
+
|
5
|
+
it "should have valid keys" do
|
6
|
+
@onion.key.should == "food:onion"
|
7
|
+
@onion.name.should == "onion"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have a working integer field" do
|
11
|
+
@onion.calories.should == 0
|
12
|
+
|
13
|
+
@onion.calories = 120
|
14
|
+
@onion.calories.should == 120
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have a valid reference" do
|
18
|
+
@onion.category.should be_nil
|
19
|
+
|
20
|
+
@onion.category = @vegetable
|
21
|
+
@onion.category.name.should == "vegetable"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should have a valid reference set" do
|
25
|
+
@fruit.foods.count.should == 0
|
26
|
+
|
27
|
+
@fruit.foods << [ @apple, @lemon ]
|
28
|
+
|
29
|
+
@fruit.foods.should be_include(@apple)
|
30
|
+
@fruit.foods.should be_include(@lemon)
|
31
|
+
@fruit.foods.should be_include(Food.new("food:lemon"))
|
32
|
+
@fruit.foods.should_not be_include(@steak)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should not allow you to instantiate with a key that doesnt match its class" do
|
36
|
+
lambda { Category.new("balogna") }.should raise_error(TypeError)
|
37
|
+
lambda { Category.new(@onion.key) }.should raise_error(TypeError)
|
38
|
+
lambda { Category.new(@vegetable.key) }.should_not raise_error(TypeError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should allow for complex types in the key" do
|
42
|
+
lambda { FavoriteFood.create("Billy Bob Thorton", "squeezy") }.should raise_error(TypeError)
|
43
|
+
|
44
|
+
fav = FavoriteFood.create("Billy Bob Thorton", @onion)
|
45
|
+
|
46
|
+
fav.key.should == "person:Billy_Bob_Thorton:food:onion"
|
47
|
+
fav.person.should == "Billy Bob Thorton"
|
48
|
+
fav.food.should == @onion
|
49
|
+
end
|
50
|
+
|
51
|
+
context "with single reference" do
|
52
|
+
|
53
|
+
it "should reverse to a single reference" do
|
54
|
+
@apple.sibling.should be_nil
|
55
|
+
@lemon.sibling.should be_nil
|
56
|
+
|
57
|
+
@apple.sibling = @lemon
|
58
|
+
|
59
|
+
@apple.sibling.should == @lemon
|
60
|
+
@lemon.sibling.should == @apple
|
61
|
+
|
62
|
+
@lemon.sibling = nil
|
63
|
+
|
64
|
+
@apple.sibling.should be_nil
|
65
|
+
@lemon.sibling.should be_nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should reverse to a reference set" do
|
69
|
+
@onion.category.should be_nil
|
70
|
+
@vegetable.foods.count.should be_zero
|
71
|
+
|
72
|
+
2.times do
|
73
|
+
@onion.category = @vegetable
|
74
|
+
|
75
|
+
@onion.category.name.should == "vegetable"
|
76
|
+
@vegetable.foods.count.should == 1
|
77
|
+
@vegetable.foods.should be_include(@onion)
|
78
|
+
end
|
79
|
+
|
80
|
+
@onion.category = nil
|
81
|
+
|
82
|
+
@onion.category.should be_nil
|
83
|
+
@vegetable.foods.count.should == 0
|
84
|
+
@vegetable.foods.should_not be_include(@onion)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should allow for generic references" do
|
88
|
+
@onion.something.should be_nil
|
89
|
+
|
90
|
+
@onion.something = @steak
|
91
|
+
@onion.something.name.should == "steak"
|
92
|
+
|
93
|
+
@onion.something = @vegetable
|
94
|
+
@onion.something.name.should == "vegetable"
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with reference sets" do
|
100
|
+
|
101
|
+
it "should reverse to single references" do
|
102
|
+
@onion.category.should be_nil
|
103
|
+
@vegetable.foods.count.should be_zero
|
104
|
+
|
105
|
+
2.times do
|
106
|
+
@vegetable.foods << @onion
|
107
|
+
|
108
|
+
@onion.category.name.should == "vegetable"
|
109
|
+
@vegetable.foods.count.should == 1
|
110
|
+
@vegetable.foods.should be_include(@onion)
|
111
|
+
end
|
112
|
+
|
113
|
+
@vegetable.foods.delete @onion
|
114
|
+
|
115
|
+
@onion.category.should be_nil
|
116
|
+
@vegetable.foods.count.should == 0
|
117
|
+
@vegetable.foods.should_not be_include(@onion)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should reverse to a reference set" do
|
121
|
+
@apple.friends.count.should == 0
|
122
|
+
@lemon.friends.count.should == 0
|
123
|
+
@onion.friends.count.should == 0
|
124
|
+
|
125
|
+
@apple.friends << [ @lemon, @onion ]
|
126
|
+
|
127
|
+
@apple.friends.count.should == 2
|
128
|
+
@lemon.friends.count.should == 1
|
129
|
+
@onion.friends.count.should == 1
|
130
|
+
|
131
|
+
@apple.friends.should be_include(@lemon)
|
132
|
+
@apple.friends.should be_include(@onion)
|
133
|
+
@lemon.friends.should be_include(@apple)
|
134
|
+
@onion.friends.should be_include(@apple)
|
135
|
+
|
136
|
+
@apple.friends.delete @lemon
|
137
|
+
|
138
|
+
@apple.friends.count.should == 1
|
139
|
+
@lemon.friends.count.should == 0
|
140
|
+
@onion.friends.count.should == 1
|
141
|
+
|
142
|
+
@apple.friends.should_not be_include(@lemon)
|
143
|
+
@apple.friends.should be_include(@onion)
|
144
|
+
@lemon.friends.should_not be_include(@apple)
|
145
|
+
@onion.friends.should be_include(@apple)
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
context "website sister sites" do
|
151
|
+
|
152
|
+
it "should update each others sister sites table" do
|
153
|
+
|
154
|
+
site1 = Website.create("Site One")
|
155
|
+
site2 = Website.create("Site Two")
|
156
|
+
|
157
|
+
site1.sister_sites.count.should == 0
|
158
|
+
site2.sister_sites.count.should == 0
|
159
|
+
site1.sister_sites.should_not be_include site2
|
160
|
+
site2.sister_sites.should_not be_include site1
|
161
|
+
|
162
|
+
site1.sister_sites << site2
|
163
|
+
|
164
|
+
site1.sister_sites.count.should == 1
|
165
|
+
site2.sister_sites.count.should == 1
|
166
|
+
site1.sister_sites.should be_include site2
|
167
|
+
site2.sister_sites.should be_include site1
|
168
|
+
|
169
|
+
site2.sister_sites.delete site1
|
170
|
+
|
171
|
+
site1.sister_sites.count.should == 0
|
172
|
+
site2.sister_sites.count.should == 0
|
173
|
+
site1.sister_sites.should_not be_include site2
|
174
|
+
site2.sister_sites.should_not be_include site1
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|