hobofields 0.7.5 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,67 +1,78 @@
1
1
  module HoboFields
2
-
2
+
3
3
  class MigrationGeneratorError < RuntimeError; end
4
-
4
+
5
5
  class MigrationGenerator
6
-
6
+
7
7
  @ignore_models = []
8
8
  @ignore_tables = []
9
-
9
+
10
10
  class << self
11
11
  attr_accessor :ignore_models, :ignore_tables
12
12
  end
13
-
13
+
14
14
  def self.run(renames={})
15
15
  g = MigrationGenerator.new
16
16
  g.renames = renames
17
17
  g.generate
18
18
  end
19
-
19
+
20
20
  def initialize(ambiguity_resolver=nil)
21
21
  @ambiguity_resolver = ambiguity_resolver
22
22
  @drops = []
23
23
  @renames = nil
24
24
  end
25
-
25
+
26
26
  attr_accessor :renames
27
-
27
+
28
+
28
29
  def load_rails_models
29
30
  if defined? RAILS_ROOT
30
- Dir.entries("#{RAILS_ROOT}/app/models/").each do |f|
31
- f =~ /^[a-zA-Z_][a-zA-Z0-9_]*\.rb$/ and f.sub(/.rb$/, '').camelize.constantize
31
+ Dir["#{RAILS_ROOT}/app/models/**/[a-z0-9_]*.rb"].each do |f|
32
+ _, filename = *f.match(%r{/app/models/([_a-z0-9/]*).rb$})
33
+ filename.camelize.constantize
32
34
  end
33
35
  end
34
36
  end
35
-
36
-
37
+
38
+
37
39
  # Returns an array of model classes that *directly* extend
38
40
  # ActiveRecord::Base, excluding anything in the CGI module
39
41
  def table_model_classes
40
42
  load_rails_models
41
43
  ActiveRecord::Base.send(:subclasses).where.descends_from_active_record?.reject {|c| c.name.starts_with?("CGI::") }
42
- end
43
-
44
-
45
- def connection
44
+ end
45
+
46
+
47
+ def self.connection
46
48
  ActiveRecord::Base.connection
47
49
  end
48
-
49
-
50
- def native_types
51
- connection.native_database_types
50
+ def connection; self.class.connection; end
51
+
52
+
53
+ def self.fix_native_types(types)
54
+ case connection.class.name
55
+ when /mysql/i
56
+ types[:integer][:limit] = 11
57
+ end
58
+ types
52
59
  end
53
-
54
-
60
+
61
+ def self.native_types
62
+ @native_types ||= fix_native_types connection.native_database_types
63
+ end
64
+ def native_types; self.class.native_types; end
65
+
55
66
  # Returns an array of model classes and an array of table names
56
67
  # that generation needs to take into account
57
68
  def models_and_tables
58
69
  ignore_model_names = MigrationGenerator.ignore_models.map &it.to_s.underscore
59
70
  models = table_model_classes.select { |m| m < HoboFields::ModelExtensions && m.name.underscore.not_in?(ignore_model_names) }
60
- db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s
71
+ db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s
61
72
  [models, db_tables]
62
73
  end
63
-
64
-
74
+
75
+
65
76
  # return a hash of table renames and modifies the passed arrays so
66
77
  # that renamed tables are no longer listed as to_create or to_drop
67
78
  def extract_table_renames!(to_create, to_drop)
@@ -72,7 +83,7 @@ module HoboFields
72
83
  renames.each_pair do |old_name, new_name|
73
84
  new_name = new_name[:table_name] if new_name.is_a?(Hash)
74
85
  next unless new_name
75
-
86
+
76
87
  if to_create.delete(new_name.to_s) && to_drop.delete(old_name.to_s)
77
88
  to_rename[old_name.to_s] = new_name.to_s
78
89
  else
@@ -80,7 +91,7 @@ module HoboFields
80
91
  end
81
92
  end
82
93
  to_rename
83
-
94
+
84
95
  elsif @ambiguity_resolver
85
96
  @ambiguity_resolver.extract_renames!(to_create, to_drop, "table")
86
97
 
@@ -88,8 +99,8 @@ module HoboFields
88
99
  raise MigrationGeneratorError, "Unable to resolve migration ambiguities"
