clevic 0.11.1 → 0.12.0

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.
@@ -39,7 +39,7 @@ module Qt
39
39
 
40
40
  PasteRole = UserRole + 1
41
41
 
42
- class ItemDelegate
42
+ class AbstractItemDelegate
43
43
  # overridden in EntryDelegate subclasses
44
44
  def full_edit
45
45
  end
data/lib/clevic/field.rb CHANGED
@@ -108,17 +108,35 @@ class Field
108
108
  # - A symbol is treated as a method to be call on an entity
109
109
  property :tooltip
110
110
 
111
- # The set of allowed values for restricted fields. If it's a hash, the
112
- # keys will be stored in the db, and the values displayed in the UI.
111
+ # An Enumerable of allowed values for restricted fields. If each yields
112
+ # two values (like it does for a Hash), the
113
+ # first will be stored in the db, and the second displayed in the UI.
114
+ # If it's a proc, it must return an Enumerable as above.
113
115
  property :set
114
116
 
117
+ # When this is true, only the values in the combo may be entered.
118
+ # Otherwise the text-entry part of the combo can be used to enter
119
+ # non-listed values. Default is true if a set is explicitly specified.
120
+ # Otherwise depends on the field type.
121
+ property :restricted
122
+
115
123
  # Only for the distinct field type. The values will be sorted either with the
116
124
  # most used values first (:frequency => true) or in alphabetical order (:description => true).
117
125
  property :frequency, :description
118
126
 
119
- # Not implemented. Default value for this field for new records. Not sure how to populate it though.
127
+ # Default value for this field for new records.
128
+ # Can be a Proc or a value. A value will just be
129
+ # set, a proc will be executed with the entity as a parameter.
120
130
  property :default
121
131
 
132
+ # the property used for finding the field, ie by TableModel#field_column
133
+ # defaults to the attribute.
134
+ property :id
135
+
136
+ # called when the data in this field changes. Either a proc( clevic_view, table_view, model_index ) or a symbol
137
+ # for a method( view, model_index ) on the Clevic::View object. Both will take
138
+ property :notify_data_changed
139
+
122
140
  # properties for ActiveRecord options
123
141
  # There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected
124
142
  # each element becomes a property.
@@ -175,6 +193,8 @@ EOF
175
193
 
176
194
  # instance variables
177
195
  @attribute = attribute
196
+ # default to attribute
197
+ @id = attribute
178
198
  @entity_class = entity_class
179
199
  @visible = true
180
200
 
@@ -239,7 +259,7 @@ EOF
239
259
  else
240
260
  @is_date_time ||=
241
261
  if display.nil?
242
- [:time, :date, :datetime].include?( meta.type )
262
+ [:time, :date, :datetime, :timestamp].include?( meta.type )
243
263
  else
244
264
  # it's a virtual field, so we need to use the value
245
265
  value.is_a?( Date ) || value.is_a?( Time )
@@ -374,7 +394,7 @@ EOF
374
394
 
375
395
  # Called by Clevic::TableModel to get the tooltip value
376
396
  def tooltip_for( entity )
377
- cache_value_for( :background, entity )
397
+ cache_value_for( :tooltip, entity )
378
398
  end
379
399
 
380
400
  # TODO Doesn't do anything useful yet.
@@ -403,6 +423,36 @@ EOF
403
423
  cache_value_for( :background, entity ) {|x| string_or_color(x)}
404
424
  end
405
425
 
426
+ def set_default_for( entity )
427
+ begin
428
+ entity[attribute] =
429
+ case default
430
+ when String
431
+ default
432
+ when Proc
433
+ default.call( entity )
434
+ end
435
+ rescue Exception => e
436
+ puts e.message
437
+ puts e.backtrace
438
+ end
439
+ end
440
+
441
+ def set_for( entity )
442
+ case set
443
+ when Proc
444
+ # the Proc should return an enumerable
445
+ set.call( entity )
446
+
447
+ when Symbol
448
+ entity.send( set )
449
+
450
+ else
451
+ # assume its an Enumerable
452
+ set
453
+ end
454
+ end
455
+
406
456
  protected
