datamapper 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/CHANGELOG +2 -0
- data/MIT-LICENSE +22 -0
- data/README +1 -0
- data/example.rb +25 -0
- data/lib/data_mapper.rb +30 -0
- data/lib/data_mapper/adapters/abstract_adapter.rb +229 -0
- data/lib/data_mapper/adapters/mysql_adapter.rb +171 -0
- data/lib/data_mapper/adapters/sqlite3_adapter.rb +189 -0
- data/lib/data_mapper/associations.rb +19 -0
- data/lib/data_mapper/associations/belongs_to_association.rb +111 -0
- data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +100 -0
- data/lib/data_mapper/associations/has_many_association.rb +101 -0
- data/lib/data_mapper/associations/has_one_association.rb +107 -0
- data/lib/data_mapper/base.rb +160 -0
- data/lib/data_mapper/callbacks.rb +47 -0
- data/lib/data_mapper/database.rb +134 -0
- data/lib/data_mapper/extensions/active_record_impersonation.rb +69 -0
- data/lib/data_mapper/extensions/callback_helpers.rb +35 -0
- data/lib/data_mapper/identity_map.rb +21 -0
- data/lib/data_mapper/loaded_set.rb +45 -0
- data/lib/data_mapper/mappings/column.rb +78 -0
- data/lib/data_mapper/mappings/schema.rb +28 -0
- data/lib/data_mapper/mappings/table.rb +99 -0
- data/lib/data_mapper/queries/conditions.rb +141 -0
- data/lib/data_mapper/queries/connection.rb +34 -0
- data/lib/data_mapper/queries/create_table_statement.rb +38 -0
- data/lib/data_mapper/queries/delete_statement.rb +17 -0
- data/lib/data_mapper/queries/drop_table_statement.rb +17 -0
- data/lib/data_mapper/queries/insert_statement.rb +29 -0
- data/lib/data_mapper/queries/reader.rb +42 -0
- data/lib/data_mapper/queries/result.rb +19 -0
- data/lib/data_mapper/queries/select_statement.rb +103 -0
- data/lib/data_mapper/queries/table_exists_statement.rb +17 -0
- data/lib/data_mapper/queries/truncate_table_statement.rb +17 -0
- data/lib/data_mapper/queries/update_statement.rb +25 -0
- data/lib/data_mapper/session.rb +240 -0
- data/lib/data_mapper/support/blank_slate.rb +3 -0
- data/lib/data_mapper/support/connection_pool.rb +117 -0
- data/lib/data_mapper/support/enumerable.rb +27 -0
- data/lib/data_mapper/support/inflector.rb +329 -0
- data/lib/data_mapper/support/proc.rb +69 -0
- data/lib/data_mapper/support/string.rb +23 -0
- data/lib/data_mapper/support/symbol.rb +91 -0
- data/lib/data_mapper/support/weak_hash.rb +46 -0
- data/lib/data_mapper/unit_of_work.rb +38 -0
- data/lib/data_mapper/validations/confirmation_validator.rb +55 -0
- data/lib/data_mapper/validations/contextual_validations.rb +50 -0
- data/lib/data_mapper/validations/format_validator.rb +85 -0
- data/lib/data_mapper/validations/formats/email.rb +78 -0
- data/lib/data_mapper/validations/generic_validator.rb +27 -0
- data/lib/data_mapper/validations/length_validator.rb +75 -0
- data/lib/data_mapper/validations/required_field_validator.rb +47 -0
- data/lib/data_mapper/validations/unique_validator.rb +65 -0
- data/lib/data_mapper/validations/validation_errors.rb +34 -0
- data/lib/data_mapper/validations/validation_helper.rb +60 -0
- data/performance.rb +156 -0
- data/profile_data_mapper.rb +18 -0
- data/rakefile.rb +80 -0
- data/spec/basic_finder.rb +67 -0
- data/spec/belongs_to.rb +47 -0
- data/spec/fixtures/animals.yaml +32 -0
- data/spec/fixtures/exhibits.yaml +90 -0
- data/spec/fixtures/fruit.yaml +6 -0
- data/spec/fixtures/people.yaml +15 -0
- data/spec/fixtures/zoos.yaml +20 -0
- data/spec/has_and_belongs_to_many.rb +25 -0
- data/spec/has_many.rb +34 -0
- data/spec/legacy.rb +14 -0
- data/spec/models/animal.rb +7 -0
- data/spec/models/exhibit.rb +6 -0
- data/spec/models/fruit.rb +6 -0
- data/spec/models/person.rb +7 -0
- data/spec/models/post.rb +4 -0
- data/spec/models/sales_person.rb +4 -0
- data/spec/models/zoo.rb +5 -0
- data/spec/new_record.rb +24 -0
- data/spec/spec_helper.rb +61 -0
- data/spec/sub_select.rb +16 -0
- data/spec/symbolic_operators.rb +21 -0
- data/spec/validates_confirmation_of.rb +36 -0
- data/spec/validates_format_of.rb +61 -0
- data/spec/validates_length_of.rb +101 -0
- data/spec/validates_uniqueness_of.rb +45 -0
- data/spec/validations.rb +63 -0
- metadata +134 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'example'
|
2
|
+
require 'ruby-prof'
|
3
|
+
|
4
|
+
# RubyProf, making profiling Ruby pretty since 1899!
|
5
|
+
def profile(&b)
|
6
|
+
result = RubyProf.profile &b
|
7
|
+
|
8
|
+
printer = RubyProf::GraphHtmlPrinter.new(result)
|
9
|
+
File::open('profile_results.html', 'w+') do |file|
|
10
|
+
printer.print(file, 0)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
profile do
|
15
|
+
1000.times do
|
16
|
+
Zoo[:name => 'Galveston']
|
17
|
+
end
|
18
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/contrib/rubyforgepublisher'
|
8
|
+
|
9
|
+
task :default => 'test'
|
10
|
+
|
11
|
+
desc "Run specifications"
|
12
|
+
Spec::Rake::SpecTask.new('test') do |t|
|
13
|
+
t.spec_opts = [ '-rspec/spec_helper' ]
|
14
|
+
t.spec_files = FileList[ENV['FILES'] || 'spec/*.rb']
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run comparison with ActiveRecord"
|
18
|
+
task :perf do
|
19
|
+
load 'performance.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Profile DataMapper"
|
23
|
+
task :profile do
|
24
|
+
load 'profile_data_mapper.rb'
|
25
|
+
end
|
26
|
+
|
27
|
+
PACKAGE_VERSION = '0.1.0'
|
28
|
+
|
29
|
+
PACKAGE_FILES = FileList[
|
30
|
+
'README',
|
31
|
+
'CHANGELOG',
|
32
|
+
'MIT-LICENSE',
|
33
|
+
'*.rb',
|
34
|
+
'lib/**/*.rb',
|
35
|
+
'spec/**/*.{rb,yaml}'
|
36
|
+
].to_a
|
37
|
+
|
38
|
+
PROJECT = 'datamapper'
|
39
|
+
|
40
|
+
desc "Generate Documentation"
|
41
|
+
rd = Rake::RDocTask.new do |rdoc|
|
42
|
+
rdoc.rdoc_dir = 'doc'
|
43
|
+
rdoc.title = "DataMapper -- An Object/Relational Mapper for Ruby"
|
44
|
+
rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
45
|
+
rdoc.rdoc_files.include(PACKAGE_FILES.reject { |path| path =~ /^(spec|\w+\.rb)/ })
|
46
|
+
end
|
47
|
+
|
48
|
+
gem_spec = Gem::Specification.new do |s|
|
49
|
+
s.platform = Gem::Platform::RUBY
|
50
|
+
s.name = PROJECT
|
51
|
+
s.summary = "An Object/Relational Mapper for Ruby"
|
52
|
+
s.description = "It's ActiveRecord, but Faster, Better, Simpler."
|
53
|
+
s.version = PACKAGE_VERSION
|
54
|
+
|
55
|
+
s.authors = 'Sam Smoot'
|
56
|
+
s.email = 'ssmoot@gmail.com'
|
57
|
+
s.rubyforge_project = PROJECT
|
58
|
+
s.homepage = 'http://datamapper.org'
|
59
|
+
|
60
|
+
s.files = PACKAGE_FILES
|
61
|
+
|
62
|
+
s.require_path = 'lib'
|
63
|
+
s.requirements << 'none'
|
64
|
+
s.autorequire = 'data_mapper'
|
65
|
+
|
66
|
+
s.has_rdoc = true
|
67
|
+
s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
|
68
|
+
s.extra_rdoc_files = rd.rdoc_files.reject { |path| path =~ /\.rb$/ }.to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
Rake::GemPackageTask.new(gem_spec) do |p|
|
72
|
+
p.gem_spec = gem_spec
|
73
|
+
p.need_tar = true
|
74
|
+
p.need_zip = true
|
75
|
+
end
|
76
|
+
|
77
|
+
desc "Publish to RubyForge"
|
78
|
+
task :rubyforge => [ :rdoc, :gem ] do
|
79
|
+
Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], "/var/www/gforge-projects/#{PROJECT}", 'doc').upload
|
80
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
context 'Finder' do
|
2
|
+
|
3
|
+
specify 'database-specific load should not fail' do
|
4
|
+
|
5
|
+
DataMapper::database do |db|
|
6
|
+
froggy = db.find(Animal, :first, :conditions => ['name = ?', 'Frog'])
|
7
|
+
froggy.name.should == 'Frog'
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
specify 'current-database load should not fail' do
|
13
|
+
froggy = DataMapper::database.find(Animal, :first).name.should == 'Frog'
|
14
|
+
end
|
15
|
+
|
16
|
+
specify 'load through ActiveRecord impersonation should not fail' do
|
17
|
+
Animal.find(:all).size.should == 16
|
18
|
+
end
|
19
|
+
|
20
|
+
specify 'load through Og impersonation should not fail' do
|
21
|
+
Animal.all.size.should == 16
|
22
|
+
end
|
23
|
+
|
24
|
+
specify ':conditions option should accept a hash' do
|
25
|
+
Animal.all(:conditions => { :name => 'Frog' }).size.should == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
specify 'non-standard options should be considered part of the conditions' do
|
29
|
+
database.log.debug('non-standard options should be considered part of the conditions')
|
30
|
+
zebra = Animal.first(:name => 'Zebra')
|
31
|
+
zebra.name.should == 'Zebra'
|
32
|
+
|
33
|
+
elephant = Animal[:name => 'Elephant']
|
34
|
+
elephant.name.should == 'Elephant'
|
35
|
+
|
36
|
+
aged = Person.all(:age => 29)
|
37
|
+
aged.size.should == 2
|
38
|
+
aged.first.name.should == 'Sam'
|
39
|
+
aged.last.name.should == 'Bob'
|
40
|
+
|
41
|
+
fixtures(:animals)
|
42
|
+
end
|
43
|
+
|
44
|
+
specify 'should not find deleted objects' do
|
45
|
+
database do
|
46
|
+
wally = Animal[:name => 'Whale']
|
47
|
+
wally.destroy!.should == true
|
48
|
+
|
49
|
+
wallys_evil_twin = Animal[:name => 'Whale']
|
50
|
+
wallys_evil_twin.should == nil
|
51
|
+
|
52
|
+
wally.new_record?.should == true
|
53
|
+
wally.save
|
54
|
+
|
55
|
+
Animal[:name => 'Whale'].should == wally
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
specify 'lazy-loads should issue for whole sets' do
|
60
|
+
people = Person.all
|
61
|
+
|
62
|
+
people.each do |person|
|
63
|
+
person.notes
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
data/spec/belongs_to.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
context 'An Exhibit' do
|
2
|
+
|
3
|
+
setup do
|
4
|
+
@aviary = Exhibit[:name => 'Monkey Mayhem']
|
5
|
+
end
|
6
|
+
|
7
|
+
specify 'has a zoo association' do
|
8
|
+
@aviary.zoo.class.should == Zoo
|
9
|
+
Exhibit.new.zoo.should == nil
|
10
|
+
end
|
11
|
+
|
12
|
+
specify 'belongs to a zoo' do
|
13
|
+
database do |db|
|
14
|
+
@aviary.zoo.should == @aviary.session.find(Zoo, :first, :name => 'San Diego')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
specify 'can build its zoo' do
|
19
|
+
database do |db|
|
20
|
+
e = Exhibit.new({:name => 'Super Extra Crazy Monkey Cage'})
|
21
|
+
e.zoo.should == nil
|
22
|
+
e.build_zoo({:name => 'Monkey Zoo'})
|
23
|
+
e.zoo.class == Zoo
|
24
|
+
e.zoo.new_record?.should == true
|
25
|
+
|
26
|
+
# Need to get associations working properly before this works ....
|
27
|
+
e.save
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
specify 'can build its zoo' do
|
32
|
+
database do |db|
|
33
|
+
e = Exhibit.new({:name => 'Super Extra Crazy Monkey Cage'})
|
34
|
+
e.zoo.should == nil
|
35
|
+
e.create_zoo({:name => 'Monkey Zoo'})
|
36
|
+
e.zoo.class == Zoo
|
37
|
+
e.zoo.new_record?.should == false
|
38
|
+
e.save
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
teardown do
|
43
|
+
fixtures('zoos')
|
44
|
+
fixtures('exhibits')
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
- name: Frog
|
2
|
+
notes: I am a Frog!
|
3
|
+
- name: Bear
|
4
|
+
notes: I am a Bear!
|
5
|
+
- name: Giraffe
|
6
|
+
notes: I am a Giraffe!
|
7
|
+
- name: Zebra
|
8
|
+
notes: I am a Zebra!
|
9
|
+
- name: Cow
|
10
|
+
notes: I am a Cow!
|
11
|
+
- name: Pig
|
12
|
+
notes: I am a Pig!
|
13
|
+
- name: Cup
|
14
|
+
notes: I am a Cup!
|
15
|
+
- name: Cat
|
16
|
+
notes: I am a Cat!
|
17
|
+
- name: Dog
|
18
|
+
notes: I am a Dog!
|
19
|
+
- name: Elephant
|
20
|
+
notes: I am a Elephant!
|
21
|
+
- name: Whale
|
22
|
+
notes: I am a Whale!
|
23
|
+
- name: Dolphin
|
24
|
+
notes: I am a Dolphin!
|
25
|
+
- name: Mouse
|
26
|
+
notes: I am a Mouse!
|
27
|
+
- name: Rat
|
28
|
+
notes: I am a Rat!
|
29
|
+
- name: Camel
|
30
|
+
notes: I am a Camel!
|
31
|
+
- name: Fox
|
32
|
+
notes: I am a Fox!
|
@@ -0,0 +1,90 @@
|
|
1
|
+
- name: Horses & Courses
|
2
|
+
zoo_id: 1
|
3
|
+
- name: Aviary
|
4
|
+
zoo_id: 1
|
5
|
+
- name: Polar Planet
|
6
|
+
zoo_id: 2
|
7
|
+
- name: Monkey Mayhem
|
8
|
+
zoo_id: 2
|
9
|
+
- name: Amazonia
|
10
|
+
zoo_id: 3
|
11
|
+
- name: Deep Blue Ocean
|
12
|
+
zoo_id: 3
|
13
|
+
- name: Fish Fight!
|
14
|
+
zoo_id: 3
|
15
|
+
- name: The Dolphin Show
|
16
|
+
zoo_id: 3
|
17
|
+
- name: Canal Life
|
18
|
+
zoo_id: 4
|
19
|
+
- name: The Reptile Pit
|
20
|
+
zoo_id: 4
|
21
|
+
- name: Kangaroo Exhibit
|
22
|
+
zoo_id: 4
|
23
|
+
- name: A Million Years Ago...
|
24
|
+
zoo_id: 5
|
25
|
+
- name: The Wild West
|
26
|
+
zoo_id: 5
|
27
|
+
- name: Tumble Weeds
|
28
|
+
zoo_id: 5
|
29
|
+
- name: Missisipi Mud
|
30
|
+
zoo_id: 6
|
31
|
+
- name: Craw-Daddy's of the Deep!
|
32
|
+
zoo_id: 6
|
33
|
+
- name: The Littlest Big City
|
34
|
+
zoo_id: 7
|
35
|
+
- name: Jellyfish On Parade
|
36
|
+
zoo_id: 8
|
37
|
+
- name: Sea-Life Among the Rigs
|
38
|
+
zoo_id: 8
|
39
|
+
- name: Monsters of the Rainforest
|
40
|
+
zoo_id: 8
|
41
|
+
- name: So You Wanna Be A Street-Kid?
|
42
|
+
zoo_id: 9
|
43
|
+
- name: Don't Feed the Hippies!
|
44
|
+
zoo_id: 9
|
45
|
+
- name: At the Equator
|
46
|
+
zoo_id: 10
|
47
|
+
- name: River Habitats
|
48
|
+
zoo_id: 10
|
49
|
+
- name: A Fiesta of Plant-life
|
50
|
+
zoo_id: 10
|
51
|
+
- name: Smog World!
|
52
|
+
zoo_id: 11
|
53
|
+
- name: Where Are They Now? Extinct Species Edition
|
54
|
+
zoo_id: 11
|
55
|
+
- name: The Fragile Eco-System of the Cheese-Steak
|
56
|
+
zoo_id: 12
|
57
|
+
- name: Rats!
|
58
|
+
zoo_id: 13
|
59
|
+
- name: Penguins on Wall St.
|
60
|
+
zoo_id: 13
|
61
|
+
- name: King of the Jungle
|
62
|
+
zoo_id: 13
|
63
|
+
- name: The World's Fish-Market
|
64
|
+
zoo_id: 14
|
65
|
+
- name: Water-World
|
66
|
+
zoo_id: 14
|
67
|
+
- name: Micro-sized Soft-shelled Crabs
|
68
|
+
zoo_id: 14
|
69
|
+
- name: Do Animals Like Coffee?
|
70
|
+
zoo_id: 14
|
71
|
+
- name: On A Clear Day...
|
72
|
+
zoo_id: 15
|
73
|
+
- name: Twenty Thousand Leagues Under the Sky
|
74
|
+
zoo_id: 15
|
75
|
+
- name: Closed for Renovation
|
76
|
+
zoo_id: 16
|
77
|
+
- name: Ducks!
|
78
|
+
zoo_id: 17
|
79
|
+
- name: Retired Animal Life
|
80
|
+
zoo_id: 18
|
81
|
+
- name: The Clean Side of the Gulf
|
82
|
+
zoo_id: 18
|
83
|
+
- name: The Complete History of Rice
|
84
|
+
zoo_id: 19
|
85
|
+
- name: Safari World
|
86
|
+
zoo_id: 19
|
87
|
+
- name: Of Mice & Men
|
88
|
+
zoo_id: 20
|
89
|
+
- name: Snakes!
|
90
|
+
zoo_id: 20
|
@@ -0,0 +1,15 @@
|
|
1
|
+
- name: Sam
|
2
|
+
age: 29
|
3
|
+
occupation: Programmer
|
4
|
+
- name: Amy
|
5
|
+
age: 28
|
6
|
+
occupation: Business Analyst Manager
|
7
|
+
- name: Scott
|
8
|
+
age: 25
|
9
|
+
occupation: Programmer
|
10
|
+
- name: Josh
|
11
|
+
age: 23
|
12
|
+
occupation: Supervisor
|
13
|
+
- name: Bob
|
14
|
+
age: 29
|
15
|
+
occupation: Peon
|
@@ -0,0 +1,20 @@
|
|
1
|
+
- name: Dallas
|
2
|
+
- name: San Diego
|
3
|
+
- name: Miami
|
4
|
+
- name: Ft. Lauderdale
|
5
|
+
- name: Ft. Worth
|
6
|
+
- name: New Orleans
|
7
|
+
- name: San Antonio
|
8
|
+
- name: Galveston
|
9
|
+
- name: Austin
|
10
|
+
- name: Brownsville
|
11
|
+
- name: Los Angeles
|
12
|
+
- name: Phillidelphia
|
13
|
+
- name: New York
|
14
|
+
- name: Seattle
|
15
|
+
- name: Houston
|
16
|
+
- name: Corpus Christi
|
17
|
+
- name: Sacramento
|
18
|
+
- name: Pensacola
|
19
|
+
- name: Santa Francisco
|
20
|
+
- name: Orlando
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# context 'An Exhibit' do
|
2
|
+
#
|
3
|
+
# setup do
|
4
|
+
# @amazonia = Exhibit[:name => 'Amazonia']
|
5
|
+
# end
|
6
|
+
#
|
7
|
+
# specify 'has an animals association' do
|
8
|
+
# [@amazonia, Exhibit.new].each do |exhibit|
|
9
|
+
# exhibit.animals.class.should == DataMapper::Associations::HasAndBelongsToManyAssociation
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# specify 'has many animals' do
|
14
|
+
# @amazonia.animals.size.should == 2
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# specify 'should load associations magically' do
|
18
|
+
# Exhibit.all.each do |exhibit|
|
19
|
+
# exhibit.animals.each do |animal|
|
20
|
+
# animal.exhibits.should.include?(exhibit)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# end
|
data/spec/has_many.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
describe DataMapper::Associations::HasManyAssociation do
|
2
|
+
|
3
|
+
before(:all) do
|
4
|
+
fixtures(:zoos)
|
5
|
+
fixtures(:exhibits)
|
6
|
+
|
7
|
+
@san_diego = Zoo[:name => 'San Diego']
|
8
|
+
@dallas = Zoo[:name => 'Dallas']
|
9
|
+
@miami = Zoo[:name => 'Miami']
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should expose a proxy for the accessor' do
|
13
|
+
[@miami, Zoo.new].each do |z|
|
14
|
+
z.exhibits.class.should == DataMapper::Associations::HasManyAssociation
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should lazily-load the association when Enumerable methods are called' do
|
19
|
+
database do |db|
|
20
|
+
@san_diego.exhibits.size.should == 2
|
21
|
+
@san_diego.exhibits.should include(@san_diego.session.find(Exhibit, :first, :name => 'Monkey Mayhem'))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should eager-load associations for an entire set' do
|
26
|
+
zoos = Zoo.all
|
27
|
+
zoos.each do |zoo|
|
28
|
+
zoo.exhibits.each do |exhibit|
|
29
|
+
exhibit.zoo.should == zoo
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|