hobofields 0.9.0 → 0.9.100

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
  require 'activerecord'
3
3
  ActiveRecord::ActiveRecordError # hack for https://rails.lighthouseapp.com/projects/8994/tickets/2577-when-using-activerecordassociations-outside-of-rails-a-nameerror-is-thrown
4
4
 
5
- RUBY = ENV['RUBY'] || (defined?(JRUBY_VERSION) ? 'jruby' : 'ruby')
5
+ RUBY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']).sub(/.*\s.*/m, '"\&"')
6
6
  RUBYDOCTEST = ENV['RUBYDOCTEST'] || "#{RUBY} `which rubydoctest`"
7
7
 
8
8
  $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobofields/lib')
@@ -24,23 +24,19 @@ namespace "test" do
24
24
  end
25
25
  end
26
26
 
27
- begin
28
- require 'jeweler'
29
- Jeweler::Tasks.new do |gemspec|
30
- gemspec.version = HoboFields::VERSION
31
- gemspec.name = "hobofields"
32
- gemspec.email = "tom@tomlocke.com"
33
- gemspec.summary = "Rich field types and migration generator for Rails"
34
- gemspec.homepage = "http://hobocentral.net/"
35
- gemspec.authors = ["Tom Locke"]
36
- gemspec.rubyforge_project = "hobo"
37
- gemspec.add_dependency("rails", [">= 2.2.2"])
38
- gemspec.add_dependency("hobosupport", ["= #{HoboFields::VERSION}"])
39
- end
40
- Jeweler::GemcutterTasks.new
41
- Jeweler::RubyforgeTasks.new do |rubyforge|
42
- rubyforge.doc_task = false
43
- end
44
- rescue LoadError
45
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
27
+ require 'jeweler'
28
+ Jeweler::Tasks.new do |gemspec|
29
+ gemspec.version = HoboFields::VERSION
30
+ gemspec.name = "hobofields"
31
+ gemspec.email = "tom@tomlocke.com"
32
+ gemspec.summary = "Rich field types and migration generator for Rails"
33
+ gemspec.homepage = "http://hobocentral.net/"
34
+ gemspec.authors = ["Tom Locke"]
35
+ gemspec.rubyforge_project = "hobo"
36
+ gemspec.add_dependency("rails", [">= 2.2.2"])
37
+ gemspec.add_dependency("hobosupport", ["= #{HoboFields::VERSION}"])
38
+ end
39
+ Jeweler::GemcutterTasks.new
40
+ Jeweler::RubyforgeTasks.new do |rubyforge|
41
+ rubyforge.doc_task = false
46
42
  end
data/lib/hobo_fields.rb CHANGED
@@ -9,7 +9,7 @@ end
9
9
 
10
10
  module HoboFields
11
11
 
12
- VERSION = "0.9.0"
12
+ VERSION = "0.9.100"
13
13
 
14
14
  extend self
15
15
 
@@ -60,7 +60,7 @@ module HoboFields
60
60
 
61
61
 
62
62
  def to_name(type)
63
- field_types.index(type) || ALIAS_TYPES[type]
63
+ field_types.key(type) || ALIAS_TYPES[type]
64
64
  end
65
65
 
66
66
 
@@ -100,6 +100,15 @@ module HoboFields
100
100
  # Add the fields do declaration to ActiveRecord::Base
101
101
  ActiveRecord::Base.send(:include, HoboFields::FieldsDeclaration)
102
102
 
103
+ # automatically load other rich types from app/rich_types/*.rb
104
+ # don't assume we're in a Rails app
105
+ if defined?(::Rails)
106
+ Dir[File.join(::Rails.root, 'app', 'rich_types', '*.rb')].each do |f|
107
+ # TODO: should we complain if field_types doesn't get a new value? Might be useful to warn people if they're missing a register_type
108
+ require f
109
+ end
110
+ end
111
+
103
112
  # Monkey patch ActiveRecord so that the attribute read & write methods
104
113
  # automatically wrap richly-typed fields.
105
114
  ActiveRecord::AttributeMethods::ClassMethods.class_eval do
@@ -19,16 +19,40 @@ module HoboFields
19
19
 
20
20
  def with_values(*values)
21
21
  @values = values.*.to_s
