clevic 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -130,11 +130,12 @@ class CacheTable < Array
130
130
  # build case statement, including recusion
131
131
  st = <<-EOF
132
132
  case
133
- when #{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
134
- when #{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
133
+ when #{model_class.table_name}.#{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
134
+ when #{model_class.table_name}.#{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
135
135
  else #{sql_boolean false}
136
136
  end
137
137
  EOF
138
+ # indent
138
139
  st.gsub!( /^/, ' ' * index )
139
140
  end
140
141
 
@@ -168,8 +169,12 @@ EOF
168
169
  end
169
170
 
170
171
  # add an id to options[:order] if it's not in there
171
- # also create @order_attributes
172
+ # also create @order_attributes, and @auto_new
172
173
  def sanitise_options( options )
174
+ # save this for later
175
+ @auto_new = options[:auto_new]
176
+ options.delete :auto_new
177
+
173
178
  options[:order] ||= ''
174
179
  @order_attributes = options[:order].split( /, */ ).map{|x| OrderAttribute.new(@model_class, x)}
175
180
 
@@ -281,9 +286,14 @@ EOF
281
286
  end
282
287
  end
283
288
 
289
+ def auto_new?
290
+ @auto_new
291
+ end
292
+
293
+ # make sure there's always at least one empty record
284
294
  def delete_at( index )
285
295
  retval = super
286
- if self.size == 0
296
+ if self.size == 0 && auto_new?
287
297
  self << @model_class.new
288
298
  end
289
299
  retval
@@ -12,8 +12,8 @@ connection to a particular database. Like this:
12
12
  end
13
13
 
14
14
  When the block ends, a check is done to see that the :database key
15
- exists. If not and exception is thrown. Finally the relevant calls to
16
- ActiveRecord are performed.
15
+ exists. If not, an exception is thrown. Finally the relevant calls to
16
+ establish the ActiveRecord connection are performed.
17
17
 
18
18
  Method calls are translated to insertions into a hash with the same
19
19
  key as the method being called. The hash is initialised
@@ -25,9 +25,10 @@ class DbOptions
25
25
 
26
26
  def initialize( options = nil )
27
27
  @options = options || {}
28
+
28
29
  # make sure the relevant entries exist, so method_missing works
29
30
  @options[:adapter] ||= ''
30
- @options[:host] ||= ''
31
+ @options[:host] ||= 'localhost'
31
32
  @options[:username] ||= ''
32
33
  @options[:password] ||= ''
33
34
  @options[:database] ||= ''
@@ -61,6 +62,7 @@ class DbOptions
61
62
  # end
62
63
  # the block is evaluated in the context of the a new DbOptions
63
64
  # object.
65
+ # TODO use instance_eval
64
66
  def self.connect( args = nil, &block )
65
67
  inst = self.new( args )
66
68
  # using the Rails implementation, included in Qt
@@ -87,10 +89,13 @@ end
87
89
 
88
90
  end
89
91
 
90
- # workaround for the date freeze issue
91
- class Date
92
- def freeze
93
- self
92
+ # workaround for the date freeze issue, if it exists
93
+ begin
94
+ Date.new.freeze.to_s
95
+ rescue TypeError
96
+ class Date
97
+ def freeze
98
+ self
99
+ end
94
100
  end
95
101
  end
96
-
@@ -365,7 +365,9 @@ class RelationalDelegate < ComboDelegate
365
365
  # send data to the editor
366
366
  def setEditorData( editor, model_index )
367
367
  if is_combo?( editor )
368
- editor.current_index = editor.find_data( model_index.attribute_value.id.to_variant )
368
+ unless model_index.attribute_value.nil?
369
+ editor.current_index = editor.find_data( model_index.attribute_value.id.to_variant )
370
+ end
369
371
  editor.line_edit.select_all
370
372
  end
371
373
  end
@@ -132,6 +132,22 @@ module Qt
132
132
 
133
133
  attr_writer :entity
134
134
 
135
+ # return true if validation failed for this indexes field
136
+ def has_errors?
137
+ # virtual fields don't have metadata
138
+ if metadata.nil?
139
+ false
140
+ else
141
+ entity.errors.invalid?( field_name.to_sym )
142
+ end
143
+ end
144
+
145
+ # return a collection of errors. Unlike AR, this
146
+ # will always return an array that will have zero, one
147
+ # or many elements.
148
+ def errors
149
+ [ entity.errors[field_name.to_sym] ].flatten
150
+ end
135
151
  end
