clevic 0.8.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +13 -10
  3. data/README.txt +6 -9
  4. data/Rakefile +35 -24
  5. data/TODO +29 -17
  6. data/bin/clevic +84 -37
  7. data/config/hoe.rb +7 -3
  8. data/lib/clevic.rb +2 -4
  9. data/lib/clevic/browser.rb +37 -49
  10. data/lib/clevic/cache_table.rb +55 -165
  11. data/lib/clevic/db_options.rb +32 -21
  12. data/lib/clevic/default_view.rb +66 -0
  13. data/lib/clevic/delegates.rb +51 -67
  14. data/lib/clevic/dirty.rb +101 -0
  15. data/lib/clevic/extensions.rb +24 -38
  16. data/lib/clevic/field.rb +400 -99
  17. data/lib/clevic/item_delegate.rb +32 -33
  18. data/lib/clevic/model_builder.rb +315 -148
  19. data/lib/clevic/order_attribute.rb +53 -0
  20. data/lib/clevic/record.rb +57 -57
  21. data/lib/clevic/search_dialog.rb +71 -67
  22. data/lib/clevic/sql_dialects.rb +33 -0
  23. data/lib/clevic/table_model.rb +73 -120
  24. data/lib/clevic/table_searcher.rb +165 -0
  25. data/lib/clevic/table_view.rb +140 -100
  26. data/lib/clevic/ui/.gitignore +1 -0
  27. data/lib/clevic/ui/browser_ui.rb +55 -56
  28. data/lib/clevic/ui/search_dialog_ui.rb +50 -51
  29. data/lib/clevic/version.rb +2 -2
  30. data/lib/clevic/view.rb +89 -0
  31. data/models/accounts_models.rb +12 -9
  32. data/models/minimal_models.rb +4 -2
  33. data/models/times_models.rb +41 -25
  34. data/models/times_sqlite_models.rb +1 -145
  35. data/models/values_models.rb +15 -16
  36. data/test/test_cache_table.rb +138 -0
  37. data/test/test_helper.rb +131 -0
  38. data/test/test_model_index_extensions.rb +22 -0
  39. data/test/test_order_attribute.rb +62 -0
  40. data/test/test_sql_dialects.rb +77 -0
  41. data/test/test_table_searcher.rb +188 -0
  42. metadata +36 -20
  43. data/bin/import-times +0 -128
  44. data/config/jamis.rb +0 -589
  45. data/env.sh +0 -1
  46. data/lib/active_record/dirty.rb +0 -87
  47. data/lib/clevic/field_builder.rb +0 -42
  48. data/website/index.html +0 -170
  49. data/website/index.txt +0 -17
  50. data/website/screenshot.png +0 -0
  51. data/website/stylesheets/screen.css +0 -131
  52. data/website/template.html.erb +0 -41
@@ -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.5.0'],
12
- ['activerecord', '=2.0.2'],
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
@@ -1,6 +1,4 @@
1
+ # This provides enough to define UIs.
1
2
  require 'clevic/db_options.rb'
2
- require 'clevic/table_view.rb'
3
- require 'clevic/model_builder.rb'
4
3
  require 'clevic/record.rb'
5
-
6
-
4
+ require 'clevic/view.rb'
@@ -1,17 +1,20 @@
1
1
  require 'clevic/search_dialog.rb'
2
2
  require 'clevic/ui/browser_ui.rb'
3
- require 'clevic/record.rb'
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 as many tabs as there are Clevic::Record or ActiveRecord::Base
10
- subclasses.
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
- load_models
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.model_class: #{table_view.model.model_class.inspect}"
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
- # return the list of descendants of ActiveRecord::Base, or
118
- # of Clevic::Record
119
- def find_models
120
- models = []
121
- ObjectSpace.each_object( Class ) do |x|
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
- models.each do |model_class|
143
- begin
144
- next unless model_class.table_exists?
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
- # create the the table_view and the table_model for the model_class
147
- tab =
148
- if model_class.respond_to?( :ui )
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( model_class.name.demodulize.tableize.humanize ) )
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( model_class.name.demodulize.tableize.humanize )
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
- tab_title = ( tab.filtered ? '| ' : '' ) + translate( model_class.name.humanize )
173
- tables_tab.set_tab_text( tables_tab.current_index, tab_title )
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 e.backtrace if $options[:debug]
177
- puts "Model #{model_class} will not be available: #{e.message}"
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
@@ -1,57 +1,16 @@
1
1
  require 'rubygems'
2
2
  require 'active_record'
3
- require 'active_record/dirty.rb'
3
+ require 'clevic/table_searcher.rb'
4
+ require 'clevic/order_attribute.rb'
4
5
  require 'bsearch'
5
6
 
6
- =begin rdoc
7
- Store the SQL order_by attributes with ascending and descending values
8
- =end
9
- class OrderAttribute
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, :model_class
33
+ attr_reader :options, :entity_class
75
34
 
76
- def initialize( model_class, find_options = {} )
35
+ def initialize( entity_class, find_options = {} )
77
36
  @preload_count = 20
78
37
  # must be before sanitise_options
79
- @model_class = model_class
38
+ @entity_class = entity_class
80
39
  # must be before anything that uses options
81
- @options = sanitise_options( find_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
- @model_class.count( :conditions => @options[:conditions] )
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
- # return a Hash containing
143
- # :sql => the sql statement to be used as part of a where clause
144
- # :params => the parameters corresponding to :sql
145
- # entity is an AR model object
146
- # direction is either :forwards or :backwards
147
- def build_sql_find( entity, direction )
148
- operator =
149
- case direction
150
- when :forwards; '>'
151
- when :backwards; '<'
152
- else; raise "unknown direction #{direction.inspect}"
153
- end
154
-
155
- # build the sql comparison where clause fragment
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( 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
- # recreate the options[:order] entry
189
- options[:order] = @order_attributes.map{|x| x.to_sql}.join(',')
80
+ # make sure we have a string here
81
+ @options[:order] ||= ''
190
82
 
191
- # give back the sanitised options
192
- options
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 = @model_class.find( :all, @options.merge( :offset => offset, :limit => preload_count ) )
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( @model_class, @options.merge( options ) )
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 << @model_class.new
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