clevic 0.11.1 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +21 -0
- data/Manifest.txt +4 -2
- data/Rakefile +32 -139
- data/TODO +12 -15
- data/bin/clevic +5 -0
- data/lib/clevic/browser.rb +1 -1
- data/lib/clevic/cache_table.rb +15 -34
- data/lib/clevic/default_view.rb +6 -0
- data/lib/clevic/delegates.rb +25 -19
- data/lib/clevic/extensions.rb +1 -1
- data/lib/clevic/field.rb +55 -5
- data/lib/clevic/filter_command.rb +51 -0
- data/lib/clevic/model_builder.rb +40 -23
- data/lib/clevic/table_model.rb +113 -8
- data/lib/clevic/table_view.rb +207 -92
- data/lib/clevic/text_delegate.rb +84 -0
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +28 -2
- data/models/accounts_models.rb +34 -40
- data/models/times_models.rb +69 -33
- data/tasks/clevic.rake +111 -0
- data/tasks/rdoc.rake +16 -0
- data/test/test_cache_table.rb +0 -23
- data/test/test_table_model.rb +61 -0
- data/test/test_table_searcher.rb +1 -1
- metadata +45 -10
- data/config/hoe.rb +0 -82
- data/config/requirements.rb +0 -15
data/lib/clevic/extensions.rb
CHANGED
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
|
-
#
|
112
|
-
#
|
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
|
-
#
|
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( :
|
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
|
data/lib/clevic/model_builder.rb
CHANGED
@@ -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{|
|
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{|
|
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
|
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
|
354
|
+
@fields[attribute] = field
|
344
355
|
end
|
345
|
-
|
346
|
-
#
|
347
|
-
|
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
|
364
|
+
field.format ||= lambda{|x| field.set[x]}
|
356
365
|
end
|
357
366
|
|
358
|
-
field.delegate =
|
359
|
-
@fields
|
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
|
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 {|
|
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
|
-
|
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 {|
|
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
|
-
@
|
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, @
|
570
|
+
@records = CacheTable.new( entity_class, @find_options )
|
554
571
|
end
|
555
572
|
@records
|
556
573
|
end
|
data/lib/clevic/table_model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
464
|
-
|
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
|