407
457
 
408
458
  # call the conversion_block with the value, or just return the
@@ -0,0 +1,51 @@
1
+ module Clevic
2
+ class FilterCommand
3
+ def initialize( table_view, filter_indexes, filter_conditions )
4
+ @table_view = table_view
5
+ @filter_conditions = filter_conditions
6
+ @filter_indexes = filter_indexes
7
+
8
+ # Better make the status message now, before the indexes become invalid
9
+ @status_message =
10
+ begin
11
+ "Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.gui_value}"
12
+ rescue
13
+ "Filtered"
14
+ end
15
+ end
16
+
17
+ # Do the filtering. Return true if successful, false otherwise.
18
+ def doit
19
+ begin
20
+ # store current AR conditions
21
+ @stored_conditions = @table_view.model.cache_table.options
22
+
23
+ # store auto_new
24
+ @auto_new = @table_view.model.auto_new
25
+
26
+ # reload cache table with new conditions
27
+ @table_view.model.auto_new = false
28
+ @table_view.model.reload_data( @filter_conditions )
29
+ rescue Exception => e
30
+ puts
31
+ puts e.message
32
+ puts e.backtrace
33
+ false
34
+ end
35
+ true
36
+ end
37
+
38
+ def undo
39
+ # restore auto_new
40
+ @table_view.model.auto_new = @auto_new
41
+
42
+ # reload cache table with stored AR conditions
43
+ @table_view.model.reload_data( @stored_conditions )
44
+ end
45
+
46
+ # return a message based on the conditions
47
+ def status_message
48
+ @status_message
49
+ end
50
+ end
51
+ end
@@ -1,7 +1,9 @@
1
1
  require 'activerecord'
2
+ require 'facets/dictionary'
2
3
 
3
4
  require 'clevic/table_model.rb'
4
5
  require 'clevic/delegates.rb'
6
+ require 'clevic/text_delegate.rb'
5
7
  require 'clevic/cache_table.rb'
6
8
  require 'clevic/field.rb'
7
9
 
@@ -273,11 +275,12 @@ class ModelBuilder
273
275
  @entity_view = entity_view
274
276
  @auto_new = true
275
277
  @read_only = false
276
- @fields = []
278
+ @fields = Dictionary.new
277
279
  exec_ui_block( &block )
278
280
  end
279
281
 
280
282
  attr_accessor :entity_view
283
+ attr_accessor :find_options
281
284
 
282
285
  # execute a block containing method calls understood by Clevic::ModelBuilder
283
286
  # arg can be something that responds to define_ui_block,
@@ -301,13 +304,13 @@ class ModelBuilder
301
304
  # The collection of Clevic::Field instances where visible == true.
302
305
  # the visible may go away.
303
306
  def fields
304
- @fields.reject{|x| !x.visible}
307
+ @fields.reject{|id,field| !field.visible}
305
308
  end
306
309
 
307
310
  # return the index of the named field in the collection of fields.
308
311
  def index( field_name_sym )
309
312
  retval = nil
310
- fields.each_with_index{|x,i| retval = i if x.attribute == field_name_sym.to_sym }
313
+ fields.each_with_index{|id,field,i| retval = i if field.attribute == field_name_sym.to_sym }
311
314
  retval
312
315
  end
313
316
 
@@ -332,7 +335,15 @@ class ModelBuilder
332
335
  # an ordinary field, edited in place with a text box
333
336
  def plain( attribute, options = {}, &block )
334
337
  read_only_default!( attribute, options )
335
- @fields << Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
338
+ @fields[attribute] = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
339
+ end
340
+
341
+ # an ordinary field like plain, except that a larger edit area can be used
342
+ def text( attribute, options = {}, &block )
343
+ read_only_default!( attribute, options )
344
+ field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
345
+ field.delegate = TextDelegate.new( nil, field )
346
+ @fields[attribute] = field
336
347
  end
