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
@@ -19,11 +19,14 @@ Method calls are translated to insertions into a hash with the same
19
19
  key as the method being called. The hash is initialised
20
20
  with the options value passed in (in this case $options).
21
21
  Values have to_s called on them so they can be symbols or strings.
22
+
23
+ #--
24
+ TODO inherit from HashCollector
22
25
  =end
23
26
  class DbOptions
24
27
  attr_reader :options
25
28
 
26
- def initialize( options = nil )
29
+ def initialize( options = nil, &block )
27
30
  @options = options || {}
28
31
 
29
32
  # make sure the relevant entries exist, so method_missing works
@@ -32,42 +35,46 @@ class DbOptions
32
35
  @options[:username] ||= ''
33
36
  @options[:password] ||= ''
34
37
  @options[:database] ||= ''
38
+
39
+ unless block.nil?
40
+ if block.arity == -1
41
+ instance_eval &block
42
+ else
43
+ yield self
44
+ end
45
+ end
35
46
  end
36
47
 
37
- def connect( *args, &block )
38
- # using the Rails implementation, included in Qt
39
- block.bind( self )[*args]
40
- do_connection
41
- end
42
-
43
- # do error checking and make the ActiveRecord connection calls.
44
- def do_connection
45
- unless @options[:database]
46
- raise "Please define database using DbOptions"
48
+ def connect
49
+ if @options[:database].nil? || @options[:database].empty?
50
+ raise "Please define database using DbOptions. Current value is #{@options[:database].inspect}."
47
51
  end
48
52
 
49
53
  # connect to db
50
54
  ActiveRecord::Base.establish_connection( options )
51
55
  ActiveRecord::Base.logger = Logger.new(STDOUT) if options[:verbose]
52
56
  #~ ActiveRecord.colorize_logging = @options[:verbose]
53
- puts "using database #{ActiveRecord::Base.connection.raw_connection.db}" if options[:debug]
54
57
  self
55
58
  end
56
59
 
57
60
  # convenience method so we can do things like
58
- # Clevic::DbOptions.connect( $options ) do
61
+ # Clevic::DbOptions.connect do
59
62
  # database :accounts
60
63
  # adapter :postgresql
61
64
  # username 'accounts_user'
62
65
  # end
63
66
  # the block is evaluated in the context of the a new DbOptions
64
- # object.
65
- # TODO use instance_eval
66
- def self.connect( args = nil, &block )
67
- inst = self.new( args )
68
- # using the Rails implementation, included in Qt
69
- block.bind( inst )[*args]
70
- inst.do_connection
67
+ # object. You can also pass a block parameter and it will receive
68
+ # the DbOptions instance, like this:
69
+ # Clevic::DbOptions.connect do |dbo|
70
+ # dbo.database :accounts
71
+ # dbo.adapter :postgresql
72
+ # dbo.username 'accounts_user'
73
+ # end
74
+ def self.connect( args = {}, &block )
75
+ inst = self.new( args, &block )
76
+ inst.connect
77
+ inst
71
78
  end
72
79
 
73
80
  # translate method calls in the context of an instance
@@ -75,7 +82,11 @@ class DbOptions
75
82
  # variable
76
83
  def method_missing(meth, *args, &block)
77
84
  if @options.has_key? meth.to_sym
78
- @options[meth.to_sym] = args[0].to_s if @options[meth.to_sym].empty?
85
+ if args.size == 0
86
+ @options[meth.to_sym]
87
+ else
88
+ @options[meth.to_sym] = args[0].to_s
89
+ end
79
90
  else
80
91
  super
81
92
  end
