clevic 0.12.0 → 0.13.0.b1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +209 -30
  3. data/README.txt +16 -20
  4. data/Rakefile +8 -8
  5. data/TODO +6 -7
  6. data/bin/clevic +12 -73
  7. data/lib/clevic/action_builder.rb +168 -0
  8. data/lib/clevic/ar_methods.rb +120 -0
  9. data/lib/clevic/attribute_list.rb +56 -0
  10. data/lib/clevic/cache_table.rb +60 -37
  11. data/lib/clevic/default_view.rb +3 -16
  12. data/lib/clevic/delegate.rb +46 -0
  13. data/lib/clevic/emitter.rb +38 -0
  14. data/lib/clevic/extensions.rb +61 -114
  15. data/lib/clevic/field.rb +159 -228
  16. data/lib/clevic/field_valuer.rb +165 -0
  17. data/lib/clevic/filter_command.rb +2 -6
  18. data/lib/clevic/generic_format.rb +52 -0
  19. data/lib/clevic/{ui → icons}/icon.png +0 -0
  20. data/lib/clevic/many_field.rb +7 -0
  21. data/lib/clevic/model_builder.rb +234 -146
  22. data/lib/clevic/model_column.rb +61 -13
  23. data/lib/clevic/order_attribute.rb +10 -0
  24. data/lib/clevic/qt.rb +35 -0
  25. data/lib/clevic/qt/action_builder.rb +47 -0
  26. data/lib/clevic/qt/boolean_delegate.rb +8 -0
  27. data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
  28. data/lib/clevic/qt/clipboard.rb +35 -0
  29. data/lib/clevic/qt/combo_delegate.rb +198 -0
  30. data/lib/clevic/qt/delegates.rb +1 -0
  31. data/lib/clevic/qt/distinct_delegate.rb +35 -0
  32. data/lib/clevic/qt/extensions.rb +52 -0
  33. data/lib/clevic/qt/field.rb +18 -0
  34. data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
  35. data/lib/clevic/qt/relational_delegate.rb +87 -0
  36. data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
  37. data/lib/clevic/qt/set_delegate.rb +44 -0
  38. data/lib/clevic/qt/table_model.rb +331 -0
  39. data/lib/clevic/qt/table_view.rb +344 -0
  40. data/lib/clevic/qt/text_area_delegate.rb +8 -0
  41. data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
  42. data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
  43. data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
  44. data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
  45. data/lib/clevic/rails_models_loaders.rb +56 -0
  46. data/lib/clevic/record.rb +2 -17
  47. data/lib/clevic/sampler.rb +81 -0
  48. data/lib/clevic/sequel_ar_adapter.rb +215 -0
  49. data/lib/clevic/sequel_length_validation.rb +23 -0
  50. data/lib/clevic/sequel_meta.rb +65 -0
  51. data/lib/clevic/sequel_naked.rb +30 -0
  52. data/lib/clevic/swing.rb +38 -0
  53. data/lib/clevic/swing/action.rb +125 -0
  54. data/lib/clevic/swing/action_builder.rb +47 -0
  55. data/lib/clevic/swing/boolean_delegate.rb +26 -0
  56. data/lib/clevic/swing/browser.rb +282 -0
  57. data/lib/clevic/swing/cell_editor.rb +95 -0
  58. data/lib/clevic/swing/cell_renderer.rb +44 -0
  59. data/lib/clevic/swing/clipboard.rb +135 -0
  60. data/lib/clevic/swing/combo_delegate.rb +336 -0
  61. data/lib/clevic/swing/confirm_dialog.rb +57 -0
  62. data/lib/clevic/swing/delegate.rb +40 -0
  63. data/lib/clevic/swing/distinct_delegate.rb +30 -0
  64. data/lib/clevic/swing/extensions.rb +274 -0
  65. data/lib/clevic/swing/field.rb +35 -0
  66. data/lib/clevic/swing/relational_delegate.rb +48 -0
  67. data/lib/clevic/swing/row_header.rb +210 -0
  68. data/lib/clevic/swing/search_dialog.rb +230 -0
  69. data/lib/clevic/swing/selection_model.rb +90 -0
  70. data/lib/clevic/swing/set_delegate.rb +41 -0
  71. data/lib/clevic/swing/swing_table_index.rb +43 -0
  72. data/lib/clevic/swing/table_model.rb +200 -0
  73. data/lib/clevic/swing/table_view.rb +385 -0
  74. data/lib/clevic/swing/table_view_focus.rb +47 -0
  75. data/lib/clevic/swing/tag_delegate.rb +127 -0
  76. data/lib/clevic/swing/tag_editor.rb +101 -0
  77. data/lib/clevic/swing/text_area_delegate.rb +46 -0
  78. data/lib/clevic/swing/text_delegate.rb +31 -0
  79. data/lib/clevic/swing/ui/build.xml +74 -0
  80. data/lib/clevic/swing/ui/dist/README.TXT +33 -0
  81. data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
  82. data/lib/clevic/swing/ui/manifest.mf +3 -0
  83. data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
  84. data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
  85. data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
  86. data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
  87. data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
  88. data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
  89. data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
  90. data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
  91. data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
  92. data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
  93. data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
  94. data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
  95. data/lib/clevic/table_index.rb +100 -0
  96. data/lib/clevic/table_model.rb +54 -425
  97. data/lib/clevic/table_searcher.rb +113 -116
  98. data/lib/clevic/table_view.rb +171 -399
  99. data/lib/clevic/table_view_paste.rb +199 -0
  100. data/lib/clevic/version.rb +3 -2
  101. data/lib/clevic/view.rb +94 -43
  102. data/models/accounts_models.rb +13 -13
  103. data/models/minimal_models.rb +5 -9
  104. data/models/times_models.rb +19 -14
  105. data/models/times_psql_models.rb +10 -0
  106. data/models/times_sqlite_models.rb +1 -8
  107. data/models/values_models.rb +2 -8
  108. data/tasks/clevic.rake +1 -1
  109. data/tasks/rdoc.rake +1 -5
  110. data/tasks/website.rake +1 -1
  111. data/test/test_cache_table.rb +15 -29
  112. data/test/test_helper.rb +14 -83
  113. data/test/test_order_attribute.rb +1 -1
  114. data/test/test_table_model.rb +0 -21
  115. data/test/test_table_searcher.rb +67 -61
  116. metadata +262 -78
  117. data/lib/clevic.rb +0 -4
  118. data/lib/clevic/db_options.rb +0 -112
  119. data/lib/clevic/delegates.rb +0 -386
