clevic 0.8.0 → 0.11.1

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.
Files changed (52) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +13 -10
  3. data/README.txt +6 -9
  4. data/Rakefile +35 -24
  5. data/TODO +29 -17
  6. data/bin/clevic +84 -37
  7. data/config/hoe.rb +7 -3
  8. data/lib/clevic.rb +2 -4
  9. data/lib/clevic/browser.rb +37 -49
  10. data/lib/clevic/cache_table.rb +55 -165
  11. data/lib/clevic/db_options.rb +32 -21
  12. data/lib/clevic/default_view.rb +66 -0
  13. data/lib/clevic/delegates.rb +51 -67
  14. data/lib/clevic/dirty.rb +101 -0
  15. data/lib/clevic/extensions.rb +24 -38
  16. data/lib/clevic/field.rb +400 -99
  17. data/lib/clevic/item_delegate.rb +32 -33
  18. data/lib/clevic/model_builder.rb +315 -148
  19. data/lib/clevic/order_attribute.rb +53 -0
  20. data/lib/clevic/record.rb +57 -57
  21. data/lib/clevic/search_dialog.rb +71 -67
  22. data/lib/clevic/sql_dialects.rb +33 -0
  23. data/lib/clevic/table_model.rb +73 -120
  24. data/lib/clevic/table_searcher.rb +165 -0
  25. data/lib/clevic/table_view.rb +140 -100
  26. data/lib/clevic/ui/.gitignore +1 -0
  27. data/lib/clevic/ui/browser_ui.rb +55 -56
  28. data/lib/clevic/ui/search_dialog_ui.rb +50 -51
  29. data/lib/clevic/version.rb +2 -2
  30. data/lib/clevic/view.rb +89 -0
  31. data/models/accounts_models.rb +12 -9
  32. data/models/minimal_models.rb +4 -2
  33. data/models/times_models.rb +41 -25
  34. data/models/times_sqlite_models.rb +1 -145
  35. data/models/values_models.rb +15 -16
  36. data/test/test_cache_table.rb +138 -0
  37. data/test/test_helper.rb +131 -0
  38. data/test/test_model_index_extensions.rb +22 -0
  39. data/test/test_order_attribute.rb +62 -0
  40. data/test/test_sql_dialects.rb +77 -0
  41. data/test/test_table_searcher.rb +188 -0
  42. metadata +36 -20
  43. data/bin/import-times +0 -128
  44. data/config/jamis.rb +0 -589
  45. data/env.sh +0 -1
  46. data/lib/active_record/dirty.rb +0 -87
  47. data/lib/clevic/field_builder.rb +0 -42
  48. data/website/index.html +0 -170
  49. data/website/index.txt +0 -17
  50. data/website/screenshot.png +0 -0
  51. data/website/stylesheets/screen.css +0 -131
  52. data/website/template.html.erb +0 -41
@@ -11,53 +11,52 @@ end
11
11
  module Clevic
12
12
 
13
13
  class ItemDelegate < Qt::ItemDelegate
14
+ attr_reader :field
14
15
 
15
- def initialize( parent )
16
- super
16
+ def initialize( parent, field )
17
+ super( parent )
18
+ @field = field
17
19
  end
18
20
 
19
- # This catches the event that begins the edit process.
20
- # Not used at the moment.
21
- def editorEvent ( event, model, style_option_view_item, model_index )
22
- #~ if $options[:debug]
23
- #~ puts "editorEvent"
24
- #~ puts "event: #{event.inspect}"
25
- #~ puts "model: #{model.inspect}"
26
- #~ puts "style_option_view_item: #{style_option_view_item.inspect}"
27
- #~ puts "model_index: #{model_index.inspect}"
28
- #~ end
29
- super
21
+ def attribute
22
+ field.attribute
30
23
  end
31
24
 
32
- def createEditor( parent_widget, style_option_view_item, model_index )
33
- if model_index.metadata.type == :date
34
- # not going to work here because being triggered by
35
- # an alphanumeric keystroke (as opposed to F4)
36
- # will result in the calendar widget being opened.
37
- #~ Qt::CalendarWidget.new( parent_widget )
38
- super
39
- else
40
- super
41
- end
25
+ def entity_class
26
+ field.entity_class
42
27
  end