@@ -0,0 +1,66 @@
1
+ module Clevic
2
+
3
+ # A subclass of Clevic::DefaultView is created by Clevic::Record
4
+ # when the latter is included in an ActiveRecord::Base subclass.
5
+ #
6
+ # The Clevic::DefaultView subclass knows how to:
7
+ # - build a fairly sensible UI from the the ActiveRecord::Base metadata.
8
+ # - create a UI definition using a class method called define_ui.
9
+ #
10
+ # See Clevic::ModelBuilder for an example.
11
+ class DefaultView < View
12
+ def method_missing( meth, *args, &block )
13
+ if entity_class.respond_to?( meth )
14
+ entity_class.send( meth, *args, &block )
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def self.define_ui_block( &block )
21
+ @define_ui_block ||= block
22
+ end
23
+
24
+ def define_ui
25
+ if self.class.define_ui_block.nil?
26
+ # use the define_ui from Clevic::View to build a default UI
27
+ super
28
+ else
29
+ # use the provided block
30
+ model_builder( &self.class.define_ui_block )
31
+ end
32
+ end
33
+
34
+ def title
35
+ @title ||= entity_class.name.demodulize.tableize.humanize
36
+ end
37
+
38
+ def define_actions( table_view, action_builder )
39
+ if entity_class.respond_to?( :actions )
40
+ puts "Deprecated: #{entity_class.name}.actions( table_view, action_builder ). Use define_actions( table_view, action_builder ) instead."
41
+ entity_class.actions( table_view, action_builder )
42
+ elsif entity_class.respond_to?( :define_actions )
43
+ entity_class.define_actions( table_view, action_builder )
44
+ end
45
+ end
46
+
47
+ def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
48
+ if entity_class.respond_to?( :data_changed )
49
+ puts "Deprecated: #{entity_class.name}.data_changed( top_left, bottom_right, table_view ). Use notify_data_changed( table_view, top_left_model_index, bottom_right_model_index ) instead."
50
+ entity_class.data_changed( top_left_model_index, bottom_right_model_index, table_view )
51
+ elsif entity_class.respond_to?( :notify_data_changed )
52
+ entity_class.notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
53
+ end
54
+ end
55
+
56
+ def notify_key_press( table_view, key_press_event, current_model_index )
57
+ if entity_class.respond_to?( :key_press_event )
58
+ puts "Deprecated: #{entity_class.name}.key_press_event( key_press_event, current_model_index, table_view ). Use notify_key_press( table_view, key_press_event, current_model_index ) instead."
59
+ entity_class.key_press_event( key_press_event, current_model_index, table_view )
60
+ elsif entity_class.respond_to?( :notify_key_press )
61
+ entity_class.notify_key_press( table_view, key_press_event, current_model_index )
62
+ end
63
+ end
64
+ end
65
+
66
+ end
@@ -6,12 +6,11 @@ module Clevic
6
6
  Base class for other delegates using Combo boxes. Emit focus out signals,
7
7
  because ComboBox stupidly doesn't.
8
8
 
9
- Generally these will be created using a ModelBuilder.
9
+ Generally these will be created using a Clevic::ModelBuilder.
10
10
  =end
11
11
  class ComboDelegate < Clevic::ItemDelegate
12
- def initialize( parent )
12
+ def initialize( parent, field )
13
13
  super
14
- @@active_record_options ||= [ :conditions, :class_name, :order ]
15
14
  end
16
15
 
17
16
  # Convert Qt:: constants from the integer value to a string value.
@@ -186,26 +185,11 @@ class ComboDelegate < Clevic::ItemDelegate
186
185
  end
187
186
 
188
187
  else
189
- model_index.gui_value = editor.text
188
+ model_index.attribute_value = editor.text
190
189
  end
191
190
  emit abstract_item_model.dataChanged( model_index, model_index )
192
191
  end
193
192
 
194
- protected
195
-
196
- # given a hash of options, return only those
197
- # which are applicable to a ActiveRecord::Base.find
198
- # method call
199
- def collect_finder_options( options )
200
- new_options = {}
201
- options.each do |key,value|
202
- if @@active_record_options.include?( key )
203
- new_options[key] = value
204
- end
205
- end
206
- new_options
207
- end
208
-
209
193
  end
210
194
 
211
195
  # Provide a list of all values in this field,
@@ -214,18 +198,9 @@ end
214
198
  # the options are sorted in order of most frequently used first.
215
199
  class DistinctDelegate < ComboDelegate
216
200
 
217
- def initialize( parent, attribute, model_class, options )
218
- @ar_model = model_class
219
- @attribute = attribute
220
- @options = options
221
- # hackery for amateur query building in populate
222
- @options[:conditions] ||= '1=1'
223
- super( parent )
224
- end
225
-
226
201
  def needs_combo?
227
202
  # works except when there is a '' in the column
228
- @ar_model.count( @attribute, collect_finder_options( @options ) ) > 0
203
+ entity_class.count( attribute.to_s, find_options ) > 0
229
204
  end
230
205
 
231
206
  def populate_current( editor, model_index )
@@ -234,22 +209,22 @@ class DistinctDelegate < ComboDelegate
234
209
 
235
210
  def query_order_description( conn, model_index )
236
211
  <<-EOF