@@ -0,0 +1,168 @@
1
+ module Clevic
2
+
3
+ =begin rdoc
4
+ This module is included in a class to make the construction of
5
+ collections of actions more rubyish. It must have
6
+ - an add_action method
7
+ - separator, which returns something which is_a? Separator
8
+ - create_action( &block ), which creates an Action object
9
+ - action_method_or_block( action, options, &block ) which handles events
10
+
11
+ Menus are generally made up of a collection of actions.
12
+
13
+ Once included, it's intended to be called as follows:
14
+ def some_setup_method_or_other
15
+ build_actions do
16
+ list :edit do
17
+ #~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
18
+ action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
19
+ action :action_paste, '&Paste', :shortcut => Qt::KeySequence::Paste, :method => :paste
20
+ separator
21
+ action :action_ditto, '&Ditto', :shortcut => 'Ctrl+\'', :method => :ditto, :tool_tip => 'Copy same field from previous record'
22
+ action :action_ditto_right, 'Ditto R&ight', :shortcut => 'Ctrl+]', :method => :ditto_right, :tool_tip => 'Copy field one to right from previous record'
23
+ action :action_ditto_left, '&Ditto L&eft', :shortcut => 'Ctrl+[', :method => :ditto_left, :tool_tip => 'Copy field one to left from previous record'
24
+ action :action_insert_date, 'Insert Date', :shortcut => 'Ctrl+;', :method => :insert_current_date
25
+ action :action_open_editor, '&Open Editor', :shortcut => 'F4', :method => :open_editor
26
+ separator
27
+ action :action_row, 'New Ro&w', :shortcut => 'Ctrl+N', :method => :row
28
+ action :action_refresh, '&Refresh', :shortcut => 'Ctrl+R', :method => :refresh
29
+ action :action_delete_rows, 'Delete Rows', :shortcut => 'Ctrl+Delete', :method => :delete_rows
30
+
31
+ if $options[:debug]
32
+ action :action_dump, 'D&ump', :shortcut => 'Ctrl+Shift+D' do
33
+ puts model.collection[current_index.row].inspect
34
+ end
35
+ end
36
+ end
37
+
38
+ separator
39
+ end
40
+ end
41
+ Or you can pass a parameter to the block if you need access to surrounding variables:
42
+ build_actions do |ab|
43
+ ab.list :edit do
44
+ #~ new_action :action_cut, 'Cu&t', :shortcut => Qt::KeySequence::Cut
45
+ ab.action :action_copy, '&Copy', :shortcut => Qt::KeySequence::Copy, :method => :copy_current_selection
46
+ end
47
+ end
48
+ If the including class defines a method called action_triggered( &block ),
49
+ it can be used to wrap the code triggered by actions. That way, the
50
+ including class
51
+ can catch exceptions and things like that.
52
+ def action_triggered( &block )
53
+ catch :something_happened do
54
+ yield
55
+ end
56
+ end
57
+ If this method is not defined, it will be created in the including class as an empty wrapper.
58
+ =end
59
+ module ActionBuilder
60
+ # raise a RuntimeError if the including class/module does not define add_action
61
+ def self.included( including_module )
62
+ shortlist = including_module.instance_methods.grep /action/i
63
+ # add_action is actually an method_missing lookup for addAction, so
64
+ # search for both.
65
+ unless shortlist.any? {|x| %w{add_action addAction}.include?( x )}
66
+ raise NotImplementedError, "#{including_module.class.name} must have an add_action method"
67
+ end
68
+ end
69
+
70
+ # Outer block for the build process.
71
+ def build_actions( &block )
72
+ raise 'a block must be present' if block.nil?
73
+ if block.arity == -1
74
+ instance_eval &block
75
+ else
76
+ yield self
77
+ end
78
+ end
79
+
80
+ def group_names
81
+ @group_names ||= []
82
+ end
83
+
84
+ # Create and return a list of actions. The actions are grouped together,
85
+ # ie live together on the menu with a separator between groups.
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.
88
+ def list( group_name, &block )
89
+ @group_name = group_name
90
+ group_names << group_name
91
+ unless respond_to?( "#{group_name.to_s}_actions" )
92
+ self.class.send( :define_method, "#{group_name.to_s}_actions" ) do
93
+ eval "@#{group_name.to_s}_actions"
94
+ end
95
+ end
96
+ self.collect_actions = []
97
+
98
+ yield( self )
99
+ # copy actions to the right instance variable
100
+ eval "@#{group_name.to_s}_actions = collect_actions"
101
+
102
+ # reset these, just for cleanliness
103
+ @group_name = nil
104
+ self.collect_actions = []
105
+ end
106
+
107
+ # Create a new Action and
108
+ # 1. pass it to add_action
109
+ # 1. add it to the collect_actions collection.
110
+ # The block takes predence over options[:method], which is a method
111
+ # on self to be called.
112
+ # Option keys can be any method in Action, ie :tool_tip, :shortcut, :status_tip etc.
113
+ # A value for :shortcut is automatically passed to create_key_sequence
114
+ def action( name_or_action, text = nil, options = {}, &block )
115
+ if name_or_action.is_a? Action
116
+ add_action( name_or_action )
117
+ else
118
+ name = name_or_action
119
+ if options.has_key?( :method ) && !block.nil?
120
+ raise "you can't specify both :method and a block"
121
+ end
122
+
123
+ create_action do |action|
124
+ action.name = name.to_s
125
+ action.text = text
126
+ options.each do |k,v|
127
+ next if k == :method
128
+ if k == :shortcut
129
+ action.shortcut = v
130
+ else
131
+ action.send( "#{k.to_s}=", v )
132
+ end
133
+ end
134
+
135
+ # add action
136
+ add_action action
137
+
138
+ # add actions for list. Yes, it's a side-effect.
139
+ # TODO is there a better way to do this?
140
+ collect_actions << action
141
+
142
+ action_method_or_block( action, options, &block )
143
+ end
144
+ end
145
+ end
146
+
147
+ protected
148
+
149
+ # the set of actions created so far in a particular list.
150
+ def collect_actions
151
+ @collect_actions ||= []
152
+ end
153
+
154
+ def collect_actions=( arr )
155
+ @collect_actions = arr
156
+ end
157
+
158
+ # If parent doesn't define this, add it so that
159
+ # our action_method_or_block will work.
160
+ unless instance_methods.include?( :action_triggered )
161
+ def action_triggered( &someblock )
162
+ yield
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ end # Clevic
@@ -0,0 +1,120 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/array/extract_options.rb'
3
+
4
+ module Sequel
5
+ module Plugins
6
+ module ArMethods
7
+ # plugin :ar_methods calls this.
8
+ # model is the model class. The rest is whatever options are
9
+ # in the plugin call, possibility for
10
+ # plugin :ar_methods :override_sequel => true
11
+ def self.configure(model, options = {})
12
+ model.instance_eval do
13
+ # store model-related stuff here
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ # Copy the necessary class instance variables to the subclass.
19
+ def inherited(subclass)
20
+ 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
+ end
30
+
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
+ 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)
75
+ end
76
+
77
+ def find_ar( *args )
78
+ # copied from ActiveRecord::Base.find
79
+ options = args.extract_options!
80
+ #~ validate_find_options(options)
81
+ #~ set_readonly_option!(options)
82
+
83
+ case args.first
84
+ when :first
85
+ translate(options).first
86
+
87
+ when :last
88
+ translate(options).last
89
+
90
+ when :all
91
+ translate(options).all
92
+
93
+ else
94
+ if args.size == 1
95
+ translate(options).filter( :id.qualify( table_name ) => args.first ).first
96
+ else
97
+ translate(options).filter( :id.qualify( table_name ) => args ).all
98
+ end
99
+ end
100
+ end
101
+
102
+ def count_ar( *args )
103
+ options = args.extract_options!
104
+ attribute = args.first
105
+
106
+ dataset = translate( options )
107
+
108
+ unless attribute.nil?
109
+ dataset = dataset.select( attribute )
110
+ end
111
+ dataset.count
112
+ end
113
+
114
+ end
115
+
116
+ module InstanceMethods
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,56 @@
1
+ module Clevic
2
+ class AttributeList
3
+ def initialize( entity_class, attribute, attribute_value, find_options )
4
+ @entity_class = entity_class
5
+ @attribute, @attribute_value, @find_options = attribute, attribute_value, find_options
6
+ end
7
+ attr_reader :entity_class, :attribute, :attribute_value, :find_options
8
+
9
+ # because Sequel::Dataset won't .filter with {}
10
+ def conditions( dataset )
11
+ # make sure the current attribute value is included if there's a filter
12
+ rv =
13
+ if find_options.has_key?( :conditions )
14
+ find_options[:conditions].lit | { attribute => attribute_value }
15
+ end
16
+
17
+ # filter if necessary
18
+ unless rv.nil?
19
+ dataset.filter( rv )
20
+ else
21
+ dataset
22
+ end
23
+ end
24
+
25
+ # sorts by attribute
26
+ def dataset_by_description
27
+ # must have attribute equality test first, otherwise if find_options
28
+ # doesn't have :conditions, then we end up with ( nil | { attribute => attribute_value } )
29
+ # which confuses Sequel
30
+ ds = entity_class.naked \
31
+ .order( attribute ) \
32
+ .select( attribute ) \
33
+ .distinct
34
+ conditions( ds )
35
+ end
36
+
37
+ # sorts by first letter then most frequent, instead of pure alphabetical
38
+ def dataset_by_frequency
39
+ ds = entity_class.naked \
40
+ .select( attribute, :count.sql_function( attribute ) ) \
41
+ .group( attribute ) \
42
+ .order( :substr.sql_function( attribute,1,1 ), :count.sql_function( attribute ).desc )
43
+ conditions( ds )
44
+ end
45
+
46
+ # by_frequency is the default
47
+ def dataset( by_description, by_frequency )
48
+ case
49
+ when by_description
50
+ dataset_by_description
51
+ else
52
+ dataset_by_frequency
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,16 +1,7 @@
1
- require 'rubygems'
2
- require 'active_record'
3
1
  require 'clevic/table_searcher.rb'