22
+ @translated_values = Hash.new do |hash, value|
23
+ if name.blank? || value.blank?
24
+ hash[value] = value
25
+ else
26
+ hash[value] = I18n.t("#{name.tableize}.#{value}", :default => value)
27
+ @detranslated_values[hash[value]] = value
28
+ hash[value]
29
+ end
30
+ end
31
+
32
+ @detranslated_values = Hash.new do |hash, value|
33
+ if name.blank?
34
+ hash[value] = value
35
+ else
36
+ hash[value] = @values.detect(proc { value } ) {|v|
37
+ @translated_values[v]==value
38
+ }
39
+ end
40
+ end
22
41
  end
23
42
 
24
43
  attr_accessor :values
25
44
 
45
+ attr_accessor :translated_values
46
+
47
+ attr_accessor :detranslated_values
48
+
26
49
  def for(*values)
50
+ options = values.extract_options!
27
51
  values = values.*.to_s
28
52
  c = Class.new(EnumString) do
29
53
  values.each do |v|
30
54
  const_name = v.upcase.gsub(/[^a-z0-9_]/i, '_').gsub(/_+/, '_')
31
- const_name = "V" + const_name if const_name =~ /^[0-9_]/
55
+ const_name = "V" + const_name if const_name =~ /^[0-9_]/ || const_name.blank?
32
56
  const_set(const_name, self.new(v)) unless const_defined?(const_name)
33
57
 
34
58
  method_name = "is_#{v.underscore}?"
@@ -36,6 +60,7 @@ module HoboFields
36
60
  end
37
61
  end
38
62
  c.with_values(*values)
63
+ c.set_name(options[:name]) if options[:name]
39
64
  c
40
65
  end
41
66
 
@@ -44,12 +69,28 @@ module HoboFields
44
69
  end
45
70
  alias_method :to_s, :inspect
46
71
 
72
+ def set_name(name)
73
+ @name = name
74
+ end
75
+
76
+ def name
77
+ @name || super
78
+ end
79
+
47
80
  end
48
81
 
49
82
  COLUMN_TYPE = :string
50
83
 
84
+ def initialize(value)
85
+ super(self.class.detranslated_values.nil? ? value : self.class.detranslated_values[value.to_s])
86
+ end
87
+
51
88
  def validate
52
- "must be one of #{self.class.values * ', '}" unless self.in?(self.class.values)
89
+ "must be one of #{self.class.values.map{|v| v.blank? ? '\'\'' : v} * ', '}" unless self.in?(self.class.values)
90
+ end
91
+
92
+ def to_html(xmldoctype = true)
93
+ self.class.translated_values[self]
53
94
  end
54
95
 
55
96
  def ==(other)
@@ -9,8 +9,9 @@ module HoboFields
9
9
  self.model = model
10
10
  self.name = name.to_sym
11
11
  self.type = type.is_a?(String) ? type.to_sym : type
12
+ position = options.delete(:position)
12
13
  self.options = options
13
- self.position = model.field_specs.length
14
+ self.position = position || model.field_specs.length
14
15
  end
15
16
 
16
17
  attr_accessor :model, :name, :type, :position, :options
@@ -2,13 +2,15 @@ module HoboFields
2
2
 
3
3
  FieldsDeclaration = classy_module do
4
4
 
5
- def self.fields(&b)
5
+ def self.fields(include_in_migration = true, &b)
6
6
  # Any model that calls 'fields' gets a bunch of other
7
- # functionality included automatically, but make sure we only include it once
7
+ # functionality included automatically, but make sure we only
8
+ # include it once
8
9
  include HoboFields::ModelExtensions unless HoboFields::ModelExtensions.in?(included_modules)
10
+ @include_in_migration ||= include_in_migration
9
11
 
10
12
  if b
11
- dsl = FieldDeclarationDsl.new(self)
13
+ dsl = HoboFields::FieldDeclarationDsl.new(self)
12
14
  if b.arity == 1
13
15
  yield dsl
14
16
  else
@@ -16,8 +16,8 @@ module HoboFields
16
16
  def self.for_model(model, old_table_name=nil)
17
17
  t = old_table_name || model.table_name
18
18
  model.connection.indexes(t).map do |i|
