clevic 0.5.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.
@@ -0,0 +1,21 @@
1
+ # set up defaults
2
+ # $options[:database] to be defined with the models
3
+ require 'active_record'
4
+
5
+ $options ||= {}
6
+ $options[:adapter] ||= 'postgresql'
7
+ $options[:host] ||= 'localhost'
8
+ $options[:username] ||= 'panic'
9
+ $options[:password] ||= ''
10
+
11
+ ActiveRecord::Base.establish_connection( $options )
12
+ ActiveRecord::Base.logger = Logger.new(STDOUT) if $options[:verbose]
13
+ #~ ActiveRecord.colorize_logging = false
14
+
15
+ # workaround for the date freeze issue
16
+ class Date
17
+ def freeze
18
+ self
19
+ end
20
+ end
21
+
@@ -0,0 +1,383 @@
1
+ require 'clevic/item_delegate.rb'
2
+
3
+ module Clevic
4
+
5
+ =begin rdoc
6
+ Base class for other delegates using Combo boxes. Emit focus out signals,
7
+ because ComboBox stupidly doesn't.
8
+
9
+ Generally these will be created using a ModelBuilder.
10
+ =end
11
+ class ComboDelegate < Clevic::ItemDelegate
12
+ def initialize( parent )
13
+ super
14
+ @@active_record_options ||= [ :conditions, :class_name, :order ]
15
+ end
16
+
17
+ # Convert Qt:: constants from the integer value to a string value.
18
+ def hint_string( hint )
19
+ hs = String.new
20
+ Qt::AbstractItemDelegate.constants.each do |x|
21
+ hs = x if eval( "Qt::AbstractItemDelegate::#{x}.to_i" ) == hint.to_i
22
+ end
23
+ hs
24
+ end
25
+
26
+ def dump_editor_state( editor )
27
+ if $options[:debug]
28
+ puts "#{self.class.name}"
29
+ puts "editor.completer.completion_count: #{editor.completer.completion_count}"
30
+ puts "editor.completer.current_completion: #{editor.completer.current_completion}"
31
+ puts "editor.find_text( editor.completer.current_completion ): #{editor.find_text( editor.completer.current_completion )}"
32
+ puts "editor.current_text: #{editor.current_text}"
33
+ puts "editor.count: #{editor.count}"
34
+ puts "editor.completer.current_row: #{editor.completer.current_row}"
35
+ puts "editor.item_data( editor.current_index ): #{editor.item_data( editor.current_index ).inspect}"
36
+ puts
37
+ end
38
+ end
39
+
40
+ # open the combo box, just like if f4 was pressed
41
+ def full_edit
42
+ if is_combo?( @editor )
43
+ @editor.show_popup
44
+ end
45
+ end
46
+
47
+ # returns true if the editor allows values outside of a predefined
48
+ # range, false otherwise.
49
+ def restricted?
50
+ false
51
+ end
52
+
53
+ # TODO fetch this from the model definition
54
+ def allow_null?
55
+ true
56
+ end
57
+
58
+ # Subclasses should override this to fill the combo box
59
+ # list with values.
60
+ def populate( editor, model_index )
61
+ raise "subclass responsibility"
62
+ end
63
+
64
+ # return true if this delegate needs a combo, false otherwise
65
+ def needs_combo?
66
+ raise "subclass responsibility"
67
+ end
68
+
69
+ def is_combo?( editor )
70
+ editor.class == Qt::ComboBox
71
+ end
72
+
73
+ # return true if this field has no data (needs_combo? is false)
74
+ # and is at the same time restricted (ie needs data from somewhere else)
75
+ def empty_set?
76
+ !needs_combo? && restricted?
77
+ end
78
+
79
+ # the message to display if the set is empty, and
80
+ # the delegate is restricted to a predefined set.
81
+ def empty_set_message
82
+ raise "subclass responsibility"
83
+ end
84
+
85
+ # if this delegate has an empty set, return the message, otherwise
86
+ # return nil.
87
+ def if_empty_message
88
+ if empty_set?
89
+ empty_set_message
90
+ end
91
+ end
92
+
93
+ def populate_current( editor, model_index )
94
+ # add the current entry, if it isn't there already
95
+ # TODO add it in the correct order
96
+ if ( editor.find_data( model_index.gui_value.to_variant ) == -1 )
97
+ editor.add_item( model_index.gui_value, model_index.gui_value.to_variant )
98
+ end
99
+ end
100
+
101
+ # Override the Qt method. Create a ComboBox widget and fill it with the possible values.
102
+ def createEditor( parent_widget, style_option_view_item, model_index )
103
+ if needs_combo?
104
+ @editor = Qt::ComboBox.new( parent_widget )
105
+
106
+ # subclasses fill in the rest of the entries
107
+ populate( @editor, model_index )
108
+
109
+ # add the current item, if it isn't there already
110
+ populate_current( @editor, model_index )
111
+
112
+ # create a nil entry
113
+ if allow_null?
114
+ if ( @editor.find_data( nil.to_variant ) == -1 )
115
+ @editor.add_item( '', nil.to_variant )
116
+ end
117
+ end
118
+
119
+ # allow prefix matching from the keyboard
120
+ @editor.editable = true
121
+
122
+ # don't insert if restricted
123
+ @editor.insert_policy = Qt::ComboBox::NoInsert if restricted?
124
+ else
125
+ @editor =
126
+ if restricted?
127
+ emit parent.status_text( empty_set_message )
128
+ nil
129
+ else
130
+ Qt::LineEdit.new( model_index.gui_value, parent_widget )
131
+ end
132
+ end
133
+ @editor
134
+ end
135
+
136
+ # Override the Qt::ItemDelegate method.
137
+ def updateEditorGeometry( editor, style_option_view_item, model_index )
138
+ rect = style_option_view_item.rect
139
+
140
+ # ask the editor for how much space it wants, and set the editor
141
+ # to that size when it displays in the table
142
+ rect.set_width( [editor.size_hint.width,rect.width].max ) if is_combo?( editor )
143
+ editor.set_geometry( rect )
144
+ end
145
+
146
+ # Override the Qt method to send data to the editor from the model.
147
+ def setEditorData( editor, model_index )
148
+ if is_combo?( editor )
149
+ editor.current_index = editor.find_data( model_index.attribute_value.to_variant )
150
+ editor.line_edit.select_all if editor.editable
151
+ else
152
+ editor.text = model_index.gui_value
153
+ end
154
+ end
155
+
156
+ # This translates the text from the editor into something that is
157
+ # stored in an underlying model. Intended to be overridden by subclasses.
158
+ def translate_from_editor_text( editor, text )
159
+ index = editor.find_text( text )
160
+ if index == -1
161
+ text unless restricted?
162
+ else
163
+ editor.item_data( index ).value
164
+ end
165
+ end
166
+
167
+ # Send the data from the editor to the model. The data will
168
+ # be translated by translate_from_editor_text,
169
+ def setModelData( editor, abstract_item_model, model_index )
170
+ if is_combo?( editor )
171
+ dump_editor_state( editor )
172
+ value =
173
+ if editor.completer.current_row == -1
174
+ # item doesn't exist in the list, add it if not restricted
175
+ editor.current_text unless restricted?
176
+ elsif editor.completer.completion_count == editor.count
177
+ # selection from drop down. if it's empty, we want a nil
178
+ editor.current_text
179
+ else
180
+ # there is a matching completion, so use it
181
+ editor.completer.current_completion
182
+ end
183
+
184
+ if value != nil
185
+ model_index.attribute_value = translate_from_editor_text( editor, value )
186
+ end
187
+
188
+ else
189
+ model_index.gui_value = editor.text
190
+ end
191
+ emit abstract_item_model.dataChanged( model_index, model_index )
192
+ end
193
+
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
+ end
210
+
211
+ # Provide a list of all values in this field,
212
+ # and allow new values to be entered.
213
+ # :frequency can be set as an option. Boolean. If it's true
214
+ # the options are sorted in order of most frequently used first.
215
+ class DistinctDelegate < ComboDelegate
216
+
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] ||= 'true'
223
+ super( parent )
224
+ end
225
+
226
+ def needs_combo?
227
+ # works except when there is a '' in the column
228
+ @ar_model.count( @attribute, collect_finder_options( @options ) ) > 0
229
+ end
230
+
231
+ def populate_current( editor, model_index )
232
+ # already done in the SQL query in populate, so don't even check
233
+ end
234
+
235
+ def query_order_description( conn, model_index )
236
+ <<-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})
242
+ EOF
243
+ end
244
+
245
+ def query_order_frequency( conn, model_index )
246
+ <<-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
253
+ EOF
254
+ end
255
+
256
+ def populate( editor, model_index )
257
+ # we only use the first column, so use the second
258
+ # column to sort by, since SQL requires the order by clause
259
+ # to be in the select list where distinct is involved
260
+ conn = @ar_model.connection
261
+ query =
262
+ if @options[:frequency]
263
+ query_order_frequency( conn, model_index )
264
+ else
265
+ query_order_description( conn, model_index )
266
+ end
267
+ rs = conn.execute( query )
268
+ rs.each do |row|
269
+ editor.add_item( row[0], row[0].to_variant )
270
+ end
271
+ end
272
+
273
+ def translate_from_editor_text( editor, text )
274
+ text
275
+ end
276
+ end
277
+
278
+ # A Combo box which only allows a restricted set of value to be entered.
279
+ class RestrictedDelegate < ComboDelegate
280
+ # options must contain a :set => [ ... ] to specify the set of values.
281
+ def initialize( parent, attribute, model_class, options )
282
+ raise "RestrictedDelegate must have a :set in options" unless options.has_key?( :set )
283
+ @ar_model = model_class
284
+ @attribute = attribute
285
+ @options = options
286
+ @set = options[:set]
287
+ super( parent )
288
+ end
289
+
290
+ def needs_combo?
291
+ true
292
+ end
293
+
294
+ def restricted?
295
+ true
296
+ end
297
+
298
+ def populate( editor, model_index )
299
+ @set.each do |item|
300
+ editor.add_item( item, item.to_variant )
301
+ end
302
+ end
303
+ end
304
+
305
+ # Edit a relation from an id and display a list of relevant entries.
306
+ #
307
+ # attribute_path is the full dotted path to get from the entity in the
308
+ # model to the values displayed in the combo box.
309
+ #
310
+ # The ids of the ActiveRecord models are stored in the item data
311
+ # and the item text is fetched from them using attribute_path.
312
+ class RelationalDelegate < ComboDelegate
313
+
314
+ def initialize( parent, attribute_path, options )
315
+ @model_class = ( options[:class_name] || attribute_path[0].to_s.classify ).constantize
316
+ @attribute_path = attribute_path[1..-1].join('.')
317
+ @options = options.clone
318
+ [ :class_name, :sample, :format ].each {|x| @options.delete x }
319
+ super( parent )
320
+ end
321
+
322
+ def needs_combo?
323
+ @model_class.count( :conditions => @options[:conditions] ) > 0
324
+ end
325
+
326
+ def empty_set_message
327
+ "There must be records in #{@model_class.name.humanize} for this field to be editable."
328
+ end
329
+
330
+ # add the current item, unless it's already in the combo data
331
+ def populate_current( editor, model_index )
332
+ # always add the current selection, if it isn't already there
333
+ # and it makes sense. This is to make sure that if the list
334
+ # is filtered, we always have the current value if the filter
335
+ # excludes it
336
+ unless model_index.nil?
337
+ item = model_index.attribute_value
338
+ if item
339
+ item_index = editor.find_data( item.id.to_variant )
340
+ if item_index == -1
341
+ editor.add_item( item[@attribute_path], item.id.to_variant )
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def populate( editor, model_index )
348
+ # add set of all possible related entities
349
+ @model_class.find( :all, collect_finder_options( @options ) ).each do |x|
350
+ editor.add_item( x[@attribute_path], x.id.to_variant )
351
+ end
352
+ end
353
+
354
+ # send data to the editor
355
+ def setEditorData( editor, model_index )
356
+ if is_combo?( editor )
357
+ editor.current_index = editor.find_data( model_index.attribute_value.id.to_variant )
358
+ editor.line_edit.select_all
359
+ end
360
+ end
361
+
362
+ # don't allow new values
363
+ def restricted?
364
+ true
365
+ end
366
+
367
+ # return an AR entity object
368
+ def translate_from_editor_text( editor, text )
369
+ item_index = editor.find_text( text )
370
+
371
+ # fetch record id from editor item_data
372
+ item_data = editor.item_data( item_index )
373
+ if item_data.valid?
374
+ # get the entity it refers to, if there is one
375
+ # use find_by_id so that if it's not found, nil will
376
+ # be returned
377
+ @model_class.find_by_id( item_data.to_int )
378
+ end
379
+ end
380
+
381
+ end
382
+
383
+ end
@@ -0,0 +1,133 @@
1
+ # extensions specific to clevic
2
+
3
+ require 'qtext/flags.rb'
4
+
5
+ module ActiveRecord
6
+ class Base
7
+ # recursively calls each entry in path_ary
8
+ def evaluate_path( path_ary )
9
+ path_ary.inject( self ) do |value, att|
10
+ if value.nil?
11
+ nil
12
+ else
13
+ value.send( att )
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.has_attribute?( attribute_sym )
19
+ if column_names.include?( attribute_sym.to_s )
20
+ true
21
+ elsif reflections.has_key?( attribute_sym )
22
+ true
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def self.attribute_names
29
+ ( column_names + reflections.keys.map {|sym| sym.to_s} ).sort
30
+ end
31
+ end
32
+ end
33
+
34
+ # convenience methods
35
+ module Qt
36
+
37
+ PasteRole = UserRole + 1
38
+
39
+ class ItemDelegate
40
+ # overridden in EntryDelegate subclasses
41
+ def full_edit
42
+ end
43
+ end
44
+
45
+ # This provides a bunch of methods to get easy access to the entity
46
+ # and it's values directly from the index without having to keep
47
+ # asking the model and jumping through other unncessary hoops
48
+ class ModelIndex
49
+ # the value to be displayed in the gui for this index
50
+ def gui_value
51
+ return nil if entity.nil?
52
+ entity.evaluate_path( attribute_path )
53
+ end
54
+
55
+ # set the value returned from the gui, as whatever the underlying
56
+ # entity wants it to be
57
+ # TODO this will break for more than 2 objects in a path
58
+ def gui_value=( obj )
59
+ entity.send( "#{model.dots[column]}=", obj )
60
+ end
61
+
62
+ def dump
63
+ <<-EOF
64
+ field_name: #{field_name}
65
+ field_value: #{field_value}
66
+ dotted_path: #{dotted_path.inspect}
67
+ attribute_path: #{attribute_path.inspect}
68
+ attribute: #{attribute.inspect}
69
+ attribute_value: #{attribute_value.inspect}
70
+ metadata: #{metadata.inspect}
71
+ EOF
72
+ end
73
+
74
+ # return the attribute of the underlying entity corresponding
75
+ # to the column of this index
76
+ def attribute
77
+ model.attributes[column]
78
+ end
79
+
80
+ # fetch the value of the attribute, without following
81
+ # the full path. This will return a related entity for
82
+ # belongs_to or has_one relationships, or a plain value
83
+ # for model attributes
84
+ def attribute_value
85
+ entity.send( attribute )
86
+ end
87
+
88
+ # set the value of the attribute, without following the
89
+ # full path
90
+ def attribute_value=( obj )
91
+ entity.send( "#{attribute.to_s}=", obj )
92
+ end
93
+
94
+ # the dotted attribute path, same as a 'column' in the model
95
+ def dotted_path
96
+ model.dots[column]
97
+ end
98
+
99
+ # return an array of path elements from dotted_path
100
+ def attribute_path
101
+ return nil if model.nil?
102
+ model.attribute_paths[column]
103
+ end
104
+
105
+ # returns the ActiveRecord column_for_attribute
106
+ def metadata
107
+ # use the optimised version
108
+ model.metadata( column )
109
+ end
110
+
111
+ # return the table's field name. For associations, this would
112
+ # be suffixed with _id
113
+ def field_name
114
+ metadata.name
115
+ end
116
+
117
+ # return the value of the field, it the _id value
118
+ def field_value
119
+ entity.send( field_name )
120
+ end
121
+
122
+ # the underlying entity
123
+ def entity
124
+ return nil if model.nil?
125
+ #~ puts "fetching entity from collection for xy=(#{row},#{column})" if @entity.nil?
126
+ @entity ||= model.collection[row]
127
+ end
128
+
129
+ attr_writer :entity
130
+
131
+ end
132
+
133
+ end