43
28
 
44
- #~ def setEditorData( editor, model_index )
45
- #~ editor.value = model_index.gui_value
46
- #~ end
47
-
48
- #~ def setModelData( editor, abstract_item_model, model_index )
49
- #~ model_index.gui_value = editor.value
50
- #~ emit abstract_item_model.dataChanged( model_index, model_index )
51
- #~ end
29
+ def find_options
30
+ field.find_options
31
+ end
52
32
 
33
+ # Figure out where to put the editor widget, taking into
34
+ # account the sizes of the headers
53
35
  def updateEditorGeometry( editor, style_option_view_item, model_index )
54
- # figure out where to put the editor widget, taking into
55
- # account the sizes of the headers
56
36
  rect = style_option_view_item.rect
57
37
  rect.set_width( [editor.size_hint.width,rect.width].max )
58
38
  rect.set_height( editor.size_hint.height )
59
39
  editor.set_geometry( rect )
60
40
  end
41
+
42
+ # This catches the event that begins the edit process.
43
+ #~ def editorEvent ( event, model, style_option_view_item, model_index )
44
+ #~ end
45
+
46
+ # This is called when one of the EditTriggers is pressed. So
47
+ # it's only good for opening a generic keystroke editor, not
48
+ # a specific one, eg a calendar-style date editor.
49
+ #~ def createEditor( parent_widget, style_option_view_item, model_index )
50
+ #~ end
51
+
52
+ # Set the data for the given editor widget
53
+ #~ def setEditorData( editor_widget, model_index )
54
+ #~ end
55
+
56
+ # Set the data for the given model and index from the given
57
+ #~ def setModelData( editor_widget, abstract_item_model, model_index )
58
+ #~ end
59
+
61
60
  end
62
61
 
63
62
  end
@@ -8,30 +8,57 @@ require 'clevic/field.rb'
8
8
  module Clevic
9
9
 
10
10
  =begin rdoc
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. Essentially it
13
- defines a DSL for building a TableModel.
14
11
 
15
- Optional specifiers are:
16
- * :sample is used to size the columns. Will default to some hopefully sensible value from the db.
17
- * :format is something that can be understood by strftime (for time and date
18
- fields) or understood by % (for everything else). It can also be a Proc
19
- that has one parameter - the current entity.
20
- * :alignment is one of Qt::TextAlignmentRole, ie Qt::AlignRight, Qt::AlignLeft, Qt::AlignCenter
21
- * :set is the set of strings that are accepted by a RestrictedDelegate
12
+ == View definition
22
13
 
23
- In the case of relational fields, all other options are passed to ActiveRecord::Base#find
14
+ Clevic::ModelBuilder defines the DSL used to create a UI definition (which is
15
+ actually a set of Clevic::Field instances), including any related tables,
16
+ restrictions on data entry, formatting and so on. The intention was to make
17
+ specifying a UI as painless as possible, with framework overhead only where
18
+ you need it.
24
19
 
25
- For example, the UI for a model called Entry could be defined like this:
20
+ To that end, there are 2 ways to define UIs:
26
21
 
27
- # inherit from Clevic::Record, which itself inherits from ActiveRecord::Base
28
- class Entry < Clevic::Record
22
+ - an Embedded View as part of the ActiveRecord object (which is useful if you
23
+ want minimal framework overhead). Just show me the data, dammit.
24
+
25
+ - a Separate View in a separate class (which is useful when you want several
26
+ diffent views of the same underlying table). I want a neato-nifty UI that does
27
+ (relatively) complex things.
28
+
29
+ I've tried to consistently refer to an instance of an ActiveRecord::Base subclass
30
+ as an 'entity'.
31
+
32
+ ==Embedded View
33
+ Minimal embedded definition is
34
+
35
+ class Position < ActiveRecord::Base
36
+ include Clevic::Record
37
+ end
38
+
39
+ which will build a fairly sensible default UI from the
40
+ entity's metadata. Obviously you can use open classes to do
41
+
42
+ class Position
43
+ include Clevic::Record
44
+ end
45
+
46
+ where Position is previously defined to inherit from ActiveRecord::Base.
47
+
48
+ A full-featured UI for an entity called Entry (part of an accounting database)
49
+ could be defined like this:
50
+
51
+ class Entry < ActiveRecord::Base
52
+ include Clevic::Record
53
+
54
+ # ActiveRecord foreign key definition
29
55
  belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
