hobofields 0.7.5 → 0.8

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.
@@ -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
-