19
- self.new(model, i.columns, :name => i.name, :unique => i.unique, :table_name => old_table_name)
20
- end
19
+ self.new(model, i.columns, :name => i.name, :unique => i.unique, :table_name => old_table_name) unless model.ignore_indexes.include?(i.name)
20
+ end.compact
21
21
  end
22
22
 
23
23
  def default_name?
@@ -0,0 +1,14 @@
1
+ module HoboFields
2
+ class LifecycleState < String
3
+
4
+ COLUMN_TYPE = :string
5
+
6
+ class << self
7
+ attr_accessor :table_name
8
+ end
9
+
10
+ def to_html(xmldoctype = true)
11
+ I18n.t("#{self.class.table_name}.states.#{self}", :default => self)
12
+ end
13
+ end
14
+ end
@@ -2,6 +2,42 @@ module HoboFields
2
2
 
3
3
  class MigrationGeneratorError < RuntimeError; end
4
4
 
5
+ class HabtmModelShim < Struct.new(:join_table, :foreign_keys, :connection)
6
+
7
+ def self.from_reflection(refl)
8
+ result = self.new
9
+ result.join_table = refl.options[:join_table].to_s
10
+ result.foreign_keys = [refl.primary_key_name.to_s, refl.association_foreign_key.to_s].sort
11
+ # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
12
+ # figure that anybody who sets THAT up can deal with their own migrations...
13
+ result.connection = refl.active_record.connection
14
+ result
15
+ end
16
+
17
+ def table_name
18
+ self.join_table
19
+ end
20
+
21
+ def field_specs
22
+ i = 0
23
+ foreign_keys.inject({}) do |h, v|
24
+ # some trickery to avoid an infinite loop when FieldSpec#initialize tries to call model.field_specs
25
+ h[v] = FieldSpec.new(self, v, :integer, :position => i)
26
+ i += 1
27
+ h
28
+ end
29
+ end
30
+
31
+ def primary_key
32
+ false
33
+ end
34
+
35
+ def index_specs
36
+ []
37
+ end
38
+
39
+ end
40
+
5
41
  class MigrationGenerator
6
42
 
7
43
  @ignore_models = []
@@ -40,7 +76,7 @@ module HoboFields
40
76
  # ActiveRecord::Base, excluding anything in the CGI module
41
77
  def table_model_classes
42
78
  load_rails_models
43
- ActiveRecord::Base.send(:subclasses).where.descends_from_active_record?.reject {|c| c.name.starts_with?("CGI::") }
79
+ ActiveRecord::Base.send(:subclasses).reject {|c| (c.base_class != c) || c.name.starts_with?("CGI::") }
44
80
  end
45
81
 
46
82
 
@@ -65,19 +101,23 @@ module HoboFields
65
101
 
66
102
  # list habtm join tables
67
103
  def habtm_tables
104
+ reflections = Hash.new { |h, k| h[k] = Array.new }
68
105
  ActiveRecord::Base.send(:subclasses).map do |c|
69
- c.reflect_on_all_associations(:has_and_belongs_to_many).map { |a| a.options[:join_table] }
70
- end.flatten.compact.*.to_s
106
+ c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
107
+ reflections[a.options[:join_table].to_s] << a
108
+ end
109
+ end
110
+ reflections
71
111
  end
72
112
 
73
113
  # Returns an array of model classes and an array of table names
74
114
  # that generation needs to take into account
75
115
  def models_and_tables
76
- ignore_model_names = MigrationGenerator.ignore_models.map &it.to_s.underscore
116
+ ignore_model_names = MigrationGenerator.ignore_models.*.to_s.*.underscore
77
117
  all_models = table_model_classes
78
- hobo_models = all_models.select { |m| m < HoboFields::ModelExtensions && m.name.underscore.not_in?(ignore_model_names) }
118
+ hobo_models = all_models.select { |m| m.try.include_in_migration && m.name.underscore.not_in?(ignore_model_names) }
79
119
  non_hobo_models = all_models - hobo_models
80
- db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s - non_hobo_models.*.table_name - habtm_tables
120
+ db_tables = connection.tables - MigrationGenerator.ignore_tables.*.to_s - non_hobo_models.*.table_name
81
121
  [hobo_models, db_tables]
82
122
  end
83
123
 