56
+ # ActiveRecord foreign key definition
30
57
  belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
31
58
 
32
59
  define_ui do
33
- # :format is optional
34
- plain :date, :format => '%d-%h-%y'
60
+ # :format and :edit_format are optional, in fact these are the defaults
61
+ plain :date, :format => '%d-%h-%y', :edit_format => '%d-%h-%Y'
35
62
  plain :start, :format => '%H:%M'
36
63
  plain :amount, :format => '%.2f'
37
64
  # :set is mandatory for a restricted field
@@ -44,20 +71,27 @@ For example, the UI for a model called Entry could be defined like this:
44
71
  tooltip 'Is VAT included?'
45
72
  end
46
73
 
47
- # distinct will show other values for this field in the combo
74
+ # distinct will retrieve from the table all other values for this field
75
+ # and display them in the combo.
48
76
  distinct :description, :conditions => 'now() - date <= interval( 1 year )'
49
77
 
50
78
  # this is a read-only field
51
79
  plain :origin, :read_only => true
52
80
 
53
- # for these, :format will be a dotted attribute accessor for the related
81
+ # :format is an attribute on the related
54
82
  # 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)'
83
+ # :order is an ActiveRecord option to find, defining the order in which related entries will be displayed.
84
+ # :conditions is an ActiveRecord option to find, defining the subset of related entries to be displayed.
85
+ relational :debit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
86
+ relational :credit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
57
87
 
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'
88
+ # or like this to have an on-the-fly transform. item will be an instance of Account.
89
+ # This also takes a block parameter
90
+ relational :credit do |field|
91
+ field.format = lambda {|item| item.name.downcase}
92
+ field.conditions = 'active = true'
93
+ field.order = 'lower(name)'
94
+ end
61
95
 
62
96
  # this is a read-only display field from a related table
63
97
  # the Entry class should then define a method called currency
@@ -72,58 +106,215 @@ For example, the UI for a model called Entry could be defined like this:
72
106
  # You can also use a Proc for :display
73
107
  plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
74
108
 
75
- # this is optional
109
+ # this is optional. By default all records in id order will be displayed.
76
110
  records :order => 'date,start'
77
111
 
78
112
  # could also be like this, where a..e are instances of Entry
79
113
  records [ a,b,c,d,e ]
80
114
  end
81
115
  end
