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.
- 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
|