89
100
  end
90
101
  end
91
-
92
-
102
+
103
+
93
104
  def extract_column_renames!(to_add, to_remove, table_name)
94
105
  if renames
95
106
  to_rename = {}
@@ -106,7 +117,7 @@ module HoboFields
106
117
  end
107
118
  end
108
119
  to_rename
109
-
120
+
110
121
  elsif @ambiguity_resolver
111
122
  @ambiguity_resolver.extract_renames!(to_add, to_remove, "column", "#{table_name}.")
112
123
 
@@ -114,19 +125,26 @@ module HoboFields
114
125
  raise MigrationGeneratorError, "Unable to resolve migration ambiguities in table #{table_name}"
115
126
  end
116
127
  end
117
-
118
128
 
129
+
130
+ def always_ignore_tables
131
+ sessions_table = CGI::Session::ActiveRecordStore::Session.table_name if
132
+ ActionController::Base.session_store == CGI::Session::ActiveRecordStore
133
+ ['schema_info', 'schema_migrations', sessions_table].compact
134
+ end
135
+
136
+
119
137
  def generate
120
138
  models, db_tables = models_and_tables
121
139
  models_by_table_name = models.index_by {|m| m.table_name}
122
140
  model_table_names = models.*.table_name
123
141
 
124
142
  to_create = model_table_names - db_tables
125
- to_drop = db_tables - model_table_names - ['schema_info']
143
+ to_drop = db_tables - model_table_names - always_ignore_tables
126
144
  to_change = model_table_names
127
-
145
+
128
146
  to_rename = extract_table_renames!(to_create, to_drop)
129
-
147
+
130
148
  renames = to_rename.map do |old_name, new_name|
131
149
  "rename_table :#{old_name}, :#{new_name}"
132
150
  end * "\n"
@@ -147,7 +165,7 @@ module HoboFields
147
165
  undo_creates = to_create.map do |t|
148
166
  "drop_table :#{t}"
149
167
  end * "\n"
150
-
168
+
151
169
  changes = []
152
170
  undo_changes = []
153
171
  to_change.each do |t|
@@ -159,7 +177,7 @@ module HoboFields
159
177
  undo_changes << undo
160
178
  end
161
179
  end
162
-
180
+
163
181
  up = [renames, drops, creates, changes].flatten.reject(&:blank?) * "\n\n"
164
182
  down = [undo_changes, undo_renames, undo_drops, undo_creates].flatten.reject(&:blank?) * "\n\n"
165
183
 
@@ -172,19 +190,19 @@ module HoboFields
172
190
  model.field_specs.values.sort_by{|f| f.position}.map {|f| create_field(f, longest_field_name)} +
173
191
  ["end"]) * "\n"
174
192
  end
175
-
193
+
176
194
  def create_field(field_spec, field_name_width)
177
195
  args = [field_spec.name.inspect] + format_options(field_spec.options, field_spec.sql_type)
178
196
  " t.%-*s %s" % [field_name_width, field_spec.sql_type, args.join(', ')]
179
197
  end
180
-
198
+
181
199
  def change_table(model, current_table_name)
182
200
  new_table_name = model.table_name
183
-
201
+
184
202
  db_columns = model.connection.columns(current_table_name).index_by{|c|c.name} - [model.primary_key]
185
203
  model_column_names = model.field_specs.keys.*.to_s
186
204
  db_column_names = db_columns.keys.*.to_s
187
-
205
+
188
206
  to_add = model_column_names - db_column_names
189
207
  to_remove = db_column_names - model_column_names - [model.primary_key.to_sym]
190
208
 
@@ -193,14 +211,14 @@ module HoboFields
193
211
  db_column_names -= to_rename.keys
194
212
  db_column_names |= to_rename.values
195
213
  to_change = db_column_names & model_column_names
196
-
214
+
197
215
  renames = to_rename.map do |old_name, new_name|
198
216
  "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
199
217
  end
200
218
  undo_renames = to_rename.map do |old_name, new_name|
201
219
  "rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
202
220
  end
203
-
221
+
204
222
  to_add = to_add.sort_by {|c| model.field_specs[c].position }