237
- select distinct #{@attribute.to_s}, lower(#{@attribute.to_s})
238
- from #{@ar_model.table_name}
239
- where (#{@options[:conditions]})
240
- or #{conn.quote_column_name( @attribute.to_s )} = #{conn.quote( model_index.attribute_value )}
241
- order by lower(#{@attribute.to_s})
212
+ select distinct #{attribute.to_s}, lower(#{attribute.to_s})
213
+ from #{entity_class.table_name}
214
+ where (#{find_options[:conditions] || '1=1'})
215
+ or #{conn.quote_column_name( attribute.to_s )} = #{conn.quote( model_index.attribute_value )}
216
+ order by lower(#{attribute.to_s})
242
217
  EOF
243
218
  end
244
219
 
245
220
  def query_order_frequency( conn, model_index )
246
221
  <<-EOF
247
- select distinct #{@attribute.to_s}, count(#{@attribute.to_s})
248
- from #{@ar_model.table_name}
249
- where (#{@options[:conditions]})
250
- or #{conn.quote_column_name( @attribute.to_s )} = #{conn.quote( model_index.attribute_value )}
251
- group by #{@attribute.to_s}
252
- order by count(#{@attribute.to_s}) desc
222
+ select distinct #{attribute.to_s}, count(#{attribute.to_s})
223
+ from #{entity_class.table_name}
224
+ where (#{find_options[:conditions] || '1=1'})
225
+ or #{conn.quote_column_name( attribute.to_s )} = #{conn.quote( model_index.attribute_value )}
226
+ group by #{attribute.to_s}
227
+ order by count(#{attribute.to_s}) desc
253
228
  EOF
254
229
  end
255
230
 
@@ -257,19 +232,21 @@ class DistinctDelegate < ComboDelegate
257
232
  # we only use the first column, so use the second
258
233
  # column to sort by, since SQL requires the order by clause
259
234
  # to be in the select list where distinct is involved
260
- conn = @ar_model.connection
235
+ conn = entity_class.connection
261
236
  query =
262
237
  case
263
- when @options[:description]
238
+ when field.description
264
239
  query_order_description( conn, model_index )
265
- when @options[:frequency]
240
+ when field.frequency
266
241
  query_order_frequency( conn, model_index )
267
242
  else
268
243
  query_order_frequency( conn, model_index )
269
244
  end
245
+ puts "query: #{query}"
270
246
  rs = conn.execute( query )
271
247
  rs.each do |row|
272
- editor.add_item( row[0], row[0].to_variant )
248
+ value = row[attribute.to_s]
249
+ editor.add_item( value, value.to_variant )
273
250
  end
274
251
  end
275
252
 
@@ -281,13 +258,9 @@ end
281
258
  # A Combo box which only allows a restricted set of value to be entered.
282
259
  class RestrictedDelegate < ComboDelegate
283
260
  # options must contain a :set => [ ... ] to specify the set of values.
284
- def initialize( parent, attribute, model_class, options )
285
- raise "RestrictedDelegate must have a :set in options" unless options.has_key?( :set )
286
- @ar_model = model_class
287
- @attribute = attribute
288
- @options = options
289
- @set = options[:set]
290
- super( parent )
261
+ def initialize( parent, field )
262
+ raise "RestrictedDelegate must have a :set in options" if field.set.nil?
263
+ super
291
264
  end
292
265
 
293
266
  def needs_combo?
@@ -299,10 +272,22 @@ class RestrictedDelegate < ComboDelegate
299
272
  end
300
273
 
301
274
  def populate( editor, model_index )
302
- @set.each do |item|
303
- editor.add_item( item, item.to_variant )
275
+ field.set.each do |item|
276
+ if item.is_a?( Array )
277
+ # this is a hash, so use key as db value
278
+ # and value as display value
279
+ editor.add_item( item.last, item.first.to_variant )
280
+ else
281
+ editor.add_item( item, item.to_variant )
282
+ end
304
283
  end
305
284
  end
285
+
286
+ #~ def translate_from_editor_text( editor, text )
287
+ #~ item_index = editor.find_text( text )
288
+ #~ item_data = editor.item_data( item_index )
289
+ #~ item_data.to_int
290
+ #~ end
306
291
  end
307
292
 
308
293
  # Edit a relation from an id and display a list of relevant entries.
@@ -313,25 +298,24 @@ end
313
298
  # and the item text is fetched from them using attribute_path.
314
299
  class RelationalDelegate < ComboDelegate
315
300
 
316
- def initialize( parent, attribute, options )
317
- @model_class = ( options[:class_name] || attribute.to_s.classify ).constantize
318
- # TODO this doesn't seem to be used
319
- @attribute = attribute.to_s
320
- @options = options.clone
321
- unless @options[:conditions].nil?
322
- @options[:conditions].gsub!( /true/, @model_class.connection.quoted_true )
323
- @options[:conditions].gsub!( /false/, @model_class.connection.quoted_false )
301
+ def initialize( parent, field )
302
+ super
303
+ unless find_options[:conditions].nil?
304
+ find_options[:conditions].gsub!( /true/, entity_class.connection.quoted_true )
305
+ find_options[:conditions].gsub!( /false/, entity_class.connection.quoted_false )
324
306
  end
325
- [ :class_name, :sample, :format ].each {|x| @options.delete x }
326
- super( parent )
307
+ end
308
+
309
+ def entity_class
310
+ @entity_class ||= ( field.class_name || field.attribute.to_s.classify ).constantize
327
311
  end
328
312
 
329
313
  def needs_combo?
330
- @model_class.count( :conditions => @options[:conditions] ) > 0
314
+ entity_class.count( :conditions => find_options[:conditions] ) > 0
331
315
  end
332
316
 
333
317
  def empty_set_message
334
- "There must be records in #{@model_class.name.humanize} for this field to be editable."
318
+ "There must be records in #{entity_class.name.humanize} for this field to be editable."
335
319
  end
336
320
 
337
321
  # add the current item, unless it's already in the combo data
@@ -353,7 +337,7 @@ class RelationalDelegate < ComboDelegate
353
337
 
354
338
  def populate( editor, model_index )
355
339
  # add set of all possible related entities
356
- @model_class.find( :all, collect_finder_options( @options ) ).each do |x|
340
+ entity_class.find( :all, find_options ).each do |x|
357
341
  add_to_list( editor, model_index, x )
358
342
  end
359
343
  end
@@ -387,7 +371,7 @@ class RelationalDelegate < ComboDelegate
387
371
  # get the entity it refers to, if there is one
388
372
  # use find_by_id so that if it's not found, nil will
389
373
  # be returned
390
- @model_class.find_by_id( item_data.to_int )
374
+ entity_class.find_by_id( item_data.to_int )
391
375
  end
392
376
  end
393
377
 
@@ -0,0 +1,101 @@
1
+ begin
2
+ ActiveRecord::Dirty
3
+ rescue NameError
4
+
5
+ module ActiveRecord
6
+ # Define ActiveRecord::Dirty if it isn't already defined by ActiveRecord,
7
+ # which it is in 2.1 and up.
8
+ module Dirty
9
+ def self.included(base)
10
+ base.attribute_method_suffix '_changed?', '_change', '_original'
11
+ base.alias_method_chain :read_attribute, :dirty
12
+ base.alias_method_chain :write_attribute, :dirty
13
+ base.alias_method_chain :save, :dirty
14
+ end
15
+
16
+ # Do any attributes have unsaved changes?
17
+ # person.changed? # => false
18
+ # person.name = 'bob'
19
+ # person.changed? # => true
20
+ def changed?
21
+ !changed_attributes.empty?
22
+ end
23
+
24
+ # List of attributes with unsaved changes.
25
+ # person.changed # => []
26
+ # person.name = 'bob'
27
+ # person.changed # => ['name']
28
+ def changed
29
+ changed_attributes.keys
30
+ end
31
+
32
+ # Map of changed attrs => [original value, new value]
33
+ # person.changes # => {}
34
+ # person.name = 'bob'
35
+ # person.changes # => { 'name' => ['bill', 'bob'] }
36
+ def changes
37
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
38
+ end
39
+
40
+
41
+ # Clear changed attributes after they are saved.
42
+ def save_with_dirty(*args) #:nodoc:
43
+ save_without_dirty(*args)
44
+ ensure
45
+ changed_attributes.clear
46
+ end
47
+
48
+ private
49
+
50
+ # Map of change attr => original value.
51
+ def changed_attributes
52
+ @changed_attributes ||= {}
53
+ end
54
+
55
+
56
+ # Wrap read_attribute to freeze its result.
57
+ def read_attribute_with_dirty(attr)
58
+ read_attribute_without_dirty(attr).freeze
59
+ end
60
+
61
+ # Wrap write_attribute to remember original attribute value.
62
+ def write_attribute_with_dirty(attr, value)
63
+ attr = attr.to_s
64
+
65
+ # The attribute already has an unsaved change.
66
+ unless changed_attributes.include?(attr)
67
+ old = read_attribute(attr)
68
+
69
+ # Remember the original value if it's different.
70
+ changed_attributes[attr] = old unless old == value
71
+ end
72
+
73
+ # Carry on.
74
+ write_attribute_without_dirty(attr, value)
75
+ end
76
+
77
+
78
+ # Handle *_changed? for method_missing.
79
+ def attribute_changed?(attr)
80
+ changed_attributes.include?(attr)
81
+ end
82
+
83
+ # Handle *_change for method_missing.
84
+ def attribute_change(attr)
85
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
86
+ end
87
+
88
+ # Handle *_original for method_missing.
89
+ def attribute_original(attr)
90
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
91
+ end
92
+ end # module Dirty
93
+
94
+ # put this here so it's part of the compatibility rescue block.
95
+ class Base
96
+ include ActiveRecord::Dirty
97
+ end
98
+
99
+ end # module ActiveRecord
100
+
101
+ end # rescue