136
152
 
137
153
  end
@@ -1,5 +1,7 @@
1
1
  require 'qtext/flags.rb'
2
2
 
3
+ require 'clevic/field_builder.rb'
4
+
3
5
  module Clevic
4
6
 
5
7
  =begin rdoc
@@ -10,6 +12,8 @@ class Field
10
12
 
11
13
  attr_accessor :attribute, :path, :label, :delegate, :class_name
12
14
  attr_accessor :alignment, :format, :tooltip, :path_block
15
+ attr_accessor :visible
16
+
13
17
  attr_writer :sample, :read_only
14
18
 
15
19
  # attribute is the symbol for the attribute on the model_class
@@ -26,6 +30,7 @@ EOF
26
30
  # set values
27
31
  @attribute = attribute
28
32
  @model_class = model_class
33
+ @visible = true
29
34
 
30
35
  options.each do |key,value|
31
36
  self.send( "#{key}=", value ) if respond_to?( "#{key}=" )
@@ -61,9 +66,8 @@ EOF
61
66
  end
62
67
  end
63
68
 
64
- # Return the attribute value for the given entity, which may
69
+ # Return the attribute value for the given entity, which will probably
65
70
  # be an ActiveRecord instance
66
- # entity is an ActiveRecord instance
67
71
  def value_for( entity )
68
72
  return nil if entity.nil?
69
73
  transform_attribute( entity.send( attribute ) )
@@ -86,6 +90,11 @@ EOF
86
90
  end
87
91
  end
88
92
 
93
+ # return true if this is a field for a related table, false otherwise.
94
+ def is_association?
95
+ meta.type == ActiveRecord::Reflection::AssociationReflection
96
+ end
97
+
89
98
  # return true if it's a date, a time or a datetime
90
99
  # cache result because the type won't change in the lifetime of the field
91
100
  def is_date_time?
@@ -93,7 +102,8 @@ EOF
93
102
  end
94
103
 
95
104
  # return ActiveRecord::Base.columns_hash[attribute]
96
- # in other words an ActiveRecord::ConnectionAdapters::Column object
105
+ # in other words an ActiveRecord::ConnectionAdapters::Column object,
106
+ # or an ActiveRecord::Reflection::AssociationReflection object
97
107
  def meta
98
108
  @model_class.columns_hash[attribute.to_s] || @model_class.reflections[attribute]
99
109
  end