205
223
  adds = to_add.map do |c|
206
224
  spec = model.field_specs[c]
@@ -210,14 +228,14 @@ module HoboFields
210
228
  undo_adds = to_add.map do |c|
211
229
  "remove_column :#{new_table_name}, :#{c}"
212
230
  end
213
-
231
+
214
232
  removes = to_remove.map do |c|
215
233
  "remove_column :#{new_table_name}, :#{c}"
216
234
  end
217
235
  undo_removes = to_remove.map do |c|
218
236
  revert_column(current_table_name, c)
219
237
  end
220
-
238
+
221
239
  old_names = to_rename.invert
222
240
  changes = []
223
241
  undo_changes = []
@@ -227,42 +245,44 @@ module HoboFields
227
245
  spec = model.field_specs[c]
228
246
  if spec.different_to?(col)
229
247
  change_spec = {}
230
- change_spec[:limit] = spec.limit unless spec.limit.nil?
248
+ change_spec[:limit] = spec.limit unless spec.limit.nil? && col.limit.nil?
231
249
  change_spec[:precision] = spec.precision unless spec.precision.nil?
232
250
  change_spec[:scale] = spec.scale unless spec.scale.nil?
233
- change_spec[:null] = false unless spec.null
251
+ change_spec[:null] = spec.null unless spec.null && col.null
234
252
  change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
235
-
236
- changes << "change_column :#{new_table_name}, :#{c}, " +
237
- ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type)).join(", ")
253
+
254
+ changes << "change_column :#{new_table_name}, :#{c}, " +
255
+ ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, true)).join(", ")
238
256
  back = change_column_back(current_table_name, col_name)
239
257
  undo_changes << back unless back.blank?
240
258
  else
241
259
  nil
242
260
  end
243
261
  end.compact
