clevic 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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