clevic 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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