244
-
262
+
245
263
  [(renames + adds + removes + changes) * "\n",
246
264
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n"]
247
265
  end
248
-
249
-
250
- def format_options(options, type)
266
+
267
+
268
+ def format_options(options, type, changing=false)
251
269
  options.map do |k, v|
252
- next if k == :limit && (type == :decimal || v == native_types[type][:limit])
253
- next if k == :null && v == true
254
- "#{k.inspect} => #{v.inspect}"
270
+ unless changing
271
+ next if k == :limit && (type == :decimal || v == native_types[type][:limit])
272
+ next if k == :null && v == true
273
+ end
274
+ "#{k.inspect} => #{v.inspect}"
255
275
  end.compact
256
276
  end
257
-
258
-
277
+
278
+
259
279
  def revert_table(table)
260
280
  res = StringIO.new
261
281
  ActiveRecord::SchemaDumper.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
262
282
  res.string.strip.gsub("\n ", "\n")
263
283
  end
264
-
265
-
284
+
285
+
266
286
  def column_options_from_reverted_table(table, column)
267
287
  revert = revert_table(table)
268
288
  if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
@@ -275,19 +295,19 @@ module HoboFields
275
295
  end
276
296
  [type, options]
277
297
  end
278
-
279
-
298
+
299
+
280
300
  def change_column_back(table, column)
281
301
  type, options = column_options_from_reverted_table(table, column)
282
302
  "change_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
283
303
  end
284
-
304
+
285
305
 
286
306
  def revert_column(table, column)
287
307
  type, options = column_options_from_reverted_table(table, column)
288
308
  "add_column :#{table}, :#{column}, #{type}#{', ' + options.strip if options}"
289
- end
290
-
309
+ end
310
+
291
311
  end
292
-
312
+
293
313
  end
@@ -1,42 +1,43 @@
1
1
  module HoboFields
2
-
2
+
3
3
  ModelExtensions = classy_module do
4
-
5
-
4
+
5
+
6
6
  # attr_types holds the type class for any attribute reader (i.e. getter
7
7
  # method) that returns rich-types
8
8
  inheriting_cattr_reader :attr_types => HashWithIndifferentAccess.new
9
-
9
+ inheriting_cattr_reader :attr_order => []
10
+
10
11
  # field_specs holds FieldSpec objects for every declared
11
12
  # field. Note that attribute readers are created (by ActiveRecord)
12
13
  # for all fields, so there is also an entry for the field in
13
14
  # attr_types. This is redundant but simplifies the implementation
14
15
  # and speeds things up a little.
15
16
  inheriting_cattr_reader :field_specs => HashWithIndifferentAccess.new
16
-
17
-
17
+
18
+
18
19
  def self.inherited(klass)
19
20
  fields do |f|
20
21
  f.field(inheritance_column, :string)
21
22
  end
22
23
  end
23
-
24
+
24
25
 
25
26
  def self.field_specs
26
27
  @field_specs ||= HashWithIndifferentAccess.new
27
28
  end
28
29
 
29
-
30
+
30
31
  private
31
-
32
+
32
33
  # Declares that a virtual field that has a rich type (e.g. created
33
34
  # by attr_accessor :foo, :type => :email_address) should be subject
34
35
  # to validation (note that the rich types know how to validate themselves)
35
36
  def self.validate_virtual_field(*args)
36
37
  validates_each(*args) {|record, field, value| msg = value.validate and record.errors.add(field, msg) if value.respond_to?(:validate) }
37
38
  end
38
-
39
-
39
+
40
+
40
41
  # This adds a ":type => t" option to attr_accessor, where t is
41
42
  # either a class or a symbolic name of a rich type. If this option
42
43
  # is given, the setter will wrap values that are not of the right
@@ -46,7 +47,7 @@ module HoboFields
46
47
  type = options.delete(:type)
47
48
  attrs << options unless options.empty?
48
49
  attr_accessor_without_rich_types(*attrs)
49
-
50
+
50
51
  if type
51
52
  type = HoboFields.to_class(type)
52
53
  attrs.each do |attr|
@@ -60,24 +61,25 @@ module HoboFields
60
61
  end
61
62
  end
62
63
  end
63
-
64
-
64
+
65
+
65
66
  # Extend belongs_to so that it creates a FieldSpec for the foreign key
66
67
  def self.belongs_to_with_field_declarations(name, options={}, &block)
67
- res = belongs_to_without_field_declarations(name, options, &block)
68
- refl = reflections[name.to_sym]
69
- fkey = refl.primary_key_name
70
68
  column_options = {}
71
- column_options[:null] = options[:null] if options.has_key?(:null)
72
- declare_field(fkey, :integer, column_options)
73
- declare_polymorphic_type_field(name, column_options) if refl.options[:polymorphic]
74
- res
69
+ column_options[:null] = options.delete(:null) if options.has_key?(:null)
70
+
71
+ returning belongs_to_without_field_declarations(name, options, &block) do
72
+ refl = reflections[name.to_sym]
73
+ fkey = refl.primary_key_name
74
+ declare_field(fkey.to_sym, :integer, column_options)
75
+ declare_polymorphic_type_field(name, column_options) if refl.options[:polymorphic]
76
+ end
75
77
  end
76
78
  class << self
77
79
  alias_method_chain :belongs_to, :field_declarations
78
80
  end
79
-
80
-
81
+
82
+
81
83
  # Declares the "foo_type" field that accompanies the "foo_id"
82
84
  # field for a polyorphic belongs_to
83
85
  def self.declare_polymorphic_type_field(name, column_options)
@@ -87,8 +89,8 @@ module HoboFields
87
89
  # never_show(type_col)
88
90
  # That needs doing somewhere
89
91
  end
90
-
91
-
92
+
93
+
92
94
  # Declare a rich-type for any attribute (i.e. getter method). This
93
95
  # does not effect the attribute in any way - it just records the
94
96
  # metadata.
@@ -102,14 +104,15 @@ module HoboFields
102
104
  # callback, allowing custom metadata to be added to field
103
105
  # declarations.
104
106
  def self.declare_field(name, type, *args)
105
- options = args.extract_options!
107
+ options = args.extract_options!
106
108
  try.field_added(name, type, args, options)
107
109
  add_validations_for_field(name, type, args, options)
108
110
  declare_attr_type(name, type) unless HoboFields.plain_type?(type)
109
111
  field_specs[name] = FieldSpec.new(self, name, type, options)
112
+ attr_order << name unless name.in?(attr_order)
110
113
  end
111
-
112
-
114
+
115
+
113
116
  # Add field validations according to arguments and options in the
114
117
  # field declaration
115
118
  def self.add_validations_for_field(name, type, args, options)
@@ -124,8 +127,8 @@ module HoboFields
124
127
  end
125
128
  end
126
129
  end
127
-
128
-
130
+
131
+
129
132
  # Extended version of the acts_as_list declaration that
130
133
  # automatically delcares the 'position' field
131
134
  def self.acts_as_list_with_field_declaration(options = {})
@@ -133,7 +136,7 @@ module HoboFields
133
136
  acts_as_list_without_field_declaration(options)
134
137
  end
135
138
 
136
-
139
+
137
140
  # Returns the type (a class) for a given field or association. If
138
141
  # the association is a collection (has_many or habtm) return the
139
142
  # AssociationReflection instead
@@ -141,27 +144,27 @@ module HoboFields
141
144
  if attr_types.nil? && self != self.name.constantize
142
145
  raise RuntimeError, "attr_types called on a stale class object (#{self.name}). Avoid storing persistent refereces to classes"
143
146
  end
144
-
147
+
145
148
  attr_types[name] or
146
-
149
+
147
150
  if (refl = reflections[name.to_sym])
148
- if refl.macro.in?([:has_one, :belongs_to])
151
+ if refl.macro.in?([:has_one, :belongs_to]) && !refl.options[:polymorphic]
149
152
  refl.klass
150
153
  else
151
154
  refl
152
155
  end
153
156
  end or
154
-
157
+
155
158
  (col = column(name.to_s) and HoboFields::PLAIN_TYPES[col.type] || col.klass)
156
159
  end
157
-
158
-
159
- # Return the entry from #columns for the named column
160
+
161
+
162
+ # Return the entry from #columns for the named column
160
163
  def self.column(name)
161
164
  name = name.to_s
162
- columns.find {|c| c.name == name }
165
+ columns.find {|c| c.name == name }
163
166
  end
164
-
167
+
165
168
  class << self
166
169
  alias_method_chain :acts_as_list, :field_declaration if defined?(ActiveRecord::Acts::List)
167
170
  alias_method_chain :attr_accessor, :rich_types
@@ -1,12 +1,12 @@
1
1
  module HoboFields
2
-
2
+
3
3
  class PasswordString < String
4
-
4
+
5
5
  COLUMN_TYPE = :string
6
-
6
+
7
7
  HoboFields.register_type(:password, self)
8
-
9
- def to_html
8
+
9
+ def to_html(xmldoctype = true)
10
10
  "[password hidden]"
11
11
  end
12
12
 
@@ -1,17 +1,17 @@
1
1
  module HoboFields
2
-
2
+
3
3
  class Text < String
4
-
4
+
5
5
  HTML_ESCAPE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
6
-
6
+
7
7
  COLUMN_TYPE = :text
8
-
9
- def to_html
10
- gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }.gsub("\n", "<br />\n")
8
+
9
+ def to_html(xmldoctype = true)
10
+ gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }.gsub("\n", "<br#{xmldoctype ? ' /' : ''}>\n")
11
11
  end
12
-
12
+
13
13
  HoboFields.register_type(:text, self)
14
-
14
+
15
15
  end
16
-
16
+
17
17
  end
@@ -1,10 +1,10 @@
1
1
  require 'redcloth'
2
2
 
3
3
  module HoboFields
4
-
4
+
5
5
  class TextileString < HoboFields::Text
6
6
 
7
- def to_html
7
+ def to_html(xmldoctype = true)
8
8
  if blank?
9
9
  ""
10
10
  else
@@ -12,18 +12,9 @@ module HoboFields
12
12
  textilized.hard_breaks = true if textilized.respond_to?("hard_breaks=")
13
13
  textilized.to_html
14
14
  end
15
- end
15
+ end
16
16
 
17
17
  HoboFields.register_type(:textile, self)
18
18
  end
19
19
 
20
20
  end
21
-
22
- class RedCloth
23
- # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
24
- # http://code.whytheluckystiff.net/redcloth/changeset/128
25
- def hard_break( text )
26
- text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks && RedCloth::VERSION == "3.0.4"
27
- end
28
- end
29
-