337
348
 
338
349
  # Returns a Clevic::Field with a DistinctDelegate, in other words
@@ -340,23 +351,27 @@ class ModelBuilder
340
351
  def distinct( attribute, options = {}, &block )
341
352
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
342
353
  field.delegate = DistinctDelegate.new( nil, field )
343
- @fields << field
354
+ @fields[attribute] = field
344
355
  end
345
-
346
- # Returns a Clevic::Field with a RestrictedDelegate,
347
- # a combo box, but restricted to a specified set, from the :set option.
348
- def restricted( attribute, options = {}, &block )
349
-
356
+
357
+ # a combo box with a set of supplied values
358
+ def combo( attribute, options = {}, &block )
350
359
  field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
351
- raise "field #{attribute} restricted must have a set" if field.set.nil?
352
360
 
353
- # TODO this really belongs in a separate 'map' field
361
+ # TODO this really belongs in a separate 'map' field?
362
+ # or maybe put it in SetDelegate?
354
363
  if field.set.is_a? Hash
355
- field.format = lambda{|x| field.set[x]}
364
+ field.format ||= lambda{|x| field.set[x]}
356
365
  end
357
366
 
358
- field.delegate = RestrictedDelegate.new( nil, field )
359
- @fields << field
367
+ field.delegate = SetDelegate.new( nil, field )
368
+ @fields[attribute] = field
369
+ end
370
+
371
+ # Returns a Clevic::Field with a restricted SetDelegate,
372
+ def restricted( attribute, options = {}, &block )
373
+ options[:restricted] = true
374
+ combo( attribute, options, &block )
360
375
  end
361
376
 
362
377
  # for foreign keys. Edited with a combo box using values from the specified
@@ -372,7 +387,7 @@ class ModelBuilder
372
387
  # check after all possible options have been collected
373
388
  raise ":display must be specified" if field.display.nil?
374
389
  field.delegate = RelationalDelegate.new( nil, field )
375
- @fields << field
390
+ @fields[attribute] = field
376
391
  end
377
392
 
378
393
  # mostly used in the new block to define the set of records
@@ -464,7 +479,7 @@ class ModelBuilder
464
479
 
465
480
  # return the named Clevic::Field object
466
481
  def field( attribute )
467
- @fields.find {|x| x.attribute == attribute }
482
+ @fields.find {|id,field| field.attribute == attribute }
468
483
  end
469
484
 
470
485
  # This takes all the information collected
@@ -475,14 +490,16 @@ class ModelBuilder
475
490
  # using @model here because otherwise the view's
476
491
  # reference to this very same model is garbage collected.
477
492
  @model = Clevic::TableModel.new( table_view )
478
- table_view.object_name = @object_name
493
+ @model.builder = self
479
494
  @model.entity_view = entity_view
480
- @model.fields = @fields
495
+ @model.fields = @fields.values
481
496
  @model.read_only = @read_only
482
497
  @model.auto_new = auto_new?
483
498
 
499
+ # setup model
500
+ table_view.object_name = @object_name
484
501
  # set parent for all delegates
485
- fields.each {|x| x.delegate.parent = table_view unless x.delegate.nil? }
502
+ fields.each {|id,field| field.delegate.parent = table_view unless field.delegate.nil? }
486
503
 
487
504
  # the data
488
505
  @model.collection = records
@@ -497,7 +514,7 @@ protected
497
514
  #--
498
515
  # TODO ActiveRecord-2.1 has smarter includes
499
516
  def add_include_options
500
- fields.each do |field|
517
+ fields.each do |id,field|
501
518
  if field.delegate.class == RelationalDelegate
502
519
  @options[:include] ||= []
503
520
  @options[:include] << field.attribute
@@ -539,7 +556,7 @@ protected
539
556
  def set_records( arg )
540
557
  if arg.class == Hash
541
558
  # need to defer this until all fields are collected
542
- @options = arg
559
+ @find_options = arg
543
560
  else