116
+
117
+ == Separate View
118
+
119
+ To define a separate ui class, do something like this:
120
+ class Prospect < Clevic::View
121
+
122
+ # This is the ActiveRecord::Base descendant
123
+ entity_class Position
124
+
125
+ # This must return a ModelBuilder instance, which is made easier
126
+ # by putting the block in a call to model_builder.
127
+ #
128
+ # With no parameter, the block
129
+ # will be evaluated in the context of a Clevic::ModelBuilder instance,
130
+ # otherwise the parameter will have the Clevic::ModelBuilder instance
131
+ # so you can still access the surrounding scope.
132
+ def define_ui
133
+ model_builder do |mb|
134
+ # use the define_ui block from Position
135
+ mb.exec_ui_block( Position )
136
+
137
+ # use a different recordset
138
+ mb.records :conditions => "status in ('prospect','open')", :order => 'date desc,code'
139
+ end
140
+ end
141
+ end
82
142
 
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
143
+ And you can even inherit UIs:
144
+
145
+ class Extinct < Prospect
146
+ def define_ui
147
+ # reuse all UI definitions from Prospect
148
+ super
149
+ # and again another recordset
150
+ model_builder do |mb|
151
+ mb.records :conditions => "status in ('dead')", :order => 'date desc,code'
152
+ end
153
+ end
154
+ end
155
+
156
+ Obviously you can use any of the Clevic::ModelBuilder calls described above, and exemplified
157
+ in the embedded example, inside of the model_builder block.
158
+
159
+ == DSL detail
160
+
161
+ This section describes the syntax of the DSL.
162
+
163
+ === Field Types and specifiers
164
+
165
+ There are only a few field types, with lots of options. All field definitions
166
+ start with a field type, have an attribute, and take either a hash of options,
167
+ or a block for options. If the block specifies a parameter, an instance of
168
+ Clevic::Field will be passed. If the block has no parameter, it will be
169
+ evaluated in the context of a Clevic::Field instance. All the options specified
170
+ can use DSL-style acessors (no assignment =) or assignment statement.
171
+
172
+ plain
173
+ is an ordinary editable field. Boolean values are displayed as checkboxes.
174
+
175
+ relational
176
+ displays a set of values pulled from a belongs_to (many-to-one) relationship.
177
+ In other words all the possible related entities that this one could belong_to. Some
178
+ concise representation of the related entities are displayed in a combo box.
179
+ :display is mandatory. All options applicable to ActiveRecord::Base#find can also be passed.
180
+
181
+ distinct
182
+ fetches the set of values already in the field, so you don't have to re-type them.
183
+ New values are added in the text field part of the combo box. There is some prefix matching.
184
+
185
+ restricted
186
+ is a combo box that is not editable in the text field part - the user must select
187
+ a value from the :set (an array of strings) supplied. If :set has a hash as its value, the field
188
+ will display the hash values, and the hash keys will be stored in the db.
189
+
190
+ hide
191
+ you won't see this field. Actually, it's only useful after a default_ui, or pulling the
192
+ definition from somewhere else. It may go away and be replaced by remove.
193
+
194
+ === Attribute
195
+
196
+ The attribute symbol is required, and is the first parameter after the field type. It must refer
197
+ to a method already defined in the entity. In other words any of:
198
+ - a db column
199
+ - a relationship (belongs_to, has_many, etc)
200
+ - a plain method that takes no parameters.
201
+
202
+ will work. Named scopes might also work, but I haven't tried them yet.
203
+
204
+ You can do things like this:
205
+
206
+ plain :entries, :label => 'First Entry', :display => 'first.date', :format => '%d-%b-%y'
207
+ plain :entries, :label => 'Last Entry', :display => 'last.date', :format => '%d-%b-%y'
208
+
209
+ Where the attribute fetches a collection of related entities, and :display will cause
210
+ exactly one of those values to be passed to :format.
211
+
212
+ === Options
213
+
214
+ Optional specifiers follow the attribute, as hash parameters, or as a block. Many of them will
215
+ accept as a value one of:
216
+ - String, some kind of value
217
+ - Symbol, referring to a method on the entity
218
+ - Proc which takes the entity as a parameter
219
+
220
+ See Clevic::Field properties for available options.
221
+
222
+ === Menu Items
223
+
224
+ You can define view/model specific actions ( an Action is Qt talk for menu items and shortcuts).
225
+ These will be added to the Edit menu, show up on context-click in the table
226
+ display, and can have optional keyboard shortcuts:
227
+
228
+ def define_actions( table_view, action_builder )
229
+ action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
230
+ # a method in the class containing define_actions
231
+ # view.current_index.entity will return the entity instance.
232
+ smart_copy( view )
233
+ end
234
+
235
+ action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
236
+ # a method in the class containing define_actions
237
+ invoice_from_project( view.current_index, view )
92
238
  end
93
239
  end
240
+
241
+ === Notifications
242
+
243
+ The following method will be called whenever data is changed, ie a field edit is completed:
244
+
245
+ def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
246
+ end
247
+
248
+ Key presses will be sent here:
249
+
250
+ def notify_key_press( table_view, key_press_event, current_model_index )
251
+ end
252
+
253
+ The above may also be defined as class methods on an entity class.
254
+
255
+ === Tab Order
256
+
257
+ Using an embedded definition, tab order in the browser is defined by the order in which view definitions
258
+ are encountered. Which is really useful if you want to have several view definitions in one file and
259
+ just execute clevic on that file.
260
+
261
+ For more complex situations where your code needs to be separated into
262
+ multiple files, as is traditional and useful for most non-trivial projects,
263
+ the order can be accessed in Clevic::View.order, and specified by
264
+
265
+ Clevic::View.order = [Position, Target, Account]
94
266
 
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.
98
267
  =end
