clevic 0.13.0.b3 → 0.13.0.b5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/History.txt +21 -0
  2. data/Manifest.txt +91 -85
  3. data/README.txt +33 -18
  4. data/Rakefile +2 -3
  5. data/TODO +8 -14
  6. data/bin/clevic +18 -20
  7. data/lib/clevic.rb +7 -1
  8. data/lib/clevic/action_builder.rb +4 -1
  9. data/lib/clevic/ar_methods.rb +72 -57
  10. data/lib/clevic/attribute_list.rb +4 -0
  11. data/lib/clevic/cache_table.rb +43 -69
  12. data/lib/clevic/dataset_roller.rb +22 -0
  13. data/lib/clevic/delegate.rb +11 -5
  14. data/lib/clevic/delegates/combo_delegate.rb +156 -0
  15. data/lib/clevic/delegates/distinct_delegate.rb +48 -0
  16. data/lib/clevic/delegates/relational_delegate.rb +59 -0
  17. data/lib/clevic/delegates/set_delegate.rb +31 -0
  18. data/lib/clevic/field.rb +39 -55
  19. data/lib/clevic/field_valuer.rb +23 -10
  20. data/lib/clevic/filter_command.rb +22 -36
  21. data/lib/clevic/framework.rb +37 -0
  22. data/lib/clevic/generic_format.rb +5 -1
  23. data/lib/clevic/many_field.rb +28 -3
  24. data/lib/clevic/model_builder.rb +27 -32
  25. data/lib/clevic/ordered_dataset.rb +45 -0
  26. data/lib/clevic/qt.rb +4 -1
  27. data/lib/clevic/qt/action_builder.rb +9 -1
  28. data/lib/clevic/qt/browser.rb +1 -1
  29. data/lib/clevic/qt/clipboard.rb +3 -3
  30. data/lib/clevic/qt/combo_delegate.rb +25 -89
  31. data/lib/clevic/qt/delegate.rb +25 -0
  32. data/lib/clevic/qt/distinct_delegate.rb +5 -23
  33. data/lib/clevic/qt/extensions.rb +8 -1
  34. data/lib/clevic/qt/qt_combo_box.rb +58 -0
  35. data/lib/clevic/qt/relational_delegate.rb +18 -58
  36. data/lib/clevic/qt/set_delegate.rb +4 -34
  37. data/lib/clevic/qt/simplest_delegate.rb +19 -0
  38. data/lib/clevic/qt/table_model.rb +10 -10
  39. data/lib/clevic/qt/table_view.rb +7 -23
  40. data/lib/clevic/qt/text_delegate.rb +2 -2
  41. data/lib/clevic/qt/ui/browser_ui.rb +1 -1
  42. data/lib/clevic/qt/ui/search_dialog_ui.rb +1 -1
  43. data/lib/clevic/rails_models_loaders.rb +13 -0
  44. data/lib/clevic/record.rb +2 -2
  45. data/lib/clevic/sampler.rb +85 -39
  46. data/lib/clevic/sequel_ar_adapter.rb +1 -28
  47. data/lib/clevic/sequel_clevic.rb +68 -0
  48. data/lib/clevic/sequel_meta.rb +1 -13
  49. data/lib/clevic/subclasses.rb +18 -0
  50. data/lib/clevic/swing.rb +2 -1
  51. data/lib/clevic/swing/action.rb +27 -3
  52. data/lib/clevic/swing/action_builder.rb +0 -2
  53. data/lib/clevic/swing/browser.rb +1 -10
  54. data/lib/clevic/swing/combo_delegate.rb +45 -133
  55. data/lib/clevic/swing/delegate.rb +2 -0
  56. data/lib/clevic/swing/distinct_delegate.rb +2 -14
  57. data/lib/clevic/swing/relational_delegate.rb +2 -20
  58. data/lib/clevic/swing/set_delegate.rb +13 -28
  59. data/lib/clevic/swing/table_view.rb +1 -1
  60. data/lib/clevic/table_model.rb +3 -4
  61. data/lib/clevic/table_searcher.rb +10 -31
  62. data/lib/clevic/table_view.rb +97 -43
  63. data/lib/clevic/ui/browser_ui.rb +133 -0
  64. data/lib/clevic/ui/search_dialog_ui.rb +106 -0
  65. data/lib/clevic/version.rb +2 -2
  66. data/models/accounts_models.rb +24 -21
  67. data/models/times_models.rb +34 -28
  68. data/models/times_psql_models.rb +9 -3
  69. data/models/times_sqlite_models.rb +24 -1
  70. data/sql/times_sqlite.sql +3 -3
  71. data/tasks/clevic.rake +2 -2
  72. data/test/test_cache_table.rb +9 -19
  73. data/test/test_table_searcher.rb +2 -5
  74. metadata +95 -91
  75. data/lib/clevic/order_attribute.rb +0 -63
  76. data/lib/clevic/qt/boolean_delegate.rb +0 -8
  77. data/lib/clevic/qt/delegates.rb +0 -1
  78. data/lib/clevic/qt/item_delegate.rb +0 -66
  79. data/lib/clevic/sql_dialects.rb +0 -33
  80. data/tasks/website.rake +0 -25
  81. data/test/test_order_attribute.rb +0 -62
  82. data/test/test_sql_dialects.rb +0 -77