4
2
  require 'clevic/order_attribute.rb'
5
3
  require 'bsearch'
6
4
 
7
- begin
8
- require 'active_record/dirty.rb'
9
- rescue MissingSourceFile
10
- require 'clevic/dirty.rb'
11
- end
12
-
13
-
14
5
  =begin rdoc
15
6
  Fetch rows from the db on demand, rather than all up front.
16
7
 
@@ -26,18 +17,20 @@ TODO drop rows when they haven't been accessed for a while
26
17
 
27
18
  TODO how to handle a quickly-changing underlying table? invalidate cache
28
19
  for each call?
20
+
21
+ TODO use Sequel instead of order_attributes
29
22
  =end
30
23
  class CacheTable < Array
31
24
  # the number of records loaded in one call to the db
32
25
  attr_accessor :preload_count
33
- attr_reader :options, :entity_class
26
+ attr_reader :find_options, :entity_class
34
27
 
35
28
  def initialize( entity_class, find_options = {} )
36
29
  @preload_count = 20
37
30
  # must be before sanitise_options
38
31
  @entity_class = entity_class
39
32
  # must be before anything that uses options
40
- @options = find_options.clone
33
+ @find_options = (find_options || {}).clone
41
34
  sanitise_options!
42
35
 
43
36
  # size the array and fill it with nils. They'll be filled