99
268
  class ModelBuilder
100
269
 
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
270
+ # Create a definition for entity_view (subclass of Clevic::View).
271
+ # Then execute block using self.instance_eval.
272
+ def initialize( entity_view, &block )
273
+ @entity_view = entity_view
107
274
  @auto_new = true
108
275
  @read_only = false
109
276
  @fields = []
110
- init_from_model( model_class, can_build_default, &block )
277
+ exec_ui_block( &block )
278
+ end
279
+
280
+ attr_accessor :entity_view
281
+
282
+ # execute a block containing method calls understood by Clevic::ModelBuilder
283
+ # arg can be something that responds to define_ui_block,
284
+ # or just the block will be executed. If both are present,
285
+ # values in the block will overwrite values in arg's block.
286
+ def exec_ui_block( arg = nil, &block )
287
+ if !arg.nil? and arg.respond_to?( :define_ui_block )
288
+ exec_ui_block( &arg.define_ui_block )
289
+ end
290
+
291
+ unless block.nil?
292
+ if block.arity == -1
293
+ instance_eval( &block )
294
+ else
295
+ block.call( self )
296
+ end
297
+ end
298
+ self
111
299
  end
112
300
 
113
- # The collection of visible Clevic::Field objects
301
+ # The collection of Clevic::Field instances where visible == true.
302
+ # the visible may go away.
114
303
  def fields
115
304
  @fields.reject{|x| !x.visible}
116
305
  end
117
306
 
118
- # return the index of the named field
307
+ # return the index of the named field in the collection of fields.
119
308
  def index( field_name_sym )
120
309
  retval = nil
121
310
  fields.each_with_index{|x,i| retval = i if x.attribute == field_name_sym.to_sym }
122
311
  retval
123
312
  end
124
313
 
125
- # the ActiveRecord::Base or Clevic::Record class
126
- attr_reader :model_class
314
+ # The ActiveRecord::Base subclass
315
+ def entity_class
316
+ @entity_view.entity_class
317
+ end
127
318
 
128
319
  # set read_only to true
129
320
  def read_only!
@@ -135,35 +326,36 @@ class ModelBuilder
135
326
  @auto_new = bool
136
327
  end
137
328
 
329
+ # should this table automatically show a new blank record?
138
330
  def auto_new?; @auto_new; end
139
331
 
140
332
  # an ordinary field, edited in place with a text box
141
333
  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 )
146
- @fields << Clevic::Field.new( attribute.to_sym, model_class, options )
334
+ read_only_default!( attribute, options )
335
+ @fields << Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
147
336
  end
148
337
 
149
- # edited with a combo box containing all previous entries in this field
338
+ # Returns a Clevic::Field with a DistinctDelegate, in other words
339
+ # a combo box containing all values for this field from the table.
150
340
  def distinct( attribute, options = {}, &block )
151
- # get values from block, if it's there
152
- options = gather_block( options, &block )
153
-
154
- field = Clevic::Field.new( attribute.to_sym, model_class, options )
155
- field.delegate = DistinctDelegate.new( nil, attribute, model_class, options )
341
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
342
+ field.delegate = DistinctDelegate.new( nil, field )
156
343
  @fields << field
157
344
  end
158
345
 
159
- # edited with a combo box, but restricted to a specified set
346
+ # Returns a Clevic::Field with a RestrictedDelegate,
347
+ # a combo box, but restricted to a specified set, from the :set option.
160
348
  def restricted( attribute, options = {}, &block )
161
- # get values from block, if it's there
162
- options = gather_block( options, &block )
163
349
 
164
- raise "restricted must have a set" unless options.has_key?( :set )
165
- field = Clevic::Field.new( attribute.to_sym, model_class, options )
166
- field.delegate = RestrictedDelegate.new( nil, attribute, model_class, options )
350
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
351
+ raise "field #{attribute} restricted must have a set" if field.set.nil?
352
+
353
+ # TODO this really belongs in a separate 'map' field
354
+ if field.set.is_a? Hash
355
+ field.format = lambda{|x| field.set[x]}
356
+ end
357
+
358
+ field.delegate = RestrictedDelegate.new( nil, field )
167
359
  @fields << field