544
561
  @records = arg
545
562
  end
@@ -550,7 +567,7 @@ protected
550
567
  def get_records
551
568
  if @records.nil?
552
569
  #~ add_include_options
553
- @records = CacheTable.new( entity_class, @options.merge( :auto_new => auto_new? ) )
570
+ @records = CacheTable.new( entity_class, @find_options )
554
571
  end
555
572
  @records
556
573
  end
@@ -19,6 +19,7 @@ class TableModel < Qt::AbstractTableModel
19
19
 
20
20
  # the CacheTable of Clevic::Record or ActiveRecord::Base objects
21
21
  attr_reader :collection
22
+ alias_method :cache_table, :collection
22
23
 
23
24
  # the collection of Clevic::Field objects
24
25
  attr_reader :fields
@@ -29,8 +30,10 @@ class TableModel < Qt::AbstractTableModel
29
30
  # should this model create a new empty record by default?
30
31
  attr_accessor :auto_new
31
32
  def auto_new?; auto_new; end
33
+ def auto_new?; auto_new; end
32
34
 
33
35
  attr_accessor :entity_view
36
+ attr_accessor :builder
34
37
 
35
38
  def entity_class
36
39
  entity_view.entity_class
@@ -55,6 +58,16 @@ class TableModel < Qt::AbstractTableModel
55
58
  @attributes = nil
56
59
  end
57
60
 
61
+ # field is a symbol or string referring to a column.
62
+ # returns the index of that field.
63
+ def field_column( field )
64
+ fields.each_with_index {|x,i| return i if x.id == field.to_sym }
65
+ end
66
+
67
+ def field_for_index( model_index )
68
+ fields[model_index.column]
69
+ end
70
+
58
71
  def labels
59
72
  @labels ||= fields.map {|x| x.label }
60
73
  end
@@ -116,10 +129,19 @@ class TableModel < Qt::AbstractTableModel
116
129
  @metadatas[column]
117
130
  end
118
131
 
132
+ # add a new item, and set defaults from the Clevic::View
119
133
  def add_new_item
120
- # 1 new row
121
134
  begin_insert_rows( Qt::ModelIndex.invalid, row_count, row_count )
122
- collection << entity_class.new
135
+ # set default values without triggering changed
136
+ entity = entity_class.new
137
+ fields.each do |f|
138
+ unless f.default.nil?
139
+ f.set_default_for( entity )
140
+ end
141
+ end
142
+
143
+ collection << entity
144
+
123
145
  end_insert_rows
124
146
  end
125
147
 
@@ -136,6 +158,9 @@ class TableModel < Qt::AbstractTableModel
136
158
  # destroy the db object, and its associated table row
137
159
  removed.destroy
138
160
  end
161
+
162
+ # create a new row if auto_new is on
163
+ add_new_item if collection.empty? && auto_new?
139
164
  end
140
165
 
141
166
  # save the AR model at the given index, if it's dirty
@@ -144,7 +169,9 @@ class TableModel < Qt::AbstractTableModel
144
169
  return false if item.nil?
145
170
  if item.changed?
146
171
  if item.valid?
147
- item.save
172
+ retval = item.save
173
+ emit headerDataChanged( Qt::Vertical, index.row, index.row )
174
+ retval
148
175
  else
149
176
  false
150
177
  end
@@ -190,8 +217,8 @@ class TableModel < Qt::AbstractTableModel
190
217
  retval
191
218
  end
192
219
 
193
- def reload_data( options = {} )
194
- # renew cache
220
+ def reload_data( options = nil )
221
+ # renew cache. All records will be dropped and reloaded.
195
222
  self.collection = self.collection.renew( options )
196
223
  # tell the UI we had a major data change
197
224
  reset
@@ -318,6 +345,7 @@ class TableModel < Qt::AbstractTableModel
318
345
  index.errors.join("\n")
319
346
 
320
347
  # provide a tooltip when an empty relational field is encountered