@@ -49,21 +42,21 @@ class CacheTable < Array
49
42
  # The count of the records according to the db, which may be different to
50
43
  # the records in the cache
51
44
  def sql_count
52
- entity_class.count( options.reject{|k,v| k == :order} )
45
+ entity_class.adaptor.count( find_options.reject{|k,v| k == :order} )
53
46
  end
54
47
 
55
48
  # Return the set of OrderAttribute objects for this collection.
56
49
  # If no order attributes are specified, the primary key will be used.
57
- # TODO what about compund primary keys?
50
+ # TODO what about compound primary keys?
58
51
  def order_attributes
59
52
  # This is sorted in @options[:order], so use that for the search
60
53
  if @order_attributes.nil?
61
- @order_attributes = @options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@entity_class, x)}
54
+ @order_attributes = find_options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@entity_class, x)}
62
55
 
63
56
  # add the primary key if nothing is specified
64
57
  # because we need an ordering of some kind otherwise
65
58
  # index_for_entity will not work
66
- if !@order_attributes.any? {|x| x.attribute == entity_class.primary_key }
59
+ unless @order_attributes.any? {|x| x.attribute.to_s == entity_class.primary_key.to_s }
67
60
  @order_attributes << OrderAttribute.new( entity_class, entity_class.primary_key )
