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.
- data/History.txt +9 -0
- data/Manifest.txt +13 -10
- data/README.txt +6 -9
- data/Rakefile +35 -24
- data/TODO +29 -17
- data/bin/clevic +84 -37
- data/config/hoe.rb +7 -3
- data/lib/clevic.rb +2 -4
- data/lib/clevic/browser.rb +37 -49
- data/lib/clevic/cache_table.rb +55 -165
- data/lib/clevic/db_options.rb +32 -21
- data/lib/clevic/default_view.rb +66 -0
- data/lib/clevic/delegates.rb +51 -67
- data/lib/clevic/dirty.rb +101 -0
- data/lib/clevic/extensions.rb +24 -38
- data/lib/clevic/field.rb +400 -99
- data/lib/clevic/item_delegate.rb +32 -33
- data/lib/clevic/model_builder.rb +315 -148
- data/lib/clevic/order_attribute.rb +53 -0
- data/lib/clevic/record.rb +57 -57
- data/lib/clevic/search_dialog.rb +71 -67
- data/lib/clevic/sql_dialects.rb +33 -0
- data/lib/clevic/table_model.rb +73 -120
- data/lib/clevic/table_searcher.rb +165 -0
- data/lib/clevic/table_view.rb +140 -100
- data/lib/clevic/ui/.gitignore +1 -0
- data/lib/clevic/ui/browser_ui.rb +55 -56
- data/lib/clevic/ui/search_dialog_ui.rb +50 -51
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +89 -0
- data/models/accounts_models.rb +12 -9
- data/models/minimal_models.rb +4 -2
- data/models/times_models.rb +41 -25
- data/models/times_sqlite_models.rb +1 -145
- data/models/values_models.rb +15 -16
- data/test/test_cache_table.rb +138 -0
- data/test/test_helper.rb +131 -0
- data/test/test_model_index_extensions.rb +22 -0
- data/test/test_order_attribute.rb +62 -0
- data/test/test_sql_dialects.rb +77 -0
- data/test/test_table_searcher.rb +188 -0
- metadata +36 -20
- data/bin/import-times +0 -128
- data/config/jamis.rb +0 -589
- data/env.sh +0 -1
- data/lib/active_record/dirty.rb +0 -87
- data/lib/clevic/field_builder.rb +0 -42
- data/website/index.html +0 -170
- data/website/index.txt +0 -17
- data/website/screenshot.png +0 -0
- data/website/stylesheets/screen.css +0 -131
- data/website/template.html.erb +0 -41
data/lib/clevic/db_options.rb
CHANGED
@@ -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
|
38
|
-
|
39
|
-
|
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
|
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
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
data/lib/clevic/delegates.rb
CHANGED
@@ -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.
|
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
|
-
|
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 #{
|
238
|
-
from #{
|
239
|
-
where (#{
|
240
|
-
or #{conn.quote_column_name(
|
241
|
-
order by lower(#{
|
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 #{
|
248
|
-
from #{
|
249
|
-
where (#{
|
250
|
-
or #{conn.quote_column_name(
|
251
|
-
group by #{
|
252
|
-
order by count(#{
|
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 =
|
235
|
+
conn = entity_class.connection
|
261
236
|
query =
|
262
237
|
case
|
263
|
-
when
|
238
|
+
when field.description
|
264
239
|
query_order_description( conn, model_index )
|
265
|
-
when
|
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
|
-
|
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,
|
285
|
-
raise "RestrictedDelegate must have a :set in options"
|
286
|
-
|
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
|
-
|
303
|
-
|
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,
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
326
|
-
|
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
|
-
|
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 #{
|
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
|
-
|
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
|
-
|
374
|
+
entity_class.find_by_id( item_data.to_int )
|
391
375
|
end
|
392
376
|
end
|
393
377
|
|
data/lib/clevic/dirty.rb
ADDED
@@ -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
|