168
360
  end
169
361
 
@@ -172,49 +364,58 @@ class ModelBuilder
172
364
  # if options[:format] has a value, it's used either as a block
173
365
  # or as a dotted path
174
366
  def relational( attribute, options = {}, &block )
175
- unless options.has_key? :class_name
176
- options[:class_name] = model_class.reflections[attribute].class_name || attribute.to_s.classify
367
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
368
+ if field.class_name.nil?
369
+ field.class_name = entity_class.reflections[attribute].class_name || attribute.to_s.classify
177
370
  end
178
371
 
179
- # get values from block, if it's there
180
- options = gather_block( options, &block )
181
-
182
372
  # 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 )
373
+ raise ":display must be specified" if field.display.nil?
374
+ field.delegate = RelationalDelegate.new( nil, field )
187
375
  @fields << field
188
376
  end
189
377
 
190
378
  # mostly used in the new block to define the set of records
191
379
  # for the TableModel, but may also be
192
380
  # used as an accessor for records.
193
- def records( *args )
381
+ def records( args = {} )
194
382
  if args.size == 0
195
383
  get_records
196
384
  else
197
- set_records( args[0] )
385
+ set_records( args )
198
386
  end
199
387
  end
200
388
 
201
- # make sure this field doesn't show up
202
- # mainly intended to be called after default_ui has been called
389
+ # Tell this field not to show up in the UI.
390
+ # Mainly intended to be called after default_ui has been called.
203
391
  def hide( attribute )
204
392
  field( attribute ).visible = false
205
393
  end
206
394
 
207
395
  # Build a default UI. All fields except the primary key are displayed
208
396
  # 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.
397
+ # combo boxes. Default ordering is the primary key.
398
+ # Subscriber is already defined elsewhere as a subclass
399
+ # of ActiveRecord::Base:
400
+ # class Subscriber
401
+ # include Clevic::Record
402
+ # define_ui do
403
+ # default_ui
404
+ # plain :password # this field does not exist in the DB
405
+ # hide :password_salt # these should be hidden
406
+ # hide :password_hash
407
+ # end
408
+ # end
409
+ #
410
+ # An attempt to use a sensible :display option for the related class. In order:
411
+ # * the name of the class
412
+ # * :name
413
+ # * :title
414
+ # * :username
214
415
  def default_ui
215
416
  # 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|
417
+ reflections = entity_class.reflections.keys.map{|x| x.to_s}
418
+ ui_columns = entity_class.columns.reject{|x| x.name == entity_class.primary_key }.map do |column|
218
419
  # TODO there must be a better way to do this
219
420
  att = column.name.gsub( /_id$/, '' )
220
421
  if reflections.include?( att )
@@ -230,16 +431,19 @@ class ModelBuilder
230
431
 
231
432
  # build columns
232
433
  ui_columns.each do |column|
233
- if model_class.reflections.has_key?( column.to_sym )
434
+ if entity_class.reflections.has_key?( column.to_sym )
234
435
  begin
235
- reflection = model_class.reflections[column.to_sym]
436
+ reflection = entity_class.reflections[column.to_sym]
236
437
  if reflection.class == ActiveRecord::Reflection::AssociationReflection
237
- # try to find a sensible display class. Default to to_s
238
438
  related_class = reflection.class_name.constantize
439
+
440
+ # try to find a sensible display class. Default to to_s
239
441
  display_method =
