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.
- data/History.txt +10 -0
- data/Manifest.txt +209 -30
- data/README.txt +16 -20
- data/Rakefile +8 -8
- data/TODO +6 -7
- data/bin/clevic +12 -73
- data/lib/clevic/action_builder.rb +168 -0
- data/lib/clevic/ar_methods.rb +120 -0
- data/lib/clevic/attribute_list.rb +56 -0
- data/lib/clevic/cache_table.rb +60 -37
- data/lib/clevic/default_view.rb +3 -16
- data/lib/clevic/delegate.rb +46 -0
- data/lib/clevic/emitter.rb +38 -0
- data/lib/clevic/extensions.rb +61 -114
- data/lib/clevic/field.rb +159 -228
- data/lib/clevic/field_valuer.rb +165 -0
- data/lib/clevic/filter_command.rb +2 -6
- data/lib/clevic/generic_format.rb +52 -0
- data/lib/clevic/{ui → icons}/icon.png +0 -0
- data/lib/clevic/many_field.rb +7 -0
- data/lib/clevic/model_builder.rb +234 -146
- data/lib/clevic/model_column.rb +61 -13
- data/lib/clevic/order_attribute.rb +10 -0
- data/lib/clevic/qt.rb +35 -0
- data/lib/clevic/qt/action_builder.rb +47 -0
- data/lib/clevic/qt/boolean_delegate.rb +8 -0
- data/lib/clevic/{browser.rb → qt/browser.rb} +35 -14
- data/lib/clevic/qt/clipboard.rb +35 -0
- data/lib/clevic/qt/combo_delegate.rb +198 -0
- data/lib/clevic/qt/delegates.rb +1 -0
- data/lib/clevic/qt/distinct_delegate.rb +35 -0
- data/lib/clevic/qt/extensions.rb +52 -0
- data/lib/clevic/qt/field.rb +18 -0
- data/lib/clevic/{item_delegate.rb → qt/item_delegate.rb} +8 -4
- data/lib/clevic/qt/relational_delegate.rb +87 -0
- data/lib/clevic/{search_dialog.rb → qt/search_dialog.rb} +1 -11
- data/lib/clevic/qt/set_delegate.rb +44 -0
- data/lib/clevic/qt/table_model.rb +331 -0
- data/lib/clevic/qt/table_view.rb +344 -0
- data/lib/clevic/qt/text_area_delegate.rb +8 -0
- data/lib/clevic/{text_delegate.rb → qt/text_delegate.rb} +6 -4
- data/lib/clevic/{ui → qt/ui}/.gitignore +0 -0
- data/lib/clevic/{ui → qt/ui}/browser.ui +0 -0
- data/lib/clevic/{ui → qt/ui}/search_dialog.ui +0 -0
- data/lib/clevic/rails_models_loaders.rb +56 -0
- data/lib/clevic/record.rb +2 -17
- data/lib/clevic/sampler.rb +81 -0
- data/lib/clevic/sequel_ar_adapter.rb +215 -0
- data/lib/clevic/sequel_length_validation.rb +23 -0
- data/lib/clevic/sequel_meta.rb +65 -0
- data/lib/clevic/sequel_naked.rb +30 -0
- data/lib/clevic/swing.rb +38 -0
- data/lib/clevic/swing/action.rb +125 -0
- data/lib/clevic/swing/action_builder.rb +47 -0
- data/lib/clevic/swing/boolean_delegate.rb +26 -0
- data/lib/clevic/swing/browser.rb +282 -0
- data/lib/clevic/swing/cell_editor.rb +95 -0
- data/lib/clevic/swing/cell_renderer.rb +44 -0
- data/lib/clevic/swing/clipboard.rb +135 -0
- data/lib/clevic/swing/combo_delegate.rb +336 -0
- data/lib/clevic/swing/confirm_dialog.rb +57 -0
- data/lib/clevic/swing/delegate.rb +40 -0
- data/lib/clevic/swing/distinct_delegate.rb +30 -0
- data/lib/clevic/swing/extensions.rb +274 -0
- data/lib/clevic/swing/field.rb +35 -0
- data/lib/clevic/swing/relational_delegate.rb +48 -0
- data/lib/clevic/swing/row_header.rb +210 -0
- data/lib/clevic/swing/search_dialog.rb +230 -0
- data/lib/clevic/swing/selection_model.rb +90 -0
- data/lib/clevic/swing/set_delegate.rb +41 -0
- data/lib/clevic/swing/swing_table_index.rb +43 -0
- data/lib/clevic/swing/table_model.rb +200 -0
- data/lib/clevic/swing/table_view.rb +385 -0
- data/lib/clevic/swing/table_view_focus.rb +47 -0
- data/lib/clevic/swing/tag_delegate.rb +127 -0
- data/lib/clevic/swing/tag_editor.rb +101 -0
- data/lib/clevic/swing/text_area_delegate.rb +46 -0
- data/lib/clevic/swing/text_delegate.rb +31 -0
- data/lib/clevic/swing/ui/build.xml +74 -0
- data/lib/clevic/swing/ui/dist/README.TXT +33 -0
- data/lib/clevic/swing/ui/dist/lib/swing-layout-1.0.3.jar +0 -0
- data/lib/clevic/swing/ui/manifest.mf +3 -0
- data/lib/clevic/swing/ui/nbproject/build-impl.xml +731 -0
- data/lib/clevic/swing/ui/nbproject/genfiles.properties +8 -0
- data/lib/clevic/swing/ui/nbproject/private/config.properties +0 -0
- data/lib/clevic/swing/ui/nbproject/private/private.properties +6 -0
- data/lib/clevic/swing/ui/nbproject/private/private.xml +4 -0
- data/lib/clevic/swing/ui/nbproject/project.properties +70 -0
- data/lib/clevic/swing/ui/nbproject/project.xml +14 -0
- data/lib/clevic/swing/ui/src/SearchDialog.form +158 -0
- data/lib/clevic/swing/ui/src/SearchDialog.java +163 -0
- data/lib/clevic/swing/ui/src/TagEditor.form +106 -0
- data/lib/clevic/swing/ui/src/TagEditor.java +108 -0
- data/lib/clevic/swing/ui/src/resources/SearchDialog.properties +0 -0
- data/lib/clevic/table_index.rb +100 -0
- data/lib/clevic/table_model.rb +54 -425
- data/lib/clevic/table_searcher.rb +113 -116
- data/lib/clevic/table_view.rb +171 -399
- data/lib/clevic/table_view_paste.rb +199 -0
- data/lib/clevic/version.rb +3 -2
- data/lib/clevic/view.rb +94 -43
- data/models/accounts_models.rb +13 -13
- data/models/minimal_models.rb +5 -9
- data/models/times_models.rb +19 -14
- data/models/times_psql_models.rb +10 -0
- data/models/times_sqlite_models.rb +1 -8
- data/models/values_models.rb +2 -8
- data/tasks/clevic.rake +1 -1
- data/tasks/rdoc.rake +1 -5
- data/tasks/website.rake +1 -1
- data/test/test_cache_table.rb +15 -29
- data/test/test_helper.rb +14 -83
- data/test/test_order_attribute.rb +1 -1
- data/test/test_table_model.rb +0 -21
- data/test/test_table_searcher.rb +67 -61
- metadata +262 -78
- data/lib/clevic.rb +0 -4
- data/lib/clevic/db_options.rb +0 -112
- 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
|
data/lib/clevic/cache_table.rb
CHANGED
@@ -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 :
|
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
|
-
@
|
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(
|
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
|
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 =
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
81
|
-
|
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,
|
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 ||
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|