data/lib/clevic.rb CHANGED
@@ -6,8 +6,14 @@ module Clevic
6
6
  end
7
7
  end
8
8
 
9
- require 'clevic/sequel_ar_adapter.rb'
9
+ # TODO should this really be here?
10
+ # There are other inflection gems.
11
+ # JRuby-1.5.2 raises exception if this require has a .rb on the
12
+ require 'active_support/inflector'
13
+
14
+ require 'clevic/framework'
10
15
  require 'clevic/sequel_length_validation.rb'
11
16
  require 'clevic/record.rb'
12
17
  require 'clevic/view.rb'
13
18
  require 'clevic/sequel_meta.rb'
19
+ require 'clevic/sequel_clevic.rb'
@@ -84,7 +84,7 @@ module ActionBuilder
84
84
  # Create and return a list of actions. The actions are grouped together,
85
85
  # ie live together on the menu with a separator between groups.
86
86
  # A method called "#{group_name}_actions" will be added to self, which will return the
87
- # set of Qt::Action instances created in the block.
87
+ # set of framework-specific Action instances created in the block.
88
88
  def list( group_name, &block )
89
89
  @group_name = group_name
90
90
  group_names << group_name
@@ -160,6 +160,9 @@ protected
160
160
  unless instance_methods.include?( :action_triggered )
161
161
  def action_triggered( &someblock )
162
162
  yield
163
+ rescue
164
+ puts $!.message
165
+ puts $!.backtrace
163
166
  end
164
167
  end
165
168
 
@@ -2,6 +2,71 @@ require 'active_support'
2
2
  require 'active_support/core_ext/array/extract_options.rb'
3
3
 
4
4
  module Sequel
5
+ class Dataset
6
+ # Basically, we're translating from AR's hash options
7
+ # to Sequel's method algebra, and returning the resulting
8
+ # dataset.
9
+ def translate( options )
10
+ # recursively send key-value pairs to self
11
+ # and return the result
12
+ options.inject( self ) do |dataset, (key, value)|
13
+ case key
14
+ when :limit; dataset.limit( value, nil )
15
+ when :offset
16
+ # workaround for Sequel's refusal to do offset without limit
17
+ # not sure we need :all for >= 3.13.0
18
+ dataset.limit( options[:limit] || :all, value )
19
+
20
+ when :order
21
+ orders = value.split(/, */ ).map do |x|
22
+ case x
23
+ when /^(\w+) +(asc|desc)$/i
24
+ $1.to_sym.send( $2 )
25
+
26
+ when /^\w+$/i
27
+ x.to_sym
28
+
29
+ else
30
+ x.lit
31
+
32
+ end
33
+ end
34
+ dataset.order( *orders )
35
+
36
+ when :conditions
37
+ # this translation is not adequate for all use cases of the AR api
38
+ # specifically where value contains a SQL expression
39
+ unless value.nil?
40
+ possible_literal =
41
+ if value.is_a?( String )
42
+ value.lit
43
+ else
44
+ value
45
+ end
46
+
47
+ dataset.filter( possible_literal )
48
+ end
49
+
50
+ when :include
51
+ # this is the class to join
52
+ joined_class = eval( model.reflections[value][:class_name] )
53
+ dataset.join_table(
54
+ :inner,
55
+ joined_class,
56
+ joined_class.primary_key => model.reflections[value][:key]
57
+ ).select( model.table_name.* )
58
+
59
+ else
60
+ raise "#{key} not implemented"
61
+ # make sure at least it's unchanged, in case options is empty
62
+ end || dataset
63
+ end
64
+
65
+ rescue Exception => e
66
+ raise RuntimeError, "#{self} #{options.inspect} #{e.message}", caller(0)
67
+ end
68
+ end
69
+
5
70
  module Plugins
