dr_nic_magic_models 0.7.1 → 0.8.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.
Files changed (39) hide show
  1. data/Rakefile +9 -9
  2. data/lib/base.rb +10 -95
  3. data/lib/connection_adapters/abstract/schema_statements.rb +0 -0
  4. data/lib/connection_adapters/abstract_adapter.rb +32 -0
  5. data/lib/connection_adapters/mysql_adapter.rb +42 -0
  6. data/lib/connection_adapters/postgresql_adapter.rb +45 -0
  7. data/lib/dr_nic_magic_models/schema.rb +210 -8
  8. data/lib/dr_nic_magic_models/validations.rb +35 -18
  9. data/lib/dr_nic_magic_models/version.rb +2 -2
  10. data/lib/dr_nic_magic_models.rb +13 -2
  11. data/lib/module.rb +0 -18
  12. data/lib/rails.rb +19 -0
  13. data/lib/schema_dumper.rb +0 -0
  14. data/scripts/txt2js +57 -0
  15. data/test/connections/native_postgresql/connection.rb +14 -0
  16. data/test/fixtures/adjectives.yml +3 -0
  17. data/test/fixtures/adjectives_fun_users.yml +3 -0
  18. data/test/fixtures/db_definitions/mysql.drop.sql +4 -30
  19. data/test/fixtures/db_definitions/mysql.sql +30 -2
  20. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  21. data/test/fixtures/db_definitions/postgresql.sql +56 -0
  22. data/test/fixtures/fun_users.yml +8 -1
  23. data/test/fixtures/group_tag.yml +3 -1
  24. data/test/fixtures/groups.yml +9 -1
  25. data/test/foreign_keys_test.rb +0 -0
  26. data/test/fun_user_plus.rb +2 -0
  27. data/test/invisible_model_access_test.rb +35 -7
  28. data/test/invisible_model_assoc_test.rb +34 -19
  29. data/test/invisible_model_classes_test.rb +5 -1
  30. data/website/index.html +24 -4
  31. data/website/index.txt +9 -1
  32. data/website/template.js +3 -0
  33. data/website/template.rhtml +10 -1
  34. data/website/version-raw.js +3 -0
  35. data/website/version-raw.txt +1 -0
  36. data/website/version.js +4 -0
  37. data/website/version.txt +3 -0
  38. metadata +32 -19
  39. data/lib/dr_nic_magic_models/associations.rb +0 -12
data/Rakefile CHANGED
@@ -18,7 +18,7 @@ RUBY_FORGE_PROJECT = "magicmodels"
18
18
  RUBY_FORGE_USER = "nicwilliams"
19
19
 
20
20
  PKG_FILES = FileList[
21
- "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "[A-Z]*", "install.rb", "Rakefile"
21
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "website/**/*", "scripts/**/*", "[A-Z]*", "install.rb", "Rakefile"
22
22
  ].exclude(/\bCVS\b|~$/)
23
23
 
24
24
 
@@ -28,7 +28,7 @@ task :test => [ :test_mysql ]
28
28
 
29
29
  # Run the unit tests
30
30
 