68
61
  end
69
62
  end
@@ -71,14 +64,28 @@ class CacheTable < Array
71
64
  end
72
65
 
73
66
  # add an id to options[:order] if it's not in there
74
- # also create @order_attributes
67
+ # make sure options[:conditions] uses db values rather than objects
68
+ # ie convert { :debit => DebitObject<...> } to { :debit_id => 5 }
75
69
  def sanitise_options!
76
70
  # make sure we have a string here, even if it's blank
77
- options[:order] ||= ''
71
+ # value would be a ,-separated list of order by fields (expressions?)
72
+ find_options[:order] ||= ''
78
73
 
79
74
  # recreate the options[:order] entry to include default
80
- # TODO why though? Can't remember
81
- options[:order] = order_attributes.map{|x| x.to_sql}.join(',')
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
82
89
  end
83
90
 
84
91
  # Execute the block with the specified preload_count,
@@ -100,7 +107,7 @@ class CacheTable < Array
100
107
  offset = index < 0 ? index + @row_count : index
101
108
 
102
109
  # fetch self.preload_count records
103
- records = entity_class.find( :all, options.merge( :offset => offset, :limit => preload_count ) )
110
+ records = entity_class.adaptor.find( :all, find_options.merge( :offset => offset, :limit => preload_count ) )
104
111
  records.each_with_index {|x,i| self[i+index] = x if !cached_at?( i+index )}