6
71
  module ArMethods
7
72
  # plugin :ar_methods calls this.
@@ -18,60 +83,10 @@ module Sequel
18
83
  # Copy the necessary class instance variables to the subclass.
19
84
  def inherited(subclass)
20
85
  super
21
- #~ store = @cache_store
22
- #~ ttl = @cache_ttl
23
- #~ cache_ignore_exceptions = @cache_ignore_exceptions
24
- #~ subclass.instance_eval do
25
- #~ @cache_store = store
26
- #~ @cache_ttl = ttl
27
- #~ @cache_ignore_exceptions = cache_ignore_exceptions
28
- #~ end
29
86
  end
30
87
 
31
- def lit_if_string( arg )
32
- if arg.is_a?( String )
33
- arg.lit
34
- else
35
- arg
36
- end
37
- end
38
-
39
- # Basically, we're translating from AR's hash options
40
- # to Sequel's method algebra, and returning the resulting
41
- # dataset.
42
88
  def translate( options )
43
- options.inject( dataset ) do |dataset, (key, value)|
44
- case key
45
- when :limit; dataset.limit( value, nil )
46
- when :offset
47
- # workaround for Sequel's refusal to do offset without limit
48
- dataset.limit( options[:limit] || :all, value )
49
-
50
- when :order
51
- dataset.order( lit_if_string( value ) )
52
-
53
- when :conditions
54
- # this is most likely not adequate for all use cases
55
- # of the AR api
56
- dataset.filter( lit_if_string( value ) ) unless value.nil?
57
-
58
- when :include
59
- # this is the class to joing
60
- joined_class = eval( reflections[value][:class_name] )
61
- dataset.join_table(
62
- :inner,
63
- joined_class,
64
- joined_class.primary_key => reflections[value][:key]
65
- ).select( table_name.* )
66
-
67
- else
68
- raise "#{key} not implemented"
69
- # make sure at least it's unchanged
70
- end || dataset
71
- end
72
-
73
- rescue Exception => e
74
- raise RuntimeError, "#{self.name} #{options.inspect} #{e.message}", caller(0)
89
+ dataset.translate( options )
75
90
  end
76
91
 
77
92
  def find_ar( *args )
@@ -82,19 +97,19 @@ module Sequel
82
97
 
83
98
  case args.first
84
99
  when :first
85
- translate(options).first
100
+ dataset.translate(options).first
86
101
 
87
102
  when :last
88
- translate(options).last
103
+ dataset.translate(options).last
89
104
 
90
105
  when :all
91
- translate(options).all
106
+ dataset.translate(options).all
92
107
 
93
108
  else
94
109
  if args.size == 1
95
- translate(options).filter( :id.qualify( table_name ) => args.first ).first
110
+ dataset.translate(options).filter( :id.qualify( table_name ) => args.first ).first
96
111
  else
97
- translate(options).filter( :id.qualify( table_name ) => args ).all
112
+ dataset.translate(options).filter( :id.qualify( table_name ) => args ).all
98
113
  end
99
114
  end
100
115
  end
@@ -103,7 +118,7 @@ module Sequel
103
118
  options = args.extract_options!
104
119
  attribute = args.first
105
120
 
106
- dataset = translate( options )
121
+ dataset = dataset.translate( options )
107
122
 
108
123
  unless attribute.nil?
109
124
  dataset = dataset.select( attribute )
@@ -1,4 +1,8 @@
1
1
  module Clevic
2
+
3
+ # Provide a list of entries for a distinct field, ordered
4
+ # by either value order, or frequency order.
5
+ # TODO move this into the common DistinctDelegate class.
2
6
  class AttributeList
3
7
  def initialize( entity_class, attribute, attribute_value, find_options )
4
8
  @entity_class = entity_class
@@ -1,7 +1,9 @@
1
- require 'clevic/table_searcher.rb'
2
- require 'clevic/order_attribute.rb'
1
+ require 'clevic/table_searcher'
2
+ require 'clevic/ordered_dataset'
3
3
  require 'bsearch'
4
4
 
5
+ module Clevic
6
+
5
7
  =begin rdoc
6
8
  Fetch rows from the db on demand, rather than all up front.
7
9
 
@@ -18,76 +20,34 @@ TODO drop rows when they haven't been accessed for a while
18
20
  TODO how to handle a quickly-changing underlying table? invalidate cache
19
21
  for each call?
20
22
 