31
- for adapter in %w( mysql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
31
+ for adapter in %w( mysql postgresql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
32
32
  Rake::TestTask.new("test_#{adapter}") { |t|
33
33
  t.libs << "test" << "test/connections/native_#{adapter}"
34
34
  t.pattern = "test/*_test{,_#{adapter}}.rb"
@@ -42,7 +42,9 @@ desc 'Build the MySQL test databases'
42
42
  task :build_mysql_databases do
43
43
  puts File.join(SCHEMA_PATH, 'mysql.sql')
44
44
  %x( mysqladmin -u root create "#{PKG_NAME}_unittest" )
45
- %x( mysql -u root "#{PKG_NAME}_unittest" < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
45
+ cmd = "mysql -u root #{PKG_NAME}_unittest < \"#{File.join(SCHEMA_PATH, 'mysql.sql')}\""
46
+ puts "#{cmd}\n"
47
+ %x( #{cmd} )
46
48
  end
47
49
 
48
50
  desc 'Drop the MySQL test databases'
@@ -55,13 +57,13 @@ task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases]
55
57
 
56
58
  desc 'Build the PostgreSQL test databases'
57
59
  task :build_postgresql_databases do
58
- %x( createdb "#{PKG_NAME}_unittest" )
59
- %x( psql "#{PKG_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} )
60
+ %x( createdb -U postgres "#{PKG_NAME}_unittest" )
61
+ %x( psql "#{PKG_NAME}_unittest" postgres -f "#{File.join(SCHEMA_PATH, 'postgresql.sql')}" )
60
62
  end
61
63
 
62
64
  desc 'Drop the PostgreSQL test databases'
63
65
  task :drop_postgresql_databases do
64
- %x( dropdb "#{PKG_NAME}_unittest" )
66
+ %x( dropdb -U postgres "#{PKG_NAME}_unittest" )
65
67
  end
66
68
 
67
69
  desc 'Rebuild the PostgreSQL test databases'
@@ -98,8 +100,6 @@ spec = Gem::Specification.new do |s|
98
100
  dist_dirs.each do |dir|
99
101
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
100
102
  end
101
-
102
- s.add_dependency('activerecord', '>= 1.14.3' + PKG_BUILD)
103
103
 
104
104
  s.require_path = 'lib'
105
105
  s.autorequire = 'dr_nic_magic_models'
@@ -156,4 +156,4 @@ task :release => [ :package ] do
156
156
  puts release_command
157
157
  system(release_command)
158
158
  end
159
- end
159
+ end
data/lib/base.rb CHANGED
@@ -1,101 +1,16 @@
1
- # Only needs to be an Include module for the generated classes, not for all ARs
1
+ #TODO: Use :dependent for FK cascade?
2
+
2
3
  module ActiveRecord
3
4
  class Base
4
5
  class << self
5
- alias old_new new
6
- def new(*args)
7
- obj = old_new(*args)
8
- unless self.methods.include? 'generate_validations'
9
- self.send(:include, DrNicMagicModels::Validations)
10
- self.generate_validations
11
- end
12
- obj
13
- end
14
- alias old_allocate allocate
15
- def allocate
16
- obj = old_allocate
17
- unless self.methods.include? 'generate_validations'
18
- self.send(:include, DrNicMagicModels::Validations)
19
- self.generate_validations
20
- end
21
- obj
22
- end
23
-
24
- # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
25
- # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
26
- # Invoice.reflect_on_association(:line_items).macro # returns :has_many
27
- def reflect_on_association(association)
28
- unless reflections[association]
29
- # See if an assocation can be generated
30
- self.new.send(association) rescue nil
31
- end
32
- reflections[association].is_a?(ActiveRecord::Reflection::AssociationReflection) ? reflections[association] : nil
33
- end
34
- end
35
-
36
- class_eval do
37
- alias :normal_method_missing :method_missing
38
- def method_missing(method, *args, &block)
39
- if unknown_method? method
40
- result = find_belongs_to method, *args, &block
41
- result = find_has_some method, *args, &block if not result
42
- result = find_has_some_indirect method, *args, &block if not result
43
- return result if result
44
- end
45
- add_known_unknown method
46
- normal_method_missing(method, *args, &block)
47
- end
48
-
49
- def add_known_unknown(method)
50
- @known_unknowns ||= {}
51
- @known_unknowns[method] = true
52
- end
53
-
54
- def unknown_method?(method)
55
- @known_unknowns.nil? or @known_unknowns.include? method
56
- end
57
-
58
- def find_belongs_to(method, *args, &block)
59
- foreign_key = self.class.columns.select {|column| column.name == method.to_s.foreign_key}.first
60
- return add_belongs_to(method, *args, &block) if foreign_key
61
- end
62
6
 
63
- def add_belongs_to(method, *args, &block)
64
- self.class.send 'belongs_to', method
65
- self.send(method, *args, &block)
66
- end
67
-
68
- def find_has_some(method, *args, &block)
69
- klass = Module.const_get method.to_s.downcase.singularize.camelize rescue nil
70
- foreign_key = klass.columns.select {|column| column.name == self.class.name.foreign_key}.first if klass
71
- return add_has_some(method, *args, &block) if foreign_key
72
- end
73
-
74
- def add_has_some(method, *args, &block)
75
- _method = method.to_s
76
- association = _method.singularize == _method ? 'has_one' : 'has_many'
77
- self.class.send association, method
78
- self.send(method, *args, &block)
79
- end
80
-
81
- def find_has_some_indirect(method, *args, &block)
82
- klass = Module.const_get method.to_s.downcase.singularize.camelize rescue return
83
- join_table = nil
84
- self.connection.tables.each do |table|
85
- unless [self.class.table_name, klass.table_name].include? table
86
- columns = self.connection.columns(table).map(&:name)
87
- join_table = table if columns.include?(self.class.to_s.foreign_key) and columns.include?(klass.to_s.foreign_key)
88
- end
89
- break if join_table
90
- end
91
- return add_has_some_through(join_table, method, *args, &block) if join_table
92
- end
93
-
94
- def add_has_some_through(join_table, method, *args, &block)
95
- self.class.send 'has_many', method, :through => join_table.to_sym
96
- self.send(method, *args, &block)
97
- end
98
-
7
+ public
8
+
9
+ def get_unique_index_columns
10
+ self.connection.indexes(self.table_name, "#{self.name} Indexes").select { |index| index.unique && index.columns.size == 1 }.map{ |index| index.columns.first }
11
+ end
99
12
  end
13
+
14
+
100
15
  end
101
- end
16
+ end
@@ -0,0 +1,32 @@
1
+
2
+ module ActiveRecord
3
+ module ConnectionAdapters # :nodoc:
4
+
5
+ # Generic holder for foreign key constraint meta-data from the database schema.
6
+ class ForeignKeyConstraint < Struct.new(:name, :table, :foreign_key, :reference_table, :reference_column, :on_update, :on_delete)
7
+ end
8
+
9
+ class AbstractAdapter
10
+
11
+ # Does this adapter support the ability to fetch foreign key information?
12
+ # Backend specific, as the abstract adapter always returns +false+.
13
+ def supports_fetch_foreign_keys?
14
+ false
15
+ end
16
+
17
+ def foreign_key_constraints(table, name = nil)
18
+ raise NotImplementedError, "foreign_key_constraints is not implemented for #{self.class}"
19
+ end
20
+
21
+ def remove_foreign_key_constraint(table_name, constraint_name)
22
+ raise NotImplementedError, "rename_table is not implemented for #{self.class}"
23
+ end
24
+
25
+ protected
26
+ def symbolize_foreign_key_constraint_action(constraint_action)
27
+ return nil if constraint_action.nil?
28
+ constraint_action.downcase.gsub(/\s/, '_').to_sym
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class MysqlAdapter < AbstractAdapter
6
+ def supports_fetch_foreign_keys?
7
+ true
8
+ end
9
+
10
+ def foreign_key_constraints(table, name = nil)
11
+ constraints = []
12
+ execute("SHOW CREATE TABLE #{table}", name).each do |row|
13
+ row[1].each do |create_line|
14
+ if create_line.strip =~ /CONSTRAINT `([^`]+)` FOREIGN KEY \(`([^`]+)`\) REFERENCES `([^`]+)` \(`([^`]+)`\)([^,]*)/
15
+ constraint = ForeignKeyConstraint.new(Regexp.last_match(1), table, Regexp.last_match(2), Regexp.last_match(3), Regexp.last_match(4), nil, nil)
16
+
17
+ constraint_params = {}
18
+
19
+ unless Regexp.last_match(5).nil?
20
+ Regexp.last_match(5).strip.split('ON ').each do |param|
21
+ constraint_params[Regexp.last_match(1).upcase] = Regexp.last_match(2).strip.upcase if param.strip =~ /([^ ]+) (.+)/
22
+ end
23
+ end
24
+
25
+ constraint.on_update = symbolize_foreign_key_constraint_action(constraint_params['UPDATE']) if constraint_params.include? 'UPDATE'
26
+ constraint.on_delete = symbolize_foreign_key_constraint_action(constraint_params['DELETE']) if constraint_params.include? 'DELETE'
27
+
28
+ constraints << constraint
29
+ end
30
+ end
31
+ end
32
+
33
+ constraints
34
+ end
35
+
36
+ def remove_foreign_key_constraint(table_name, constraint_name)
37
+ execute "ALTER TABLE #{table_name} DROP FOREIGN KEY #{constraint_name}"
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # Foreign Key support from http://wiki.rubyonrails.org/rails/pages/Foreign+Key+Schema+Dumper+Plugin
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class PostgreSQLAdapter < AbstractAdapter
6
+
7
+ def supports_fetch_foreign_keys?
8
+ true
9
+ end
10
+
11
+ def foreign_key_constraints(table, name = nil)
12
+
13
+
14
+ sql = "SELECT conname, pg_catalog.pg_get_constraintdef(oid) AS consrc FROM pg_catalog.pg_constraint WHERE contype='f' "
15
+ sql += "AND conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname='#{table}')"
16
+
17
+ result = query(sql, name)
18
+
19
+ keys = []
20
+ re = /(?i)^FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)(?: ON UPDATE (\w+))?(?: ON DELETE (\w+))?$/
21
+ result.each do |row|
22
+ # pg_catalog.pg_get_constraintdef returns a string like this:
23
+ # FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE
24
+ if match = re.match(row[1])
25
+
26
+ keys << ForeignKeyConstraint.new(row[0],
27
+ table,
28
+ match[1],
29
+ match[2],
30
+ match[3],
31
+ symbolize_foreign_key_constraint_action(match[4]),
32
+ symbolize_foreign_key_constraint_action(match[5]))
33
+ end
34
+ end
35
+
36
+ keys
37
+ end
38
+
39
+ def remove_foreign_key_constraint(table_name, constraint_name)
40
+ execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint_name}"
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -1,23 +1,225 @@
1
1
  module DrNicMagicModels
2
+
2
3
  class Schema
3
4
  class << self
5
+
6
+ # all in lower case please
7
+ ReservedTables = [:schema_info, :sessions]
8
+ @@models = nil
9
+
10
+ def logger
11
+ @@logger ||= DrNicMagicModels::Logger
12
+ end
13
+
4
14
  def models
5
- @@models ||= nil
6
- load_schema if @@models.blank?
15
+ load_schema if @@models.nil?
7
16
  @@models
8
17
  end
9
18
 
10
- def reset
11
- @@models = nil
19
+ def fks_on_table(table_name)
20
+ load_schema if @@models.nil?
21
+ @@fks_by_table[table_name.to_s] || []
12
22
  end
13
23
 
24
+ # active record only support 2 column link tables, otherwise use a model table, has_many and through
25
+ def is_link_table?(table_name)
26
+ load_schema if @@models.nil?
27
+ return @@link_tables[table_name] if ! @@link_tables[table_name].nil?
28
+ column_names = @conn.columns(table_name).map{|x| x.name }
29
+ @@link_tables[table_name] = ! column_names.include?("id") && column_names.length == 2 && column_names.select { |x| x =~ /_id$/ } == column_names
30
+ return @@link_tables[table_name]
31
+ end
32
+
33
+ def link_tables_for_class(klass)
34
+ load_schema if @@models.nil?
35
+ end
36
+
14
37
  def load_schema
15
- if conn = ActiveRecord::Base.connection
16
- @@models = DrNicMagicModels::ModelHash.new
17
- @@models.merge! conn.tables.inject({}) {|model_list,table_name| model_list[ActiveRecord::Base.class_name(table_name)] = table_name; model_list}
38
+
39
+ return if ! @@models.nil?
40
+
41
+ raise "No database connection" if !(@conn = ActiveRecord::Base.connection)
42
+
43
+ @@models = ModelHash.new
44
+ @@tables = Hash.new
45
+ @@fks_by_table = Hash.new
46
+ @@link_tables = Hash.new
47
+
48
+ tables = @conn.tables.sort
49
+
50
+ # Work out which tables are in the model and which aren't
51
+ tables.each do |table_name|
52
+
53
+ # deal with reserved tables & link_tables && other stray id-less tables
54
+ next if ReservedTables.include?(table_name.downcase.to_sym) || is_link_table?(table_name) || ! @conn.columns(table_name).map{ |x| x.name}.include?("id")
55
+
56
+ # a model table then...
57
+ model_class_name = ActiveRecord::Base.class_name(table_name)
58
+
59
+ logger.debug "Got a model table: #{table_name} => class #{model_class_name}"
60
+
61
+ @@models[model_class_name] = table_name
62
+ @@tables[table_name] = model_class_name
63
+
64
+ # create by MAGIC!
65
+ begin
66
+ klass = model_class_name.constantize
67
+ rescue Exception => e
68
+ # create our class!
69
+ class_def = <<-end_eval
70
+ class #{model_class_name} < ActiveRecord::Base
71
+ set_table_name('#{table_name}')
72
+ end
73
+ end_eval
74
+ eval(class_def, TOPLEVEL_BINDING)
75
+ klass = model_class_name.constantize
76
+ end
77
+
78
+ # magic up some validation
79
+ klass.send(:extend, DrNicMagicModels::Validations)
80
+ klass.send(:generate_validations)
81
+
18
82
  end
83
+
84
+ # Process FKs?
85
+ if @conn.supports_fetch_foreign_keys?
86
+
87
+ tables.each do |table_name|
88
+ logger.debug "Getting FKs for #{table_name}"
89
+ @@fks_by_table[table_name] = Array.new
90
+ @conn.foreign_key_constraints(table_name).each do |fk|
91
+ logger.debug "Got one: #{fk}"
92
+ @@fks_by_table[table_name].push(fk)
93
+ end # do each fk
94
+
95
+ end # each table
96
+ end
97
+
98
+ # Try to work out our link tables now...
99
+ @@models.keys.sort.each{|klass| process_table(@@models[klass.to_s])}
100
+ @@link_tables.keys.sort.each{|table_name| process_link_table(table_name) if @@link_tables[table_name]}
101
+
102
+
19
103
  end
20
- end
104
+
105
+ def process_table(table_name)
106
+
107
+ logger.debug "Processing model table #{table_name}"
108
+
109
+ # ok, so let's look at the foreign keys on the table...
110
+ belongs_to_klass = @@tables[table_name].constantize rescue return
111
+
112
+ processed_columns = Hash.new
113
+
114
+ fks_on_table(table_name).each do |fk|
115
+ logger.debug "Found FK column by suffix _id [#{fk.foreign_key}]"
116
+ has_some_klass = Inflector.classify(fk.reference_table).constantize rescue next
117
+ processed_columns[fk.foreign_key] = { :has_some_klass => has_some_klass }
118
+ processed_columns[fk.foreign_key].merge! add_has_some_belongs_to(belongs_to_klass, fk.foreign_key, has_some_klass) rescue next
119
+ end
120
+
121
+ column_names = @conn.columns(table_name).map{ |x| x.name}
122
+ column_names.each do |column_name|
123
+ next if not column_name =~ /_id$/
124
+ logger.debug "Found FK column by suffix _id [#{column_name}]"
125
+ if processed_columns.key?(column_name)
126
+ logger.debug "Skipping, already processed"
127
+ next
128
+ end
129
+ has_some_klass = Inflector.classify(column_name.sub(/_id$/,"")).constantize rescue next
130
+ processed_columns[column_name] = { :has_some_klass => has_some_klass }
131
+ processed_columns[column_name].merge! add_has_some_belongs_to(belongs_to_klass, column_name, has_some_klass) rescue next
132
+ end
133
+
134
+ #TODO: what if same classes in table?
135
+
136
+ # is this a link table with attributes? (has_many through?)
137
+ return if processed_columns.keys.length < 2
138
+
139
+ processed_columns.keys.each do |key1|
140
+ processed_columns.keys.each do |key2|
141
+ next if key1 == key2
142
+ logger.debug "\n*** #{processed_columns[key1][:has_some_class]}.send 'has_many', #{processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym}, :through => #{processed_columns[key2][:has_some_name]}\n\n"
143
+ processed_columns[key1][:has_some_class].send 'has_many', processed_columns[key2][:belongs_to_name].to_s.pluralize.to_sym, :through => processed_columns[key2][:has_some_name].to_sym
144
+ end
145
+ end
146
+
147
+ end
148
+
149
+
150
+ def add_has_some_belongs_to(belongs_to_klass, belongs_to_fk, has_some_klass)
151
+
152
+ logger.debug "Trying to add a #{belongs_to_klass} belongs_to #{has_some_klass}..."
153
+
154
+ # so this is a belongs_to & has_some style relationship...
155
+ # is it a has_many, or a has_one? Well, let's assume a has_one has a unique index on the column please... good db design, haha!
156
+ unique = belongs_to_klass.get_unique_index_columns.include?(belongs_to_fk)
157
+ belongs_to_name = belongs_to_fk.sub(/_id$/, '').to_sym
158
+
159
+ logger.debug "\n*** #{belongs_to_klass}.send 'belongs_to', #{belongs_to_name}, :class_name => #{has_some_klass}, :foreign_key => #{belongs_to_fk}\n"
160
+ belongs_to_klass.send(:belongs_to, belongs_to_name, :class_name => has_some_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
161
+
162
+ # work out if we need a prefix
163
+ has_some_name = ((unique ? belongs_to_klass.table_name.singularize : belongs_to_klass.table_name.pluralize) + (belongs_to_name.to_s == has_some_klass.table_name.singularize ? "" : "_as_"+belongs_to_name.to_s)).downcase.to_sym
164
+ method = unique ? :has_one : :has_many
165
+ logger.debug "\n*** #{has_some_klass}.send(#{method}, #{has_some_name}, :class_name => #{belongs_to_klass.to_s}, :foreign_key => #{belongs_to_fk.to_sym})\n\n"
166
+ has_some_klass.send(method, has_some_name, :class_name => belongs_to_klass.to_s, :foreign_key => belongs_to_fk.to_sym)
167
+
168
+ return { :method => method, :belongs_to_name => belongs_to_name, :has_some_name => has_some_name, :has_some_class => has_some_klass }
169
+
170
+ end
171
+
172
+ def process_link_table(table_name)
173
+
174
+ logger.debug "Processing link table #{table_name}"
175
+
176
+ classes_map = Hash.new
177
+ column_names = @conn.columns(table_name).map{ |x| x.name}
178
+
179
+ # use foreign keys first
180
+ fks_on_table(table_name).each do |fk|
181
+ logger.debug "Processing fk: #{fk}"
182
+ klass = Inflector.classify(fk.reference_table).constantize rescue logger.debug("Cannot find model #{class_name} for table #{fk.reference_table}") && return
183
+ classes_map[fk.foreign_key] = klass
184
+ end
185
+
186
+ logger.debug "Got #{classes_map.keys.length} references from FKs"
187
+
188
+ if classes_map.keys.length < 2
189
+
190
+ #Fall back on good ol _id recognition
191
+
192
+ column_names.each do |column_name|
193
+
194
+ # check we haven't processed by fks already
195
+ next if ! classes_map[column_name].nil?
196
+ referenced_table = column_name.sub(/_id$/, '')
197
+
198
+ begin
199
+ klass = Inflector.classify(referenced_table).constantize
200
+ # fall back on FKs here
201
+ if ! klass.nil?
202
+ classes_map[column_name] = klass
203
+ end
204
+ rescue
205
+ end
206
+ end
207
+ end
208
+
209
+ # not detected the link table?
210
+ logger.debug "Got #{classes_map.keys.length} references"
211
+ logger.debug "Cannot detect both tables referenced in link table" && return if classes_map.keys.length != 2
212
+
213
+ logger.debug "Adding habtm relationship"
214
+
215
+ logger.debug "\n*** #{classes_map[column_names[0]]}.send 'has_and_belongs_to_many', #{column_names[1].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[1]].to_s}, :join_table => #{table_name.to_sym}\n"
216
+ logger.debug "\n*** #{classes_map[column_names[1]]}.send 'has_and_belongs_to_many', #{column_names[0].sub(/_id$/,'').pluralize.to_sym}, :class_name => #{classes_map[column_names[0]].to_s}, :join_table => #{table_name.to_sym}\n\n"
217
+
218
+ classes_map[column_names[0]].send 'has_and_belongs_to_many', column_names[1].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[1]].to_s, :join_table => table_name.to_sym
219
+ classes_map[column_names[1]].send 'has_and_belongs_to_many', column_names[0].sub(/_id$/,'').pluralize.to_sym, :class_name => classes_map[column_names[0]].to_s, :join_table => table_name.to_sym
220
+
221
+ end
222
+ end
21
223
  end
22
224
 
23
225
  class ModelHash < Hash
@@ -1,26 +1,43 @@
1
1
  module DrNicMagicModels
2
2
  module Validations
3
- def self.append_features(base)
4
- super
5
- base.extend(ClassMethods)
6
- base.generate_validations
3
+
4
+ def generate_validations
5
+
6
+ logger = DrNicMagicModels::Logger
7
+
8
+ # Code reworked from http://www.redhillconsulting.com.au/rails_plugins.html
9
+ # Thanks Red Hill Consulting for using an MIT licence :o)
10
+
11
+ # NOT NULL constraints
12
+ self.columns.reject { |column| column.name =~ /(?i)^(((created|updated)_(at|on))|position|type|id)$/ }.each do |column|
7
13
 
8
- # Currently only invoked on generated classes
9
- # How to include and invoke on all ARs? - hook into class loading??
10
- end
11
-
12
- module ClassMethods
13
- def generate_validations
14
- unless @@generated_validations ||= false
15
- @@generated_validations = true
16
- column_names = self.columns.select {|column| !column.null and !column.primary}.map {|column| column.name.to_sym}
17
- add_validation :validates_presence_of, column_names
14
+ if column.type == :integer
15
+ logger.debug "validates_numericality_of #{column.name}, :allow_nil => true, :only_integer => true"
16
+ self.validates_numericality_of column.name, :allow_nil => true, :only_integer => true
17
+ elsif column.number?
18
+ logger.debug "validates_numericality_of #{column.name}, :allow_nil => true"
19
+ self.validates_numericality_of column.name, :allow_nil => true
20
+ elsif column.text? && column.limit
21
+ logger.debug "validates_length_of #{column.name}, :allow_nil => true, :maximum => #{column.limit}"
22
+ self.validates_length_of column.name, :allow_nil => true, :maximum => column.limit
18
23
  end
19
- end
20
24
 
21
- def add_validation(validation, column_names)
22
- self.send validation, *column_names
25
+ # Active record seems to interpolate booleans anyway to either true, false or nil...
26
+ if column.type == :boolean
27
+ logger.debug "validates_inclusion_of #{column.name}, :in => [true, false], :allow_nil => #{column.null}, :message => ActiveRecord::Errors.default_error_messages[:blank]"
28
+ self.validates_inclusion_of column.name, :in => [true, false], :allow_nil => column.null, :message => ActiveRecord::Errors.default_error_messages[:blank]
29
+ elsif !column.null
30
+ logger.debug "validates_presence_of #{column.name}"
31
+ self.validates_presence_of column.name
32
+ end
23
33
  end
24
- end
34
+
35
+ # Single-column UNIQUE indexes
36
+ get_unique_index_columns.each do |col|
37
+ logger.debug "validates_uniqueness_of #{col}"
38
+ self.validates_uniqueness_of col
39
+ end
40
+
41
+ end
25
42
  end
26
43
  end
@@ -1,8 +1,8 @@
1
1
  module DrNicMagicModels #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 7
5
- TINY = 1
4
+ MINOR = 8
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -3,7 +3,6 @@ $:.unshift(File.dirname(__FILE__)) unless
3
3
 
4
4
  unless defined?(ActiveRecord)
5
5
  begin
6
- $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
7
6
  require 'active_record'
8
7
  rescue LoadError
9
8
  require 'rubygems'
@@ -11,7 +10,19 @@ unless defined?(ActiveRecord)
11
10
  end
12
11
  end
13
12
 
13
+ module DrNicMagicModels
14
+ Logger = RAILS_DEFAULT_LOGGER rescue Logger.new(STDERR)
15
+ end
16
+
14
17
  require 'dr_nic_magic_models/schema'
15
18
  require 'dr_nic_magic_models/validations'
16
- require 'module'
17
19
  require 'base'
20
+ require 'rails' rescue nil
21
+ require 'connection_adapters/abstract_adapter'
22
+ require 'connection_adapters/mysql_adapter'
23
+ require 'connection_adapters/postgresql_adapter'
24
+
25
+ # load the schema
26
+ DrNicMagicModels::Schema.load_schema
27
+
28
+
data/lib/module.rb CHANGED
@@ -1,18 +0,0 @@
1
- class Module
2
- alias :normal_const_missing :const_missing
3
-
4
- def const_missing(class_id)
5
- begin
6
- return normal_const_missing(class_id)
7
- rescue
8
- end
9
- unless table_name = DrNicMagicModels::Schema.models[class_id]
10
- raise NameError.new("uninitialized constant #{class_id}") if DrNicMagicModels::Schema.models.enquired? class_id
11
- end
12
- klass_code = lambda {klass_code}
13
- klass = Class.new ActiveRecord::Base, &klass_code
14
- const_set class_id, klass
15
- klass.set_table_name table_name
16
- klass
17
- end
18
- end
data/lib/rails.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Dependencies #:nodoc:#
2
+
3
+ @@models_dir = File.expand_path(File.join(RAILS_ROOT,'app','models'))
4
+
5
+ # don't reload models... it doesn't work anyway, not sure why they haven't done this?
6
+ # submit as patch?
7
+ alias require_or_load_old require_or_load
8
+ def require_or_load(file_name, *args)
9
+ file_name = $1 if file_name =~ /^(.*)\.rb$/
10
+ expanded = File.expand_path(file_name)
11
+ old_mechanism = mechanism
12
+ if expanded =~ /^#{@@models_dir}/
13
+ RAILS_DEFAULT_LOGGER.debug "*** Not reloading #{file_name}"
14
+ Dependencies.mechanism = :require
15
+ end
16
+ require_or_load_old(file_name, *args)
17
+ mechanism = old_mechanism
18
+ end
19
+ end
File without changes