240
- %w{#{model_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
442
+ %w{#{entity_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
241
443
  related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
242
444
  end
445
+
446
+ # set the display method
243
447
  relational column.to_sym, :display => display_method
244
448
  else
245
449
  plain column.to_sym
@@ -248,14 +452,14 @@ class ModelBuilder
248
452
  puts $!.message
249
453
  puts $!.backtrace
250
454
  # just do a plain
251
- puts "Doing plain for #{model_class}.#{column}"
455
+ puts "Doing plain for #{entity_class}.#{column}"
252
456
  plain column.to_sym
253
457
  end
254
458
  else
255
459
  plain column.to_sym
256
460
  end
257
461
  end
258
- records :order => model_class.primary_key
462
+ records :order => entity_class.primary_key
259
463
  end
260
464
 
261
465
  # return the named Clevic::Field object
@@ -264,14 +468,15 @@ class ModelBuilder
264
468
  end
265
469
 
266
470
  # This takes all the information collected
267
- # by the other methods, and returns the new TableModel
471
+ # by the other methods, and returns a new TableModel
472
+ # with the given table_view as its parent.
268
473
  def build( table_view )
269
474
  # build the model with all it's collections
270
475
  # using @model here because otherwise the view's
271
476
  # reference to this very same model is garbage collected.
272
477
  @model = Clevic::TableModel.new( table_view )
273
- @model.object_name = model_class.name
274
- @model.model_class = model_class
478
+ table_view.object_name = @object_name
479
+ @model.entity_view = entity_view
275
480
  @model.fields = @fields
276
481
  @model.read_only = @read_only
277
482
  @model.auto_new = auto_new?
@@ -285,36 +490,12 @@ class ModelBuilder
285
490
  @model
286
491
  end
287
492
 
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
493
+ protected
305
494
 
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
495
+ # Add ActiveRecord :include options for foreign keys, but it takes up too much memory,
496
+ # and actually takes longer to load a data set.
497
+ #--
498
+ # TODO ActiveRecord-2.1 has smarter includes
318
499
  def add_include_options
319
500
  fields.each do |field|
320
501
  if field.delegate.class == RelationalDelegate
@@ -324,9 +505,8 @@ private
324
505
  end
325
506
  end
326
507
 
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 )
508
+ # set a sensible read-only value if it isn't already specified in options
509
+ def read_only_default!( attribute, options )
330
510
  # sensible defaults for read-only-ness
331
511
  options[:read_only] ||=
332
512
  case
@@ -334,18 +514,18 @@ private
334
514
  # it's a Proc or a Method, so we can't set it
335
515
  true
336
516
 
337
- when model_class.column_names.include?( options[:display].to_s )
517
+ when entity_class.column_names.include?( options[:display].to_s )
338
518
  # it's a DB column, so it's not read only
339
519
  false
340
520
 
341
- when model_class.reflections.include?( attribute )
521
+ when entity_class.reflections.include?( attribute )
342
522
  # one-to-one relationships can be edited. many-to-one certainly can't
343
- reflection = model_class.reflections[attribute]
523
+ reflection = entity_class.reflections[attribute]
344
524
  reflection.macro != :has_one
345
525
 
346
- when model_class.instance_methods.include?( attribute.to_s )
526
+ when entity_class.instance_methods.include?( attribute.to_s )
347
527
  # read-only if there's no setter for the attribute
348
- !model_class.instance_methods.include?( "#{attribute.to_s}=" )
528
+ !entity_class.instance_methods.include?( "#{attribute.to_s}=" )
349
529
  else
350
530
  # default to not read-only
351
531
  false
@@ -354,8 +534,8 @@ private
354
534
 
355
535
  # The collection of model objects to display in a table
356
536
  # arg can either be a Hash, in which case a new CacheTable
357
- # is created, or it can be an array
358
- # called by records( *args )
537
+ # is created, or it can be an array.
538
+ # Called by records( *args )
359
539
  def set_records( arg )
360
540
  if arg.class == Hash
361
541
  # need to defer this until all fields are collected
@@ -365,28 +545,15 @@ private
365
545
  end
366
546
  end
367
547
 
368
- # return a collection of records. Usually this will be a CacheTable.
369
- # called by records( *args )
548
+ # Return a collection of records. Usually this will be a CacheTable.
549
+ # Called by records( *args )
370
550
  def get_records
371
551
  if @records.nil?
372
552
  #~ add_include_options
373
- @options[:auto_new] = auto_new?
374
- @records = CacheTable.new( model_class, @options )
553
+ @records = CacheTable.new( entity_class, @options.merge( :auto_new => auto_new? ) )
375
554
  end
376
555
  @records
377
556
  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
557
 
391
558
  end
392
559