21
- TODO use Sequel instead of order_attributes
23
+ TODO figure out how to handle situations where the character set ordering
24
+ in the db and in Ruby are different.
22
25
  =end
23
26
  class CacheTable < Array
27
+
28
+ include OrderedDataset
29
+
24
30
  # the number of records loaded in one call to the db
25
31
  attr_accessor :preload_count
26
- attr_reader :find_options, :entity_class
32
+ attr_reader :entity_class
27
33
 
28
- def initialize( entity_class, find_options = {} )
29
- @preload_count = 20
30
- # must be before sanitise_options
34
+ def initialize( entity_class, dataset = nil )
35
+ @preload_count = 30
31
36
  @entity_class = entity_class
32
- # must be before anything that uses options
33
- @find_options = (find_options || {}).clone
34
- sanitise_options!
37
+ # defined in OrderAttributes
38
+ self.dataset = dataset || entity_class.dataset
35
39
 
36
40
  # size the array and fill it with nils. They'll be filled
37
41
  # in by the [] operator
38
- @row_count = sql_count
39
- super( @row_count )
42
+ super( sql_count )
40
43
  end
41
44
 
42
45
  # The count of the records according to the db, which may be different to
43
46
  # the records in the cache
44
47
  def sql_count
45
- entity_class.adaptor.count( find_options.reject{|k,v| k == :order} )
48
+ dataset.count
46
49
  end
47
50
 
48
- # Return the set of OrderAttribute objects for this collection.
49
- # If no order attributes are specified, the primary key will be used.
50
- # TODO what about compound primary keys?
51
- def order_attributes
52
- # This is sorted in @find_options[:order], so use that for the search
53
- if @order_attributes.nil?
54
- @order_attributes = find_options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@entity_class, x)}
55
-
56
- # add the primary key if nothing is specified
57
- # because we need an ordering of some kind otherwise
58
- # index_for_entity will not work
59
- unless @order_attributes.any? {|x| x.attribute.to_s == entity_class.primary_key.to_s }
60
- @order_attributes << OrderAttribute.new( entity_class, entity_class.primary_key )
61
- end
62
- end
63
- @order_attributes
64
- end
65
-
66
- # add an id to options[:order] if it's not in there
67
- # make sure options[:conditions] uses db values rather than objects
68
- # ie convert { :debit => DebitObject<...> } to { :debit_id => 5 }
69
- def sanitise_options!
70
- # make sure we have a string here, even if it's blank
71
- # value would be a ,-separated list of order by fields (expressions?)
72
- find_options[:order] ||= ''
73
-
74
- # recreate the options[:order] entry to include default
75
- find_options[:order] = order_attributes.map{|x| x.to_sql}.join(',')
76
-
77
- # make sure objects are converted to ids
78
- if find_options.has_key?( :conditions ) && find_options[:conditions].is_a?( Hash )
79
- conditions = find_options[:conditions].map do |key,value|
80
- metadata = entity_class.meta[key]
81
- if metadata.association?
82
- [metadata.key, value.send( value.primary_key )]
83
- else
84
- [key,value]
85
- end
86
- end
87
- find_options[:conditions] = Hash[ *conditions.flatten ]
88
- end
89
- end
90
-
91
51
  # Execute the block with the specified preload_count,
92
52
  # and restore the existing one when done.
93
53
  # Return the value of the block
@@ -104,14 +64,14 @@ class CacheTable < Array
104
64
  # hits on the db
105
65
  def fetch_entity( index )
106
66
  # calculate negative indices for the SQL offset
107
- offset = index < 0 ? index + @row_count : index
67
+ offset = index < 0 ? index + sql_count : index
108
68
 
109
69
  # fetch self.preload_count records
110
- records = entity_class.adaptor.find( :all, find_options.merge( :offset => offset, :limit => preload_count ) )
70
+ records = dataset.limit( preload_count, offset )
111
71
  records.each_with_index {|x,i| self[i+index] = x if !cached_at?( i+index )}
112
72
 
113
73
  # return the first one
114
- records[0]
74
+ records.first
115
75
  end
116
76
 
117
77
  # return the entity at the given index. Fetch it from the
@@ -120,11 +80,25 @@ class CacheTable < Array
120
80
  super( index ) || fetch_entity( index )
121
81
  end
122
82
 