105
112
 
106
113
  # return the first one
@@ -115,15 +122,38 @@ class CacheTable < Array
115
122
 
116
123
  # make a new instance that has the attributes of this one, but an empty
117
124
  # data set. pass in ActiveRecord options to filter.
118
- # TODO using named scopes might make filtering easier.
119
125
  def renew( args = nil )
120
126
  clear
121
- self.class.new( entity_class, args || options )
127
+ self.class.new( entity_class, args || find_options )
128
+ end
129
+
130
+ # key is what we're searching for. candidate
131
+ # is what the current candidate is. direction is 1
132
+ # for sorted ascending, and -1 for sorted descending
133
+ # TODO retrieve nulls first/last from dataset. In sequel (>3.13.0)
134
+ # this is related to entity_class.filter( :release_date.desc(:nulls=>:first), :name.asc(:nulls=>:last) )
135
+ def compare( key, candidate, direction )
136
+ if ( key_nil = key.nil? ) || candidate.nil?
137
+ if key == candidate
138
+ # both nil, ie equal
139
+ 0
140
+ else
141
+ # assume nil is sorted greater
142
+ # TODO this should be retrieved from the db
143
+ # ie candidate(nil) <=> key is 1
144
+ # and key <=> candidate(nil) is -1
145
+ key_nil ? -1 : 1
146
+ end
147
+ else
148
+ candidate <=> key
149
+ end * direction
150
+ # reverse the result if we're searching a desc attribute,
151
+ # where direction will be -1
122
152
  end
123
153
 
124
154
  # find the index for the given entity, using a binary search algorithm (bsearch).
125
155
  # The order_by ActiveRecord style options are used to do the binary search.
126
- # 0 is returned if the entity is nil
156
+ # nil is returned if the entity is nil
127
157
  # nil is returned if the array is empty
128
158
  def index_for_entity( entity )
129
159
  return nil if size == 0 || entity.nil?
@@ -135,35 +165,28 @@ class CacheTable < Array
135
165
  bsearch do |candidate|
136
166
  # find using all sort attributes
137
167
  order_attributes.inject(0) do |result,attribute|
168
+ # value from the block should be in [-1,0,1],
169
+ # similar to candidate <=> entity
138
170
  if result == 0
171
+ # they're equal, so compare attribute values
139
172
  method = attribute.attribute.to_sym
173
+
140
174
  # compare taking ordering direction into account
141
- retval =
142
- if attribute.direction == :asc
143
- # TODO which would be more efficient here?
144
- #~ candidate.send( method ) <=> entity.send( method )
145
- candidate[method] <=> entity[method]
146
- else
147
- #~ entity.send( method ) <=> candidate.send( method )
148
- entity[method] <=> candidate[method]
149
- end
175
+ retval = compare( entity.send( method ), candidate.send( method ), attribute.to_i )
176
+
150
177
  # exit now because we have a difference
151
178
  next( retval ) if retval != 0
152
179
 
153
180
  # otherwise try with the next order attribute
154
181
  retval
155
182
  else
156
- # they're equal, so try next order attribute
183
+ # recurse out because we have a difference already
157
184
  result
158
185
  end
159
186
  end
160
187
  end
161
188
  end
162
189
  end
163
-
164
- def search( field, search_criteria, start_entity )
165
- Clevic::TableSearcher.new( entity_class, order_attributes, search_criteria, field ).search( start_entity )
166
- end
167
190
  end
168
191
 
169
192
  # This is part of Array in case the programmer wants to use