348
+ # TODO should be part of field definition
321
349
  when index.metadata.type == :association
322
350
  index.field.delegate.if_empty_message
323
351
 
@@ -360,6 +388,7 @@ class TableModel < Qt::AbstractTableModel
360
388
 
361
389
  type = index.metadata.type
362
390
  value = variant.value
391
+ #~ puts "#{type.inspect} is #{value}"
363
392
 
364
393
  # translate the value from the ui to something that
365
394
  # the AR model will understand
@@ -396,12 +425,28 @@ class TableModel < Qt::AbstractTableModel
396
425
  # 01:17, 0117, 117, 1 17, are all accepted
397
426
  when type == :time && value =~ %r{^(\d{1,2}).?(\d{2})$}
398
427
  Time.parse( "#$1:#$2" )
428
+
429
+ # remove thousand separators, allow for space and comma
430
+ # instead of . as a decimal separator
431
+ when type == :decimal
432
+ # do various transforms
433
+ value =
434
+ case
435
+ # accept a space or a comma instead of a . for floats
436
+ when value =~ /(.*?)(\d)[ ,](\d{2})$/
437
+ "#$1#$2.#$3"
438
+ else
439
+ value
440
+ end
399
441
 
442
+ # strip remaining commas
443
+ value.gsub( ',', '' )
444
+
400
445
  else
401
446
  value
402
447
  end
403
448
 
404
- emit dataChanged( index, index )
449
+ data_changed( index )
405
450
  # value conversion was successful
406
451
  true
407
452
  rescue Exception => e
@@ -460,8 +505,68 @@ class TableModel < Qt::AbstractTableModel
460
505
  end
461
506
  end
462
507
 
463
- def field_for_index( model_index )
464
- fields[model_index.column]
508
+ class DataChange
509
+ class ModelIndexProxy
510
+ attr_accessor :row
511
+ attr_accessor :column
512
+
513
+ def initialize( other = nil )
514
+ unless other.nil?
515
+ @row = other.row
516
+ @column = other.column
517
+ end
518
+ end
519
+ end
520
+
521
+ def top_left
522
+ @top_left ||= ModelIndexProxy.new
523
+ end
524
+
525
+ def bottom_right
526
+ @bottom_right ||= ModelIndexProxy.new
527
+ end
528
+
529
+ attr_writer :bottom_right
530
+ attr_writer :top_left
531
+
532
+ attr_reader :index
533
+ def index=( other )
534
+ self.top_left = ModelIndexProxy.new( other )
535
+ self.bottom_right = ModelIndexProxy.new( other )
536
+ end
537
+ end
538
+
539
+ # A rubyish way of doing dataChanged
540
+ # - if args has one element, it's either a single ModelIndex
541
+ # or something that understands top_left and bottom_right. These
542
+ # will be turned into a ModelIndex by calling create_index
543
+ # - if args has two element, assume it's a two ModelIndex instances
544
+ # - otherwise create a new DataChange and pass it to the block.
545
+ def data_changed( *args, &block )
546
+ case args.size
547
+ when 1
548
+ arg = args.first
549
+ if ( arg.respond_to?( :top_left ) && arg.respond_to?( :bottom_right ) ) || arg.is_a?( Qt::ItemSelectionRange )
550
+ # object is a DataChange, or a SelectionRange
551
+ top_left_index = create_index( arg.top_left.row, arg.top_left.column )
552
+ bottom_right_index = create_index( arg.bottom_right.row, arg.bottom_right.column )
553
+ emit dataChanged( top_left_index, bottom_right_index )
554
+ else
555
+ # assume it's a ModelIndex
556
+ emit dataChanged( arg, arg )
557
+ end
558
+
559
+ when 2
560
+ emit dataChanged( args.first, args.last )
561
+
562
+ else
563
+ unless block.nil?
564
+ change = DataChange.new
565
+ block.call( change )
566
+ # recursive call
567
+ data_changed( change )
568
+ end
569
+ end
465
570
  end
466
571
 
467
572
  end