123
- # make a new instance that has the attributes of this one, but an empty
124
- # data set. pass in ActiveRecord options to filter.
125
- def renew( args = nil )
126
- clear
127
- self.class.new( entity_class, args || find_options )
83
+ # Make a new instance based on the current dataset.
84
+ # Unless new_dataset is specified, pass the dataset
85
+ # to the block, and use the return
86
+ # value from the block as the new dataset.
87
+ #
88
+ # This is so that filter of datasets can be based on the
89
+ # existing one, but it's easy to go back to previous data
90
+ # sets if necessary.
91
+ # TODO write tests for both cases.
92
+ def renew( new_dataset = nil, &block )
93
+ if new_dataset && block_given?
94
+ raise "Passing a new dataset and a modification block doesn't make sense."
95
+ end
96
+
97
+ if block_given?
98
+ self.class.new( entity_class, block.call( dataset ) )
99
+ else
100
+ self.class.new( entity_class, new_dataset || dataset )
101
+ end
128
102
  end
129
103
 
130
104
  # key is what we're searching for. candidate
@@ -165,14 +139,12 @@ class CacheTable < Array
165
139
  bsearch do |candidate|
166
140
  # find using all sort attributes
167
141
  order_attributes.inject(0) do |result,attribute|
168
- # value from the block should be in [-1,0,1],
142
+ # result from the block should be in [-1,0,1],
169
143
  # similar to candidate <=> entity
144
+ key, direction = attribute
170
145
  if result == 0
171
- # they're equal, so compare attribute values
172
- method = attribute.attribute.to_sym
173
-
174
146
  # compare taking ordering direction into account
175
- retval = compare( entity.send( method ), candidate.send( method ), attribute.to_i )
147
+ retval = compare( entity.send( key ), candidate.send( key ), direction )
176
148
 
177
149
  # exit now because we have a difference
178
150
  next( retval ) if retval != 0
@@ -189,6 +161,8 @@ class CacheTable < Array
189
161
  end
190
162
  end
191
163
 
164
+ end
165
+
192
166
  # This is part of Array in case the programmer wants to use
193
167
  # a simple array instead of a CacheTable.
194
168
  class Array
@@ -0,0 +1,22 @@
1
+ module Clevic
2
+
3
+ # Tricky, this. Keeps passing on the dataset and
4
+ # lets it build up, but keeps the result.
5
+ # Used in the UI block to make a nice syntax for specifying the dataset.
6
+ class DatasetRoller
7
+ def initialize( original_dataset )
8
+ @rolling_dataset = original_dataset
9
+ end
10
+
11
+ def dataset
12
+ @rolling_dataset
13
+ end
14
+
15
+ def method_missing(meth, *args, &block)
16
+ @rolling_dataset = @rolling_dataset.send( meth, *args, &block )
17
+ self
18
+ end
19
+ end
20
+
21
+
22
+ end
@@ -8,12 +8,21 @@ class Delegate
8
8
  include FieldValuer
9
9
 
10
10
  def initialize( field )
11
+ super()
11
12
  @field = field
12
13
  end
13
14
 
14
- attr_accessor :entity, :parent
15
+ # This is the ORM entity instance for which this delegate
16
+ # is editing a single field. It needs to be the entire entity
17
+ # so we can set the edited field value on it.
18
+ attr_accessor :entity
15
19
 
20
+ # The parent widget of this delegate / this delegate's widget
21
+ attr_accessor :parent
22
+
23
+ # the Clevic::Field instance which this delegate edits.
16
24
  attr_reader :field
25
+
17
26
  def attribute
18
27
  field.attribute
19
28
  end
@@ -22,10 +31,7 @@ class Delegate
22
31
  field.entity_class
23
32
  end
24
33
 
25
- def find_options
26
- field.find_options
27
- end
28
-
34
+ # assume this is not a combo delegate. That will come later.
29
35
  def is_combo?
30
36
  false
31
37
  end