@@ -134,8 +174,8 @@ module HoboFields
134
174
  raise MigrationGeneratorError, "Unable to resolve migration ambiguities in table #{table_name}"
135
175
  end
136
176
  end
137
-
138
-
177
+
178
+
139
179
  def always_ignore_tables
140
180
  # TODO: figure out how to do this in a sane way and be compatible with 2.2 and 2.3 - class has moved
141
181
  sessions_table = CGI::Session::ActiveRecordStore::Session.table_name if
@@ -158,6 +198,10 @@ module HoboFields
158
198
  models_by_table_name[m.table_name] = m
159
199
  end
160
200
  end
201
+ # generate shims for HABTM models
202
+ habtm_tables.each do |name, refls|
203
+ models_by_table_name[name] = HabtmModelShim.from_reflection(refls.first)
204
+ end
161
205
  model_table_names = models_by_table_name.keys
162
206
 
163
207
  to_create = model_table_names - db_tables
@@ -189,25 +233,35 @@ module HoboFields
189
233
 
190
234
  changes = []
191
235
  undo_changes = []
236
+ index_changes = []
237
+ undo_index_changes = []
192
238
  to_change.each do |t|
193
239
  model = models_by_table_name[t]
194
- table = to_rename.index(t) || model.table_name
240
+ table = to_rename.key(t) || model.table_name
195
241
  if table.in?(db_tables)
196
- change, undo = change_table(model, table)
242
+ change, undo, index_change, undo_index = change_table(model, table)
197
243
  changes << change
198
244
  undo_changes << undo
245
+ index_changes << index_change
246
+ undo_index_changes << undo_index
199
247
  end
200
248
  end
201
249
 
202
- up = [renames, drops, creates, changes].flatten.reject(&:blank?) * "\n\n"
203
- down = [undo_changes, undo_renames, undo_drops, undo_creates].flatten.reject(&:blank?) * "\n\n"
250
+ up = [renames, drops, creates, changes, index_changes].flatten.reject(&:blank?) * "\n\n"
251
+ down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes].flatten.reject(&:blank?) * "\n\n"
204
252
 
205
253
  [up, down]
206
254
  end
207
255
 
208
256
  def create_table(model)
209
257
  longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
210
- primary_key_option = ", :primary_key => :#{model.primary_key}" if model.primary_key != "id"
258
+ if model.primary_key != "id"
259
+ if model.primary_key
260
+ primary_key_option = ", :primary_key => :#{model.primary_key}"
261
+ else
262
+ primary_key_option = ", :id => false"
263
+ end
264
+ end
211
265
  (["create_table :#{model.table_name}#{primary_key_option} do |t|"] +
212
266
  model.field_specs.values.sort_by{|f| f.position}.map {|f| create_field(f, longest_field_name)} +
213
267
  ["end"] + (MigrationGenerator.disable_indexing ? [] : create_indexes(model))) * "\n"
@@ -226,15 +280,16 @@ module HoboFields
226
280
  new_table_name = model.table_name
227
281
 
228
282
  db_columns = model.connection.columns(current_table_name).index_by{|c|c.name}
229
- key_missing = db_columns[model.primary_key].nil?
283
+ key_missing = db_columns[model.primary_key].nil? && model.primary_key
230
284
  db_columns -= [model.primary_key]
231
285
 
232
286
  model_column_names = model.field_specs.keys.*.to_s
233
287
  db_column_names = db_columns.keys.*.to_s
234
288
 
235
289
  to_add = model_column_names - db_column_names
236
- to_add += [model.primary_key] if key_missing
237
- to_remove = db_column_names - model_column_names - [model.primary_key.to_sym]
290
+ to_add += [model.primary_key] if key_missing && model.primary_key
291
+ to_remove = db_column_names - model_column_names
292
+ to_remove = to_remove - [model.primary_key.to_sym] if model.primary_key
238
293
 
239
294
  to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
240
295
 
@@ -292,12 +347,14 @@ module HoboFields
292
347
 
293
348
  index_changes, undo_index_changes = change_indexes(model, current_table_name)
294
349
 