@@ -0,0 +1,42 @@
1
+ module Clevic
2
+
3
+ class BlankSlate
4
+ keep_methods = %w( __send__ __id__ send class inspect instance_eval instance_variables )
5
+ instance_methods.each do |m|
6
+ undef_method(m) unless keep_methods.include?(m)
7
+ end
8
+ end
9
+
10
+ class FieldBuilder < BlankSlate
11
+ def initialize( hash = {} )
12
+ @hash = hash
13
+ end
14
+
15
+ # modified from Jim Freeze's article
16
+ def self.dsl_accessor(*symbols)
17
+ symbols.each do |sym|
18
+ line, st = __LINE__, <<EOF
19
+ def #{sym}(*val)
20
+ if val.empty?
21
+ @hash[#{sym.to_sym.inspect}]
22
+ else
23
+ @hash[#{sym.to_sym.inspect}] = val.size == 1 ? val[0] : val
24
+ end
25
+ end
26
+ EOF
27
+ class_eval st, __FILE__, line + 1
28
+ end
29
+ end
30
+
31
+ # originally from Jim Freeze's article
32
+ def method_missing(sym, *args)
33
+ self.class.dsl_accessor sym
34
+ send(sym, *args)
35
+ end
36
+
37
+ def to_hash
38
+ @hash
39
+ end
40
+ end
41
+
42
+ end
@@ -9,67 +9,110 @@ module Clevic
9
9
 
10
10
  =begin rdoc
11
11
  This is used to define a set of fields in a UI, any related tables,
12
- restrictions on data entry, formatting and that kind of thing.
12
+ restrictions on data entry, formatting and that kind of thing. Essentially it
13
+ defines a DSL for building a TableModel.
13
14
 
14
15
  Optional specifiers are:
15
16
  * :sample is used to size the columns. Will default to some hopefully sensible value from the db.
16
17
  * :format is something that can be understood by strftime (for time and date
17
- fields) or understood by % (for everything else)
18
+ fields) or understood by % (for everything else). It can also be a Proc
19
+ that has one parameter - the current entity.
18
20
  * :alignment is one of Qt::TextAlignmentRole, ie Qt::AlignRight, Qt::AlignLeft, Qt::AlignCenter
19
21
  * :set is the set of strings that are accepted by a RestrictedDelegate
20
22
 
21
23
  In the case of relational fields, all other options are passed to ActiveRecord::Base#find
22
24
 
23
- For example, a the UI for a model called Entry would be defined like this:
25
+ For example, the UI for a model called Entry could be defined like this:
24
26
 
25
- Clevic::TableView.new( Entry, parent ).create_model do
26
- # :format is optional
27
- plain :date, :format => '%d-%h-%y'
28
- plain :start, :format => '%H:%M'
29
- plain :amount, :format => '%.2f'
30
- # :set is mandatory
31
- restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
32
- distinct :description, :conditions => 'now() - date <= interval( 1 year )'
33
-
34
- # this is a read-only field
35
- plain :origin, :read_only => true
36
-
37
- # for these, :format will be a dotted attribute accessor for the related
38
- # ActiveRecord entity, in this case an instance of Account
39
- relational :debit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
40
- relational :credit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
41
-
42
- # or like this to have an on-the-fly transform
43
- # item will be an instance of Account
44
- relational :credit, :format => lambda {|item| item.name.downcase}, :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
45
-
46
- # this is a read-only display field from a related table
47
- # the Entry class should then define a method called currency
48
- # which returns an object that responds to 'short'.
49
- # You can also use a Proc for :display
50
- plain :currency, :display => 'short', :label => 'Currency'
51
-
52
- # this is a read-only display field from a related table
53
- # the Entry class should then define a method called currency
54
- # which returns an object that responds to 'currency', which
55
- # returns an object that responds to 'rate'.
56
- # You can also use a Proc for :display
57
- plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
58
-
59
- # this is optional
60
- records :order => 'date,start'
61
-
62
- # could also be like this, where a..e are instances of Entry
63
- records [ a,b,c,d,e ]
27
+ # inherit from Clevic::Record, which itself inherits from ActiveRecord::Base
28
+ class Entry < Clevic::Record
29
+ belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
30
+ belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
31
+
32
+ define_ui do
33
+ # :format is optional
34
+ plain :date, :format => '%d-%h-%y'
35
+ plain :start, :format => '%H:%M'
36
+ plain :amount, :format => '%.2f'
37
+ # :set is mandatory for a restricted field
38
+ restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
39
+
40
+ # alternately with a block for readability
41
+ restricted :vat do
42
+ label 'VAT'
43
+ set %w{ yes no all }
44
+ tooltip 'Is VAT included?'
45
+ end
46
+
47
+ # distinct will show other values for this field in the combo
48
+ distinct :description, :conditions => 'now() - date <= interval( 1 year )'
49
+
50
+ # this is a read-only field
51
+ plain :origin, :read_only => true
52
+
53
+ # for these, :format will be a dotted attribute accessor for the related
54
+ # ActiveRecord entity, in this case an instance of Account
55
+ relational :debit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
56
+ relational :credit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
57
+
58
+ # or like this to have an on-the-fly transform
59
+ # item will be an instance of Account
60
+ relational :credit, :format => lambda {|item| item.name.downcase}, :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
61
+
62
+ # this is a read-only display field from a related table
63
+ # the Entry class should then define a method called currency
64
+ # which returns an object that responds to 'short'.
65
+ # You can also use a Proc for :display
66
+ plain :currency, :display => 'short', :label => 'Currency'
67
+
68
+ # this is a read-only display field from a related table
69
+ # the Entry class should then define a method called currency
70
+ # which returns an object that responds to 'currency', which
71
+ # returns an object that responds to 'rate'.
72
+ # You can also use a Proc for :display
73
+ plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
74
+
75
+ # this is optional
76
+ records :order => 'date,start'
77
+
78
+ # could also be like this, where a..e are instances of Entry
79
+ records [ a,b,c,d,e ]
80
+ end
64
81
  end
82
+
83
+ For ActiveRecord::Base classes, ModelBuilder knows how to build a
84
+ fairly sensible default UI. For small tweaks, something like this
85
+ can be used (where Subscriber is already defined elsewhere as a subclass
86
+ of ActiveRecord::Base):
87
+ class Subscriber
88
+ post_default_ui do
89
+ plain :password # this field does not exist in the DB
90
+ hide :password_salt # these should be hidden
91
+ hide :password_hash
92
+ end
93
+ end
94
+
95
+ Subclasses of Clevic::Record may also implement <tt>self.key_press_event( event, current_index, view )</tt>
96
+ and <tt>self.data_changed( top_left_index, bottom_right_index, view )</tt> methods so that
97
+ they can respond to editing events and do Neat Stuff.
65
98
  =end
66
99
  class ModelBuilder
67
- # The collection of Clevic::Field objects
68
- attr_reader :fields
69
100
 
70
- def initialize( table_view )
71
- @table_view = table_view
101
+ # Create a definition for model_class (subclass of ActiveRecord::Base
102
+ # or Clevic::Record). Then execute block using self.instance_eval.
103
+ # The builder will construct a default TableModel from the model_class
104
+ # unless can_build_default == false
105
+ def initialize( model_class, can_build_default = true, &block )
106
+ @model_class = model_class
107
+ @auto_new = true
108
+ @read_only = false
72
109
  @fields = []
110
+ init_from_model( model_class, can_build_default, &block )
111
+ end
112
+
113
+ # The collection of visible Clevic::Field objects
114
+ def fields
115
+ @fields.reject{|x| !x.visible}
73
116
  end
74
117
 
75
118
  # return the index of the named field
@@ -79,29 +122,48 @@ class ModelBuilder
79
122
  retval
80
123
  end
81
124
 
82
- # the AR class for this table
83
- def model_class
84
- @table_view.model_class
125
+ # the ActiveRecord::Base or Clevic::Record class
126
+ attr_reader :model_class
127
+
128
+ # set read_only to true
129
+ def read_only!
130
+ @read_only = true
131
+ end
132
+
133
+ # should this table automatically show a new blank record?
134
+ def auto_new( bool )
135
+ @auto_new = bool
85
136
  end
137
+
138
+ def auto_new?; @auto_new; end
86
139
 
87
140
  # an ordinary field, edited in place with a text box
88
- def plain( attribute, options = {} )
89
- options[:read_only] = true if options.has_key?( :display )
141
+ def plain( attribute, options = {}, &block )
142
+ # get values from block, if it's there
143
+ options = gather_block( options, &block )
144
+
145
+ read_only_default( attribute, options )
90
146
  @fields << Clevic::Field.new( attribute.to_sym, model_class, options )
91
147
  end
92
148
 
93
149
  # edited with a combo box containing all previous entries in this field
94
- def distinct( attribute, options = {} )
150
+ def distinct( attribute, options = {}, &block )
151
+ # get values from block, if it's there
152
+ options = gather_block( options, &block )
153
+
95
154
  field = Clevic::Field.new( attribute.to_sym, model_class, options )
96
- field.delegate = DistinctDelegate.new( @table_view, attribute, @table_view.model_class, options )
155
+ field.delegate = DistinctDelegate.new( nil, attribute, model_class, options )
97
156
  @fields << field
98
157
  end
99
158
 
100
159
  # edited with a combo box, but restricted to a specified set
101
- def restricted( attribute, options = {} )
160
+ def restricted( attribute, options = {}, &block )
161
+ # get values from block, if it's there
162
+ options = gather_block( options, &block )
163
+
102
164
  raise "restricted must have a set" unless options.has_key?( :set )
103
165
  field = Clevic::Field.new( attribute.to_sym, model_class, options )
104
- field.delegate = RestrictedDelegate.new( @table_view, attribute, @table_view.model_class, options )
166
+ field.delegate = RestrictedDelegate.new( nil, attribute, model_class, options )
105
167
  @fields << field
106
168
  end
107
169
 
@@ -110,29 +172,24 @@ class ModelBuilder
110
172
  # if options[:format] has a value, it's used either as a block
111
173
  # or as a dotted path
112
174
  def relational( attribute, options = {}, &block )
113
- options[:display] ||= 'to_s'
114
175
  unless options.has_key? :class_name
115
176
  options[:class_name] = model_class.reflections[attribute].class_name || attribute.to_s.classify
116
177
  end
117
- field = Clevic::Field.new( attribute.to_sym, model_class, options )
118
178
 
119
- field.delegate = RelationalDelegate.new( @table_view, field.attribute_path, options )
179
+ # get values from block, if it's there
180
+ options = gather_block( options, &block )
181
+
182
+ # check after all possible options have been collected
183
+ raise ":display must be specified" if options[:display].nil?
184
+
185
+ field = Clevic::Field.new( attribute.to_sym, model_class, options )
186
+ field.delegate = RelationalDelegate.new( nil, field.attribute_path, options )
120
187
  @fields << field
121
188
  end
122
189
 
123
- # add AR :include options for foreign keys, but it takes up too much memory,
124
- # and actually takes longer to load a data set
125
- def add_include_options
126
- @fields.each do |field|
127
- if field.delegate.class == RelationalDelegate
128
- @options[:include] ||= []
129
- @options[:include] << field.attribute
130
- end
131
- end
132
- end
133
-
134
- # mostly used in the create_model block, but may also be
135
- # used as an accessor for records
190
+ # mostly used in the new block to define the set of records
191
+ # for the TableModel, but may also be
192
+ # used as an accessor for records.
136
193
  def records( *args )
137
194
  if args.size == 0
138
195
  get_records
@@ -141,37 +198,159 @@ class ModelBuilder
141
198
  end
142
199
  end
143
200
 
144
- # This is intended to be called from the view class which instantiated
145
- # this builder object.
146
- def build
201
+ # make sure this field doesn't show up
202
+ # mainly intended to be called after default_ui has been called
203
+ def hide( attribute )
204
+ field( attribute ).visible = false
205
+ end
206
+
207
+ # Build a default UI. All fields except the primary key are displayed
208
+ # as editable in the table. Any belongs_to relations are used to build
209
+ # combo boxes.
210
+ # Try to use a sensible :display option for the related class. In order:
211
+ # the name of the class, name, title, username
212
+ # order by the primary key. The class can use post_default_ui( &block )
213
+ # to do small tweaks.
214
+ def default_ui
215
+ # combine reflections and attributes into one set
216
+ reflections = model_class.reflections.keys.map{|x| x.to_s}
217
+ ui_columns = model_class.columns.reject{|x| x.name == model_class.primary_key }.map do |column|
218
+ # TODO there must be a better way to do this
219
+ att = column.name.gsub( /_id$/, '' )
220
+ if reflections.include?( att )
221
+ att
222
+ else
223
+ column.name
224
+ end
225
+ end
226
+
227
+ # don't create an empty record, because sometimes there are
228
+ # validations that will cause trouble
229
+ auto_new false
230
+
231
+ # build columns
232
+ ui_columns.each do |column|
233
+ if model_class.reflections.has_key?( column.to_sym )
234
+ begin
235
+ reflection = model_class.reflections[column.to_sym]
236
+ if reflection.class == ActiveRecord::Reflection::AssociationReflection
237
+ # try to find a sensible display class. Default to to_s
238
+ related_class = reflection.class_name.constantize
239
+ display_method =
240
+ %w{#{model_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
241
+ related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
242
+ end
243
+ relational column.to_sym, :display => display_method
244
+ else
245
+ plain column.to_sym
246
+ end
247
+ rescue
248
+ puts $!.message
249
+ puts $!.backtrace
250
+ # just do a plain
251
+ puts "Doing plain for #{model_class}.#{column}"
252
+ plain column.to_sym
253
+ end
254
+ else
255
+ plain column.to_sym
256
+ end
257
+ end
258
+ records :order => model_class.primary_key
259
+ end
260
+
261
+ # return the named Clevic::Field object
262
+ def field( attribute )
263
+ @fields.find {|x| x.attribute == attribute }
264
+ end
265
+
266
+ # This takes all the information collected
267
+ # by the other methods, and returns the new TableModel
268
+ def build( table_view )
147
269
  # build the model with all it's collections
148
270
  # using @model here because otherwise the view's
149
271
  # reference to this very same model is garbage collected.
150
- # TODO put @fields into TableModel, and access from there?
151
- @model = Clevic::TableModel.new( self )
152
- @model.object_name = @table_view.model_class.name
153
- @model.dots = @fields.map {|x| x.column }
154
- @model.labels = @fields.map {|x| x.label }
155
- @model.attributes = @fields.map {|x| x.attribute }
156
- @model.attribute_paths = @fields.map { |x| x.attribute_path }
272
+ @model = Clevic::TableModel.new( table_view )
273
+ @model.object_name = model_class.name
274
+ @model.model_class = model_class
275
+ @model.fields = @fields
276
+ @model.read_only = @read_only
277
+ @model.auto_new = auto_new?
278
+
279
+ # set parent for all delegates
280
+ fields.each {|x| x.delegate.parent = table_view unless x.delegate.nil? }
157
281
 
158
282
  # the data
159
283
  @model.collection = records
160
- # fill in an empty record for data entry
161
- @model.collection << model_class.new if @model.collection.size == 0
162
284
 
163
- # now set delegates
164
- @table_view.item_delegate = Clevic::ItemDelegate.new( @table_view )
165
- @fields.each_with_index do |field, index|
166
- @table_view.set_item_delegate_for_column( index, field.delegate )
285
+ @model
286
+ end
287
+
288
+ private
289
+
290
+ def init_from_model( model_class, can_build_default, &block )
291
+ if model_class.respond_to?( :build_table_model )
292
+ # call build_table_model
293
+ method = model_class.method :build_table_model
294
+ method.call( builder )
295
+ elsif !model_class.define_ui_block.nil?
296
+ #define_ui is used, so use that block
297
+ instance_eval( &model_class.define_ui_block )
298
+ elsif can_build_default
299
+ # build a default UI
300
+ default_ui
301
+
302
+ # allow for smallish changes to a default build
303
+ instance_eval( &model_class.post_default_ui_block ) unless model_class.post_default_ui_block.nil?
304
+ end
305
+
306
+ # the local block adds to the previous definitions
307
+ unless block.nil?
308
+ if block.arity == 0
309
+ instance_eval( &block )
310
+ else
311
+ yield( builder )
312
+ end
313
+ end
314
+ end
315
+
316
+ # add AR :include options for foreign keys, but it takes up too much memory,
317
+ # and actually takes longer to load a data set
318
+ def add_include_options
319
+ fields.each do |field|
320
+ if field.delegate.class == RelationalDelegate
321
+ @options[:include] ||= []
322
+ @options[:include] << field.attribute
323
+ end
167
324
  end
168
-
169
- # give the built model back to the view class
170
- # see above comment about @model
171
- @table_view.model = @model
172
325
  end
173
326
 
174
- private
327
+ # set a sensible read-only value if it isn't already
328
+ # specified in options doesn't alread
329
+ def read_only_default( attribute, options )
330
+ # sensible defaults for read-only-ness
331
+ options[:read_only] ||=
332
+ case
333
+ when options[:display].respond_to?( :call )
334
+ # it's a Proc or a Method, so we can't set it
335
+ true
336
+
337
+ when model_class.column_names.include?( options[:display].to_s )
338
+ # it's a DB column, so it's not read only
339
+ false
340
+
341
+ when model_class.reflections.include?( attribute )
342
+ # one-to-one relationships can be edited. many-to-one certainly can't
343
+ reflection = model_class.reflections[attribute]
344
+ reflection.macro != :has_one
345
+
346
+ when model_class.instance_methods.include?( attribute.to_s )
347
+ # read-only if there's no setter for the attribute
348
+ !model_class.instance_methods.include?( "#{attribute.to_s}=" )
349
+ else
350
+ # default to not read-only
351
+ false
352
+ end
353
+ end
175
354
 
176
355
  # The collection of model objects to display in a table
177
356
  # arg can either be a Hash, in which case a new CacheTable
@@ -191,10 +370,24 @@ private
191
370
  def get_records
192
371
  if @records.nil?
193
372
  #~ add_include_options
373
+ @options[:auto_new] = auto_new?
194
374
  @records = CacheTable.new( model_class, @options )
195
375
  end
196
376
  @records
197
377
  end
378
+ # update options with the values in block, using FieldBuilder
379
+ # to evaluate block
380
+
381
+ def gather_block( options, &block )
382
+ unless block.nil?
383
+ fb = FieldBuilder.new( options )
384
+ fb.instance_eval( &block )
385
+ fb.to_hash
386
+ else
387
+ options
388
+ end
389
+ end
390
+
198
391
  end
199
392
 
200
393
  end