@@ -0,0 +1,156 @@
1
+ require 'andand'
2
+
3
+ module Clevic
4
+
5
+ =begin rdoc
6
+ Base class for other delegates using Combo boxes.
7
+ =end
8
+ # TODO this should be a module
9
+ class ComboDelegate
10
+ # Return the GUI component / widget that is displayed when editing.
11
+ # Usually this will be a combo box widget, but it can be a text editor
12
+ # in some cases.
13
+ # if editor is a combo it must support no_insert=( bool )
14
+ attr_reader :editor
15
+
16
+ # Return a string to be shown to the user.
17
+ # model_value is an item stored in the combo box model.
18
+ def display_for( model_value )
19
+ field.transform_attribute( model_value )
20
+ end
21
+
22
+ # Some GUIs (Qt) can just set this. Swing can't.
23
+ def configure_prefix
24
+ end
25
+
26
+ # TODO kinda redundant because all combos must be editable
27
+ # to support prefix matching
28
+ def configure_editable
29
+ editor.editable = true
30
+ end
31
+
32
+ # this will create the actual combo box widget
33
+ framework_responsibility :create_combo_box
34
+
35
+ # framework-specific code goes in here
36
+ # it's called right at the end of init_component, once all the other setup
37
+ # is done. Mainly it's so that event handlers can be attached
38
+ # to the combo box without having to deal with events triggered
39
+ # by setup code.
40
+ framework_responsibility :framework_setup
41
+
42
+ # This is called by the combo box to convert an item
43
+ # to something that the combo can insert into
44
+ # itself. Usually this will be a display value
45
+ # and a storage value.
46
+ framework_responsibility :item_to_editor
47
+
48
+ # This is called by the combo box when it needs to convert a
49
+ # storage value to an item, which is something that the delegate
50
+ # will understand.
51
+ framework_responsibility :editor_to_item
52
+
53
+ # Create a GUI widget and fill it with the possible values.
54
+ # *args will be passed as-is to framework_setup
55
+ # NOTE RIght now it's the framework's responsibility to call
56
+ # self.entity = some_entity
57
+ # There must be a good way to check that though.
58
+ def init_component( *args )
59
+ if needs_combo?
60
+ @editor = create_combo_box( *args )
61
+ @editor.delegate = self
62
+
63
+ # add all entries from population
64
+ population.each do |item|
65
+ editor << item
66
+ end
67
+
68
+ # create a nil entry if necessary
69
+ if allow_null? && !editor.include?( nil )
70
+ editor << nil
71
+ end
72
+
73
+ # don't allow inserts if the delegate is restricted
74
+ editor.no_insert = restricted?
75
+
76
+ # set the correct value in the list
77
+ editor.selected_item = entity.nil? ? nil : attribute_value
78
+
79
+ # set up prefix matching when typing in the editor
80
+ configure_prefix
81
+
82
+ framework_setup( *args )
83
+ else
84
+ @editor =
85
+ if restricted?
86
+ show_message( empty_set_message )
87
+ nil
88
+ else
89
+ # there is no data yet for the combo, and it's
90
+ # not restricted, so just edit with a simple text field.
91
+ line_editor( edit_value )
92
+ end
93
+ end
94
+ editor
95
+ end
96
+
97
+ # open the combo box, just like if F4 was pressed
98
+ framework_responsibility :full_edit
99
+
100
+ # show only the text editor part, not the drop-down
101
+ def minimal_edit
102
+ editor.hide_popup if is_combo?
103
+ end
104
+
105
+ # returns true if the editor allows values outside of a predefined
106
+ # range, false otherwise.
107
+ def restricted?
108
+ false
109
+ end
110
+
111
+ # TODO fetch this from the model definition
112
+ def allow_null?
113
+ true
114
+ end
115
+
116
+ # Subclasses should override this to prove a list of
117
+ # values to be used by the combo box. Values could
118
+ # be pretty much anything, depending on the delegate.
119
+ # For example, a RelationalDelegate will have a collection
120
+ # of entity objects, most other delegates will have collections
121
+ # of strings.
122
+ subclass_responsibility :population
123
+
124
+ # Return true if this delegate needs a combo, false otherwise
125
+ # ie if there are no values yet and it's not restricted, then a
126
+ # full combo doesn't make sense
127
+ subclass_responsibility :needs_combo?
128
+
129
+ # return true if this delegate has/needs a combo widget
130
+ # or false if it's a plain text field.
131
+ framework_responsibility :is_combo?
132
+
133
+ # return true if this field has no data (needs_combo? is false)
134
+ # and is at the same time restricted (ie needs data from somewhere else)
135
+ def empty_set?
136
+ !needs_combo? && restricted?
137
+ end
138
+
139
+ # the message to display if the set is empty, and
140
+ # the delegate is restricted to a predefined set.
141
+ subclass_responsibility :empty_set_message
142
+
143
+ # if this delegate has an empty set, return the message, otherwise
144
+ # return nil.
145
+ def if_empty_message
146
+ empty_set_message if empty_set?
147
+ end
148
+
149
+ # the value represented by the combo, ie either
150
+ # the current attribute_value of the field
151
+ # this combo is editing, or an object that could
152
+ # be a new attribute_value
153
+ framework_responsibility :value
154
+ end
155
+
156
+ end