295
- [(renames + adds + removes + changes + index_changes) * "\n",
296
- (undo_renames + undo_adds + undo_removes + undo_changes + undo_index_changes) * "\n"]
350
+ [(renames + adds + removes + changes) * "\n",
351
+ (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
352
+ index_changes * "\n",
353
+ undo_index_changes * "\n"]
297
354
  end
298
355
 
299
356
  def change_indexes(model, old_table_name)
300
- return [[],[]] if MigrationGenerator.disable_indexing
357
+ return [[],[]] if MigrationGenerator.disable_indexing || model.is_a?(HabtmModelShim)
301
358
  new_table_name = model.table_name
302
359
  existing_indexes = IndexSpec.for_model(model, old_table_name)
303
360
  model_indexes = model.index_specs
@@ -2,6 +2,9 @@ module HoboFields
2
2
 
3
3
  ModelExtensions = classy_module do
4
4
 
5
+ # ignore the model in the migration until somebody sets
6
+ # @include_in_migration via the fields declaration
7
+ inheriting_cattr_reader :include_in_migration => false
5
8
 
6
9
  # attr_types holds the type class for any attribute reader (i.e. getter
7
10
  # method) that returns rich-types
@@ -17,19 +20,26 @@ module HoboFields
17
20
 
18
21
  # index_specs holds IndexSpec objects for all the declared indexes.
19
22
  inheriting_cattr_reader :index_specs => []
23
+ inheriting_cattr_reader :ignore_indexes => []
20
24
 
21
25
  def self.inherited(klass)
22
26
  fields do |f|
23
27
  f.field(inheritance_column, :string)
24
28
  end
25
- index(inheritance_column) unless index_specs.*.fields.include?([inheritance_column])
29
+ index(inheritance_column)
26
30
  super
27
31
  end
28
32
 
29
33
  def self.index(fields, options = {})
30
- index_specs << IndexSpec.new(self, fields, options)
34
+ # don't double-index fields
35
+ index_specs << HoboFields::IndexSpec.new(self, fields, options) unless index_specs.*.fields.include?(Array.wrap(fields).*.to_s)
31
36
  end
32
37
 
38
+ # tell the migration generator to ignore the named index. Useful for existing indexes, or for indexes
39
+ # that can't be automatically generated (for example: an prefix index in MySQL)
40
+ def self.ignore_index(index_name)
41
+ ignore_indexes << index_name.to_s
42
+ end
33
43
 
34
44
  private
35
45
 
@@ -49,7 +59,9 @@ module HoboFields
49
59
  options = attrs.extract_options!
50
60
  type = options.delete(:type)
51
61
  attrs << options unless options.empty?
62
+ public
52
63
  attr_accessor_without_rich_types(*attrs)
64
+ private
53
65
 
54
66
  if type
55
67
  type = HoboFields.to_class(type)
@@ -124,7 +136,7 @@ module HoboFields
124
136
  add_validations_for_field(name, type, args)
125
137
  add_index_for_field(name, args, options)
126
138
  declare_attr_type(name, type, options) unless HoboFields.plain_type?(type)
127
- field_specs[name] = FieldSpec.new(self, name, type, options)
139
+ field_specs[name] = HoboFields::FieldSpec.new(self, name, type, options)
128
140
  attr_order << name unless name.in?(attr_order)
129
141
  end
130
142
 
@@ -136,7 +148,7 @@ module HoboFields
136
148
  validates_uniqueness_of name, :allow_nil => !:required.in?(args) if :unique.in?(args)
137
149
 
138
150
  type_class = HoboFields.to_class(type)
139
- if type_class && "validate".in?(type_class.public_instance_methods)
151
+ if type_class && type_class.public_method_defined?("validate")
140
152
  self.validate do |record|
141
153
  v = record.send(name)._?.validate
142
154
  record.errors.add(name, v) if v.is_a?(String)
@@ -53,7 +53,9 @@ class HoboMigrationGenerator < Rails::Generator::Base
53
53
  at_exit { rake_migrate } if action == 'm'
54
54
 
55
55
  up.gsub!("\n", "\n ")
56
+ up.gsub!(/ +\n/, "\n")
56
57
  down.gsub!("\n", "\n ")
58
+ down.gsub!(/ +\n/, "\n")
57
59
 
58
60
  record do |m|
59
61
  m.migration_template 'migration.rb', 'db/migrate',
@@ -73,27 +75,40 @@ class HoboMigrationGenerator < Rails::Generator::Base
73
75
  to_rename = {}
74
76
  rename_to_choices = to_create
75
77
  to_drop.dup.each do |t|
76
- if rename_to_choices.empty?
77
- puts "\nCONFIRM DROP! #{kind_str} #{name_prefix}#{t}"
78
- resp = input("Enter 'drop #{t}' to confirm:")
79
- if resp.strip != "drop " + t.to_s
80
- to_drop.delete(t)
81
- end
82
- else
83
- puts "\nDROP or RENAME?: #{kind_str} #{name_prefix}#{t}"
84
- puts "Rename choices: #{to_create * ', '}"
85
- resp = input("Enter either 'drop #{t}' or one of the rename choices:")
86
- resp.strip!
87
-
88
- if resp == "drop " + t
89
- # Leave things as they are
78
+ while true
79
+ if rename_to_choices.empty?
80
+ puts "\nCONFIRM DROP! #{kind_str} #{name_prefix}#{t}"
81
+ resp = input("Enter 'drop #{t}' to confirm or press enter to keep:")
82
+ if resp.strip == "drop " + t.to_s
83
+ break
84
+ elsif resp.strip.empty?
85
+ to_drop.delete(t)
86
+ break
87
+ else
88
+ next
89
+ end
90
90
  else
91
- resp.gsub!(' ', '_')
92
- to_drop.delete(t)
93
- if resp.in?(rename_to_choices)
94
- to_rename[t] = resp
95
- to_create.delete(resp)
96
- rename_to_choices.delete(resp)
91
+ puts "\nDROP, RENAME or KEEP?: #{kind_str} #{name_prefix}#{t}"
92
+ puts "Rename choices: #{to_create * ', '}"
93
+ resp = input("Enter either 'drop #{t}' or one of the rename choices or press enter to keep:")
94
+ resp.strip!
95
+
96
+ if resp == "drop " + t
97
+ # Leave things as they are
98
+ break
99
+ else
100
+ resp.gsub!(' ', '_')
101
+ to_drop.delete(t)
102
+ if resp.in?(rename_to_choices)
103
+ to_rename[t] = resp
104
+ to_create.delete(resp)
105
+ rename_to_choices.delete(resp)
106
+ break
107
+ elsif resp.empty?
108
+ break
109
+ else
110
+ next
111
+ end
97
112
  end
98
113
  end
99
114
  end
@@ -132,7 +147,7 @@ class HoboMigrationGenerator < Rails::Generator::Base
132
147
  opt.separator 'Options:'
133
148
 
134
149
  opt.on("--force-drop", "Don't prompt with 'drop or rename' - just drop everything") { |v| options[:force_drop] = true }
135
- opt.on("--default-name", "Dont' prompt for a migration name - just pick one") { |v| options[:default_name] = true }
150
+ opt.on("--default-name", "Don't prompt for a migration name - just pick one") { |v| options[:default_name] = true }
136
151
  opt.on("--generate", "Don't prompt for action - generate the migration") { |v| options[:action] = 'g' }
137
152
  opt.on("--migrate", "Don't prompt for action - generate and migrate") { |v| options[:action] = 'm' }
138
153
  end
@@ -70,6 +70,9 @@ The migration generator only takes into account classes that use HoboFields, i.e
70
70
  >> HoboFields::MigrationGenerator.run
71
71
  => ["", ""]
72
72
 
73
+ You can also tell HoboFields to ignore additional tables. You can place this command in your environment.rb or elsewhere:
74
+
75
+ >> HoboFields::MigrationGenerator.ignore_tables = ["green_fishes"]
73
76
 
74
77
  ### Create the table
75
78
 
@@ -266,10 +269,12 @@ foreign-key field. It also generates an index on the field.
266
269
  >> up
267
270
  =>
268
271
  "add_column :adverts, :category_id, :integer
272
+
269
273
  add_index :adverts, [:category_id]"
270
274
  >> down
271
275
  =>
272
276
  "remove_column :adverts, :category_id
277
+
273
278
  remove_index :adverts, :name => :index_adverts_on_category_id"
274
279
 
275
280
  Cleanup:
@@ -289,6 +294,7 @@ If you specify a custom foreign key, the migration generator observes that:
289
294
  >> up
290
295
  =>
291
296
  "add_column :adverts, :c_id, :integer
297
+
292
298
  add_index :adverts, [:c_id]"
293
299
 
294
300
  Cleanup:
@@ -325,6 +331,7 @@ You can specify the index name with :index
325
331
  >> up
326
332
  =>
327
333
  "add_column :adverts, :category_id, :integer
334
+
328
335
  add_index :adverts, [:category_id], :name => 'my_index'"
329
336
 
330
337
  Cleanup:
@@ -373,7 +380,7 @@ You can add an index to a field definition
373
380
  end
374
381
  end
375
382
  >> up, down = HoboFields::MigrationGenerator.run
376
- >> up.split("\n")[1]
383
+ >> up.split("\n")[2]
377
384
  => 'add_index :adverts, [:title]'
378
385
 
379
386
  Cleanup:
@@ -391,7 +398,7 @@ You can ask for a unique index
391
398
  end
392
399
  end
393
400
  >> up, down = HoboFields::MigrationGenerator.run
394
- >> up.split("\n")[1]
401
+ >> up.split("\n")[2]
395
402
  => 'add_index :adverts, [:title], :unique => true'
396
403
 
397
404
  Cleanup:
@@ -409,7 +416,7 @@ You can specify the name for the index
409
416
  end
410
417
  end
411
418
  >> up, down = HoboFields::MigrationGenerator.run
412
- >> up.split("\n")[1]
419
+ >> up.split("\n")[2]
413
420
  => "add_index :adverts, [:title], :name => 'my_index'"
414
421
 
415
422
  Cleanup:
@@ -425,7 +432,7 @@ You can ask for an index outside of the fields block
425
432
  index :title
426
433
  end
427
434
  >> up, down = HoboFields::MigrationGenerator.run
428
- >> up.split("\n")[1]
435
+ >> up.split("\n")[2]
429
436
  => "add_index :adverts, [:title]"
430
437
 
431
438
  Cleanup:
@@ -441,7 +448,7 @@ The available options for the index function are `:unique` and `:name`
441
448
  index :title, :unique => true, :name => 'my_index'
442
449
  end
443
450
  >> up, down = HoboFields::MigrationGenerator.run
444
- >> up.split("\n")[1]
451
+ >> up.split("\n")[2]
445
452
  => "add_index :adverts, [:title], :unique => true, :name => 'my_index'"
446
453
 
447
454
  Cleanup:
@@ -457,7 +464,7 @@ You can create an index on more than one field
457
464
  index [:title, :category_id]
458
465
  end
459
466
  >> up, down = HoboFields::MigrationGenerator.run
460
- >> up.split("\n")[1]
467
+ >> up.split("\n")[2]
461
468
  => "add_index :adverts, [:title, :category_id]"
462
469
 
463
470
  Cleanup:
@@ -466,6 +473,8 @@ Cleanup:
466
473
  >> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
467
474
  {.hidden}
468
475
 
476
+ Finally, you can specify that the migration generator should completely ignore an index by passing its name to ignore_index in the model. This is helpful for preserving indices that can't be automatically generated, such as prefix indices in MySQL.
477
+
469
478
  ### Rename a table
470
479
 
471
480
  The migration generator respects the `set_table_name` declaration, although as before, we need to explicitly tell the generator that we want a rename rather than a create and a drop.
@@ -556,10 +565,12 @@ Adding a subclass or two should introduce the 'type' column and no other changes
556
565
  >> up
557
566
  =>
558
567
  "add_column :adverts, :type, :string
568
+
559
569
  add_index :adverts, [:type]"
560
570
  >> down
561
571
  =>
562
572
  "remove_column :adverts, :type
573
+
563
574
  remove_index :adverts, :name => :index_adverts_on_type"
564
575
 
565
576
  Cleanup
@@ -1,6 +1,6 @@
1
1
  # HoboFields -- Rich Types
2
2
 
3
- This doctest describes the rich types bundles with HoboFields, and the process by which you can create and register your own types.
3
+ This doctest describes the rich types bundled with HoboFields, and the process by which you can create and register your own types.
4
4
 
5
5
  Our test requires rails:
6
6
  {.hidden}
@@ -41,7 +41,6 @@ Finally we can require hobofields:
41
41
  >> require 'hobosupport'
42
42
  >> require 'hobofields'
43
43
 
44
-
45
44
  ## `to_html` method
46
45
 
47
46
  The rich types provide a `to_html` method. If you are using the full Hobo stack you don't need to be aware of this unless you're defining your own rich types -- the `<view>` tag uses `to_html` to render a rich type. If you are not using DRYML and Rapid, you can simply call `to_html` in your views, e.g.
@@ -163,7 +162,7 @@ Provides validation of correct email address format.
163
162
 
164
163
  `HoboFields::EnumString` is not a rich type that you use directly. It's a "type generator", rather like Ruby's `Struct`. It's used for the common situation in database driven apps that you want an enumerated type, but it's not worth going to the extra bother of a separate table enumerating the values. For example you could create a type to represent the status of an article:
165
164
 
166
- >> ArticleStatus = HoboFields::EnumString.for(:draft, :approved, :published)
165
+ >> ArticleStatus = HoboFields::EnumString.for('', :draft, :approved, :published)
167
166
  => ArticleStatus
168
167
 
169
168
  Note that, like all dynamically created classes in Ruby, the class is anonymous until assigned to a constant:
@@ -208,6 +207,13 @@ Note that every enum you create is a subclass of HoboFields::EnumString:
208
207
  >> a.is_a?(HoboFields::EnumString)
209
208
  => true
210
209
 
210
+ EnumString's have a `validate` function defined to fit in with the
211
+ ActiveRecord validation framework.
212
+
213
+ >> a.validate
214
+ => nil
215
+ >> ArticleStatus.new("junked").validate
216
+ => "must be one of '', draft, approved, published"
211
217
 
212
218
  ### Using EnumString in your models
213
219
 
@@ -228,12 +234,46 @@ Sometimes it's nice to have a proper type name. Here's one way you might go abou
228
234
  class Article < ActiveRecord::Base
229
235
  Status = HoboFields::EnumString.for(:draft, :approved, :published)
230
236
  fields do
231
- status Status
237
+ status Article::Status
232
238
  end
233
239
  end
234
240
  >> Article.attr_type :status
235
241
  => Article::Status
236
242
 
243
+ ### Translating EnumString's
244
+
245
+ Named EnumString's may be translated. Here is an example fr.yml:
246
+
247
+ fr:
248
+ article/statuses:
249
+ draft: "esquisse"
250
+ approved: "approuvé"
251
+ published: "publiés"
252
+
253
+ Alternatively, the translations may be supplied in ruby:
254
+
255
+ >> Article::Status.translated_values = { "draft"=>"esquisse", "approved"=>"approuvé", "published"=>"publiés" }
256
+
257
+ EnumString's can be created with the translated values. Internally,
258
+ they always store the native version.
259
+
260
+ >> a=Article::Status.new("esquisse")
261
+ => "draft"
262
+ >> a.is_draft?
263
+ => true
264
+
265
+ The translated value is available via `to_html`:
266
+
267
+ >> Article::Status::PUBLISHED.to_html
268
+ => "publiés"
269
+
270
+ Translations only work with named EnumString's. The recommended way of naming the EnumString is to assign it to a constant, but if you do not wish to do this, you can supply the name in an option:
271
+
272
+ >> HoboFields::EnumString.for('one', 'two', 'three', :name => "Count").inspect
273
+ => "Count"
274
+
275
+ `tableize` will be called on your name to provide the translation key.
276
+
237
277
  ## Cleanup
238
278
  {.hidden}
239
279
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hobofields
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.100
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Locke
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-17 00:00:00 +00:00
12
+ date: 2009-12-01 00:00:00 +00:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,7 +30,7 @@ dependencies:
30
30
  requirements:
31
31
  - - "="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.9.0
33
+ version: 0.9.100
34
34
  version:
35
35
  description:
36
36
  email: tom@tomlocke.com
@@ -55,6 +55,7 @@ files:
55
55
  - lib/hobo_fields/fields_declaration.rb
56
56
  - lib/hobo_fields/html_string.rb
57
57
  - lib/hobo_fields/index_spec.rb
58
+ - lib/hobo_fields/lifecycle_state.rb
58
59
  - lib/hobo_fields/markdown_string.rb
59
60
  - lib/hobo_fields/migration_generator.rb
60
61
  - lib/hobo_fields/model_extensions.rb