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.
- data/History.txt +9 -0
- data/Manifest.txt +13 -10
- data/README.txt +6 -9
- data/Rakefile +35 -24
- data/TODO +29 -17
- data/bin/clevic +84 -37
- data/config/hoe.rb +7 -3
- data/lib/clevic.rb +2 -4
- data/lib/clevic/browser.rb +37 -49
- data/lib/clevic/cache_table.rb +55 -165
- data/lib/clevic/db_options.rb +32 -21
- data/lib/clevic/default_view.rb +66 -0
- data/lib/clevic/delegates.rb +51 -67
- data/lib/clevic/dirty.rb +101 -0
- data/lib/clevic/extensions.rb +24 -38
- data/lib/clevic/field.rb +400 -99
- data/lib/clevic/item_delegate.rb +32 -33
- data/lib/clevic/model_builder.rb +315 -148
- data/lib/clevic/order_attribute.rb +53 -0
- data/lib/clevic/record.rb +57 -57
- data/lib/clevic/search_dialog.rb +71 -67
- data/lib/clevic/sql_dialects.rb +33 -0
- data/lib/clevic/table_model.rb +73 -120
- data/lib/clevic/table_searcher.rb +165 -0
- data/lib/clevic/table_view.rb +140 -100
- data/lib/clevic/ui/.gitignore +1 -0
- data/lib/clevic/ui/browser_ui.rb +55 -56
- data/lib/clevic/ui/search_dialog_ui.rb +50 -51
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +89 -0
- data/models/accounts_models.rb +12 -9
- data/models/minimal_models.rb +4 -2
- data/models/times_models.rb +41 -25
- data/models/times_sqlite_models.rb +1 -145
- data/models/values_models.rb +15 -16
- data/test/test_cache_table.rb +138 -0
- data/test/test_helper.rb +131 -0
- data/test/test_model_index_extensions.rb +22 -0
- data/test/test_order_attribute.rb +62 -0
- data/test/test_sql_dialects.rb +77 -0
- data/test/test_table_searcher.rb +188 -0
- metadata +36 -20
- data/bin/import-times +0 -128
- data/config/jamis.rb +0 -589
- data/env.sh +0 -1
- data/lib/active_record/dirty.rb +0 -87
- data/lib/clevic/field_builder.rb +0 -42
- data/website/index.html +0 -170
- data/website/index.txt +0 -17
- data/website/screenshot.png +0 -0
- data/website/stylesheets/screen.css +0 -131
- data/website/template.html.erb +0 -41
data/config/hoe.rb
CHANGED
@@ -8,9 +8,10 @@ RUBYFORGE_PROJECT = 'clevic' # The unix name for your project
|
|
8
8
|
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
9
9
|
DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
|
10
10
|
EXTRA_DEPENDENCIES = [
|
11
|
-
['qtext', '>=0.
|
12
|
-
['activerecord', '
|
13
|
-
['fastercsv', '>=1.2.3']
|
11
|
+
['qtext', '>=0.6.2'],
|
12
|
+
['activerecord', '>=2.0.2'],
|
13
|
+
['fastercsv', '>=1.2.3'],
|
14
|
+
['gather', '>=0.0.3']
|
14
15
|
# This isn't always installed from gems
|
15
16
|
#~ ['qtruby4', '>=1.4.9']
|
16
17
|
# bsearch can't be installed from gems
|
@@ -44,6 +45,9 @@ RDOC_OPTS = ['--quiet', '--title', 'clevic documentation',
|
|
44
45
|
"--opname", "index.html",
|
45
46
|
"--line-numbers",
|
46
47
|
"--main", "README",
|
48
|
+
#~ '--accessor=property',
|
49
|
+
'-A', 'property=Property',
|
50
|
+
'--format=darkfish',
|
47
51
|
"--inline-source"]
|
48
52
|
|
49
53
|
class Hoe
|
data/lib/clevic.rb
CHANGED
data/lib/clevic/browser.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
require 'clevic/search_dialog.rb'
|
2
2
|
require 'clevic/ui/browser_ui.rb'
|
3
|
-
require 'clevic/
|
3
|
+
require 'clevic/table_view.rb'
|
4
4
|
require 'clevic.rb'
|
5
5
|
|
6
6
|
module Clevic
|
7
7
|
|
8
8
|
=begin rdoc
|
9
|
-
The main application class. Display
|
10
|
-
|
9
|
+
The main application class. Display one tabs for each descendant of Clevic::View
|
10
|
+
in Clevic::View.order. DefaultView classes created by Clevic::Record are automatically
|
11
|
+
added.
|
11
12
|
=end
|
12
13
|
class Browser < Qt::Widget
|
13
14
|
slots *%w{dump() refresh_table() filter_by_current(bool) next_tab() previous_tab() current_changed(int)}
|
14
15
|
|
16
|
+
attr_reader :tables_tab
|
17
|
+
|
15
18
|
def initialize( main_window )
|
16
19
|
super( main_window )
|
17
20
|
|
@@ -42,10 +45,17 @@ class Browser < Qt::Widget
|
|
42
45
|
|
43
46
|
tables_tab.connect SIGNAL( 'currentChanged(int)' ), &method( :current_changed )
|
44
47
|
|
45
|
-
|
48
|
+
load_views
|
46
49
|
update_menus
|
50
|
+
main_window.window_title = [database_name, 'Clevic'].compact.join ' '
|
47
51
|
end
|
48
52
|
|
53
|
+
# Set the main window title to the name of the database, if we can find it.
|
54
|
+
def database_name
|
55
|
+
#~ FIXME #{__FILE__}:#{__LINE__}"
|
56
|
+
#~ table_view.model.db_options.database
|
57
|
+
end
|
58
|
+
|
49
59
|
def update_menus
|
50
60
|
# update edit menu
|
51
61
|
@layout.menu_edit.clear
|
@@ -70,7 +80,7 @@ class Browser < Qt::Widget
|
|
70
80
|
# activated by Ctrl-Shift-D for debugging
|
71
81
|
def dump
|
72
82
|
puts "table_view.model: #{table_view.model.inspect}"
|
73
|
-
puts "table_view.model.
|
83
|
+
puts "table_view.model.entity_class: #{table_view.model.entity_class.inspect}"
|
74
84
|
end
|
75
85
|
|
76
86
|
# return the Clevic::TableView object in the currently displayed tab
|
@@ -78,10 +88,6 @@ class Browser < Qt::Widget
|
|
78
88
|
tables_tab.current_widget
|
79
89
|
end
|
80
90
|
|
81
|
-
def tables_tab
|
82
|
-
@tables_tab
|
83
|
-
end
|
84
|
-
|
85
91
|
# slot to handle Ctrl-Tab and move to next tab, or wrap around
|
86
92
|
def next_tab
|
87
93
|
tables_tab.current_index =
|
@@ -114,53 +120,33 @@ class Browser < Qt::Widget
|
|
114
120
|
Qt::Application.translate("Browser", st, nil, Qt::Application::UnicodeUTF8)
|
115
121
|
end
|
116
122
|
|
117
|
-
#
|
118
|
-
#
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
if x.ancestors.include?( ActiveRecord::Base )
|
123
|
-
case
|
124
|
-
when x == ActiveRecord::Base; # don't include this
|
125
|
-
when x == Clevic::Record; # don't include this
|
126
|
-
else; models << x
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
models.sort{|a,b| a.name <=> b.name}
|
131
|
-
end
|
132
|
-
|
133
|
-
# Create the tabs, each with a collection for a particular model class.
|
134
|
-
#
|
135
|
-
# models parameter can be an array of Model objects, in order of display.
|
136
|
-
# if models is nil, find_models is called
|
137
|
-
def load_models
|
138
|
-
models = Clevic::Record.models
|
139
|
-
models = find_models if models.empty?
|
123
|
+
# Create the tabs, each with a collection for a particular entity class.
|
124
|
+
# views come from Clevic::View.order
|
125
|
+
def load_views
|
126
|
+
views = Clevic::View.order.uniq
|
127
|
+
Kernel.raise "no views to display" if views.empty?
|
140
128
|
|
141
129
|
# Add all existing model objects as tabs, one each
|
142
|
-
|
143
|
-
|
144
|
-
|
130
|
+
views.each do |view_class|
|
131
|
+
view = view_class.new
|
132
|
+
unless view.entity_class.table_exists?
|
133
|
+
puts "No table for #{view.entity_class.inspect}"
|
134
|
+
next
|
135
|
+
end
|
145
136
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
puts "Entity#ui deprecated. Use build_table_model instead."
|
150
|
-
model_class.ui( tables_tab )
|
151
|
-
else
|
152
|
-
Clevic::TableView.new( model_class, tables_tab )
|
153
|
-
end
|
137
|
+
begin
|
138
|
+
# create the the table_view and the table_model for the entity_class
|
139
|
+
tab = Clevic::TableView.new( view )
|
154
140
|
|
155
141
|
# show status messages
|
156
142
|
tab.connect( SIGNAL( 'status_text(QString)' ) ) { |msg| @layout.statusbar.show_message( msg, 10000 ) }
|
157
143
|
|
158
144
|
# add a new tab
|
159
|
-
tables_tab.add_tab( tab, translate(
|
145
|
+
tables_tab.add_tab( tab, translate( tab.title ) )
|
160
146
|
|
161
147
|
# add the table to the Table menu
|
162
148
|
action = Qt::Action.new( @layout.menu_model )
|
163
|
-
action.text = translate(
|
149
|
+
action.text = translate( tab.title )
|
164
150
|
action.connect SIGNAL( 'triggered()' ) do
|
165
151
|
tables_tab.current_widget = tab
|
166
152
|
end
|
@@ -169,12 +155,14 @@ class Browser < Qt::Widget
|
|
169
155
|
# handle filter status changed, so we can provide a visual indication
|
170
156
|
tab.connect SIGNAL( 'filter_status(bool)' ) do |status|
|
171
157
|
# update the tab, so there's a visual indication of filtering
|
172
|
-
|
173
|
-
tables_tab.set_tab_text( tables_tab.current_index,
|
158
|
+
filter_title = ( tab.filtered ? '| ' : '' ) + translate( tab.title )
|
159
|
+
tables_tab.set_tab_text( tables_tab.current_index, filter_title )
|
174
160
|
end
|
175
161
|
rescue Exception => e
|
176
|
-
puts
|
177
|
-
puts "
|
162
|
+
puts
|
163
|
+
puts "UI from #{view} will not be available: #{e.message}"
|
164
|
+
puts e.backtrace #if $options[:debug]
|
165
|
+
puts
|
178
166
|
end
|
179
167
|
end
|
180
168
|
end
|
data/lib/clevic/cache_table.rb
CHANGED
@@ -1,57 +1,16 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'active_record'
|
3
|
-
require '
|
3
|
+
require 'clevic/table_searcher.rb'
|
4
|
+
require 'clevic/order_attribute.rb'
|
4
5
|
require 'bsearch'
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
attr_reader :direction, :attribute
|
11
|
-
|
12
|
-
def initialize( model_class, sql_order_fragment )
|
13
|
-
@model_class = model_class
|
14
|
-
if sql_order_fragment =~ /.*\.(.*?) *asc/
|
15
|
-
@direction = :asc
|
16
|
-
@attribute = $1
|
17
|
-
elsif sql_order_fragment =~ /.*\.(.*?) *desc/
|
18
|
-
@direction = :desc
|
19
|
-
@attribute = $1
|
20
|
-
else
|
21
|
-
@direction = :asc
|
22
|
-
@attribute = sql_order_fragment
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# return ORDER BY field name
|
27
|
-
def to_s
|
28
|
-
@string ||= attribute
|
29
|
-
end
|
30
|
-
|
31
|
-
def to_sym
|
32
|
-
@sym ||= attribute.to_sym
|
33
|
-
end
|
34
|
-
|
35
|
-
# return 'field ASC' or 'field DESC', depending
|
36
|
-
def to_sql
|
37
|
-
"#{@model_class.table_name}.#{attribute} #{direction.to_s}"
|
38
|
-
end
|
39
|
-
|
40
|
-
def reverse( direction )
|
41
|
-
case direction
|
42
|
-
when :asc; :desc
|
43
|
-
when :desc; :asc
|
44
|
-
else; raise "unknown direction #{direction}"
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# return the opposite ASC or DESC from to_sql
|
49
|
-
def to_reverse_sql
|
50
|
-
"#{@model_class.table_name}.#{attribute} #{reverse(direction).to_s}"
|
51
|
-
end
|
52
|
-
|
7
|
+
begin
|
8
|
+
require 'active_record/dirty.rb'
|
9
|
+
rescue MissingSourceFile
|
10
|
+
require 'clevic/dirty.rb'
|
53
11
|
end
|
54
12
|
|
13
|
+
|
55
14
|
=begin rdoc
|
56
15
|
Fetch rows from the db on demand, rather than all up front.
|
57
16
|
|
@@ -61,7 +20,7 @@ is specified, the primary key of the entity will be used.
|
|
61
20
|
|
62
21
|
It hasn't been tested with compound primary keys.
|
63
22
|
|
64
|
-
|
23
|
+
#--
|
65
24
|
|
66
25
|
TODO drop rows when they haven't been accessed for a while
|
67
26
|
|
@@ -71,125 +30,59 @@ for each call?
|
|
71
30
|
class CacheTable < Array
|
72
31
|
# the number of records loaded in one call to the db
|
73
32
|
attr_accessor :preload_count
|
74
|
-
attr_reader :options, :
|
33
|
+
attr_reader :options, :entity_class
|
75
34
|
|
76
|
-
def initialize(
|
35
|
+
def initialize( entity_class, find_options = {} )
|
77
36
|
@preload_count = 20
|
78
37
|
# must be before sanitise_options
|
79
|
-
@
|
38
|
+
@entity_class = entity_class
|
80
39
|
# must be before anything that uses options
|
81
|
-
@options =
|
40
|
+
@options = find_options.clone
|
41
|
+
sanitise_options!
|
82
42
|
|
83
43
|
# size the array and fill it with nils. They'll be filled
|
84
44
|
# in by the [] operator
|
85
45
|
@row_count = sql_count
|
86
|
-
super(@row_count)
|
46
|
+
super( @row_count )
|
87
47
|
end
|
88
48
|
|
89
49
|
# The count of the records according to the db, which may be different to
|
90
50
|
# the records in the cache
|
91
51
|
def sql_count
|
92
|
-
@
|
93
|
-
end
|
94
|
-
|
95
|
-
def order
|
96
|
-
@options[:order]
|
97
|
-
end
|
98
|
-
|
99
|
-
def reverse_order
|
100
|
-
@order_attributes.map{|x| x.to_reverse_sql}.join(',')
|
101
|
-
end
|
102
|
-
|
103
|
-
def quote_column( field_name )
|
104
|
-
@model_class.connection.quote_column_name( field_name )
|
105
|
-
end
|
106
|
-
|
107
|
-
# return a string containing the correct
|
108
|
-
# boolean value depending on the DB adapter
|
109
|
-
# because Postgres wants real true and false in complex statements, not 't' and 'f'
|
110
|
-
def sql_boolean( value )
|
111
|
-
case model_class.connection.adapter_name
|
112
|
-
when 'PostgreSQL'
|
113
|
-
value ? 'true' : 'false'
|
114
|
-
else
|
115
|
-
value ? model_class.connection.quoted_true : model_class.connection.quoted_false
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# recursively create a case statement to do the comparison
|
120
|
-
# because and ... and ... and filters on *each* one rather than
|
121
|
-
# consecutively.
|
122
|
-
# operator is either '<' or '>'
|
123
|
-
def build_recursive_comparison( operator, index = 0 )
|
124
|
-
# end recursion
|
125
|
-
return sql_boolean( false ) if index == @order_attributes.size
|
126
|
-
|
127
|
-
# fetch the current attribute
|
128
|
-
attribute = @order_attributes[index]
|
129
|
-
|
130
|
-
# build case statement, including recusion
|
131
|
-
st = <<-EOF
|
132
|
-
case
|
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
|
-
else #{sql_boolean false}
|
136
|
-
end
|
137
|
-
EOF
|
138
|
-
# indent
|
139
|
-
st.gsub!( /^/, ' ' * index )
|
52
|
+
@entity_class.count( @options.reject{|k,v| k == :order} )
|
140
53
|
end
|
141
54
|
|
142
|
-
#
|
143
|
-
#
|
144
|
-
#
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
sql = build_recursive_comparison( operator )
|
157
|
-
|
158
|
-
# only Postgres seems to understand real booleans
|
159
|
-
# everything else needs the big case statement to be compared
|
160
|
-
# to something
|
161
|
-
unless model_class.connection.adapter_name == 'PostgreSQL'
|
162
|
-
sql += " = #{sql_boolean true}"
|
55
|
+
# Return the set of OrderAttribute objects for this collection.
|
56
|
+
# If no order attributes are specified, the primary key will be used.
|
57
|
+
# TODO what about compund primary keys?
|
58
|
+
def order_attributes
|
59
|
+
# This is sorted in @options[:order], so use that for the search
|
60
|
+
if @order_attributes.nil?
|
61
|
+
@order_attributes = @options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@entity_class, x)}
|
62
|
+
|
63
|
+
# add the primary key if nothing is specified
|
64
|
+
# because we need an ordering of some kind otherwise
|
65
|
+
# index_for_entity will not work
|
66
|
+
if !@order_attributes.any? {|x| x.attribute == @entity_class.primary_key }
|
67
|
+
@order_attributes << OrderAttribute.new( @entity_class, @entity_class.primary_key )
|
68
|
+
end
|
163
69
|
end
|
164
|
-
|
165
|
-
# build parameter values
|
166
|
-
params = {}
|
167
|
-
@order_attributes.each {|x| params[x.to_sym] = entity.send( x.attribute )}
|
168
|
-
{ :sql => sql, :params => params }
|
70
|
+
@order_attributes
|
169
71
|
end
|
170
72
|
|
171
73
|
# add an id to options[:order] if it's not in there
|
172
74
|
# also create @order_attributes, and @auto_new
|
173
|
-
def sanitise_options
|
75
|
+
def sanitise_options!
|
174
76
|
# save this for later
|
175
|
-
@auto_new = options[:auto_new]
|
176
|
-
options.delete :auto_new
|
177
|
-
|
178
|
-
options[:order] ||= ''
|
179
|
-
@order_attributes = options[:order].split( /, */ ).map{|x| OrderAttribute.new(@model_class, x)}
|
180
|
-
|
181
|
-
# add the primary key if nothing is specified
|
182
|
-
# because we need an ordering of some kind otherwise
|
183
|
-
# index_for_entity will not work
|
184
|
-
if !@order_attributes.any? {|x| x.attribute == @model_class.primary_key }
|
185
|
-
@order_attributes << OrderAttribute.new( @model_class, @model_class.primary_key )
|
186
|
-
end
|
77
|
+
@auto_new = @options[:auto_new]
|
78
|
+
@options.delete :auto_new
|
187
79
|
|
188
|
-
#
|
189
|
-
options[:order]
|
80
|
+
# make sure we have a string here
|
81
|
+
@options[:order] ||= ''
|
190
82
|
|
191
|
-
#
|
192
|
-
|
83
|
+
# recreate the options[:order] entry to include default
|
84
|
+
# TODO why though?
|
85
|
+
@options[:order] = order_attributes.map{|x| x.to_sql}.join(',')
|
193
86
|
end
|
194
87
|
|
195
88
|
# Execute the block with the specified preload_count,
|
@@ -211,7 +104,7 @@ EOF
|
|
211
104
|
offset = index < 0 ? index + @row_count : index
|
212
105
|
|
213
106
|
# fetch self.preload_count records
|
214
|
-
records = @
|
107
|
+
records = @entity_class.find( :all, @options.merge( :offset => offset, :limit => preload_count ) )
|
215
108
|
records.each_with_index {|x,i| self[i+index] = x if !cached_at?( i+index )}
|
216
109
|
|
217
110
|
# return the first one
|
@@ -228,23 +121,7 @@ EOF
|
|
228
121
|
# data set. pass in ActiveRecord options to filter
|
229
122
|
def renew( options = {} )
|
230
123
|
clear
|
231
|
-
self.class.new( @
|
232
|
-
end
|
233
|
-
|
234
|
-
# Return the set of OrderAttribute objects for this collection
|
235
|
-
def order_attributes
|
236
|
-
# This is sorted in @options[:order], so use that for the search
|
237
|
-
if @order_attributes.nil?
|
238
|
-
@order_attributes = @options[:order].to_s.split( /, */ ).map{|x| OrderAttribute.new(@model_class, x)}
|
239
|
-
|
240
|
-
# add the primary key if nothing is specified
|
241
|
-
# because we need an ordering of some kind otherwise
|
242
|
-
# index_for_entity will not work
|
243
|
-
if !@order_attributes.any? {|x| x.attribute == @model_class.primary_key }
|
244
|
-
@order_attributes << OrderAttribute.new( @model_class, @model_class.primary_key )
|
245
|
-
end
|
246
|
-
end
|
247
|
-
@order_attributes
|
124
|
+
self.class.new( @entity_class, @options.merge( options ) )
|
248
125
|
end
|
249
126
|
|
250
127
|
# find the index for the given entity, using a binary search algorithm (bsearch).
|
@@ -255,7 +132,8 @@ EOF
|
|
255
132
|
return nil if size == 0
|
256
133
|
return nil if entity.nil?
|
257
134
|
|
258
|
-
# only load one record at a time
|
135
|
+
# only load one record at a time, because mostly we only
|
136
|
+
# need one for the binary seach. No point in pulling several out.
|
259
137
|
preload_limit( 1 ) do
|
260
138
|
# do the binary search based on what we know about the search order
|
261
139
|
bsearch do |candidate|
|
@@ -266,6 +144,7 @@ EOF
|
|
266
144
|
# compare taking ordering direction into account
|
267
145
|
retval =
|
268
146
|
if attribute.direction == :asc
|
147
|
+
# TODO which would be more efficient here?
|
269
148
|
#~ candidate.send( method ) <=> entity.send( method )
|
270
149
|
candidate[method] <=> entity[method]
|
271
150
|
else
|
@@ -290,20 +169,31 @@ EOF
|
|
290
169
|
@auto_new
|
291
170
|
end
|
292
171
|
|
172
|
+
def search( field, search_criteria, start_entity )
|
173
|
+
Clevic::TableSearcher.new( entity_class, order_attributes, search_criteria, field ).search( start_entity )
|
174
|
+
end
|
175
|
+
|
176
|
+
# delete the given index. If the size ends up as 0,
|
293
177
|
# make sure there's always at least one empty record
|
294
178
|
def delete_at( index )
|
295
179
|
retval = super
|
296
180
|
if self.size == 0 && auto_new?
|
297
|
-
self << @
|
181
|
+
self << @entity_class.new
|
298
182
|
end
|
299
183
|
retval
|
300
184
|
end
|
301
185
|
|
302
186
|
end
|
303
187
|
|
188
|
+
# This is part of Array in case the programmer wants to use
|
189
|
+
# a simple array instead of a CacheTable.
|
304
190
|
class Array
|
305
191
|
# For use with CacheTable. Return true if something is cached, false otherwise
|
306
192
|
def cached_at?( index )
|
307
193
|
!at(index).nil?
|
308
194
|
end
|
195
|
+
|
196
|
+
def searcher
|
197
|
+
raise "not implemented"
|
198
|
+
end
|
309
199
|
end
|