clevic 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/clevic CHANGED
@@ -20,7 +20,7 @@ BANNER
20
20
  oparser.separator ''
21
21
 
22
22
  oparser.on( '-H', '--host HOST', 'RDBMS host', String ) { |o| $options[:host] = o }
23
- oparser.on( '-u', '--user USERNAME', String ) { |o| $options[:user] = o }
23
+ oparser.on( '-u', '--user USERNAME', String ) { |o| $options[:username] = o }
24
24
  oparser.on( '-p', '--pass PASSWORD', String ) { |o| $options[:password] = o }
25
25
  oparser.on( '-t', '--table TABLE', 'Table to display', String ) { |o| $options[:table] = o }
26
26
  oparser.on( '-d', '--database DATABASE', 'Database name', String ) { |o| $options[:database] = o }
@@ -33,6 +33,11 @@ end
33
33
 
34
34
  args = oparser.parse( ARGV )
35
35
 
36
+ if $options[:debug]
37
+ require 'pp'
38
+ pp $options
39
+ end
40
+
36
41
  if args.size > 0
37
42
  $options[:definition] = args.shift
38
43
  require_if "#{$options[:definition]}_models"
@@ -46,8 +51,8 @@ app = Qt::Application.new( args )
46
51
  # show UI
47
52
  main_window = Qt::MainWindow.new
48
53
  browser = Clevic::Browser.new( main_window )
49
- browser.open
50
54
  # this must come after Clevic::Browser.new
55
+ # TODO should really find a better place for this
51
56
  main_window.window_title = $options[:database]
52
57
  main_window.show
53
58
  # make sure any partially edited records are saved when the window is closed
data/config/hoe.rb CHANGED
@@ -8,10 +8,12 @@ RUBYFORGE_PROJECT = 'clevic' # The unix name for your project
8
8
  HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
9
  DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
10
  EXTRA_DEPENDENCIES = [
11
- ['qtext', '>=0.2.0'],
12
- ['activerecord', '>=2.0.2']
11
+ ['qtext', '>=0.4.1'],
12
+ ['activerecord', '=2.0.2'],
13
+ ['fastercsv', '>=1.2.3']
13
14
  # This isn't always installed from gems
14
15
  #~ ['qtruby4', '>=1.4.9']
16
+ # bsearch can't be installed from gems
15
17
  ] # An array of rubygem dependencies [name, version]
16
18
 
17
19
  @config_file = "~/.rubyforge/user-config.yml"
data/lib/clevic.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'clevic/db_options.rb'
2
2
  require 'clevic/table_view.rb'
3
3
  require 'clevic/model_builder.rb'
4
+ require 'clevic/record.rb'
4
5
 
5
6
 
@@ -52,6 +52,7 @@ class Browser < Qt::Widget
52
52
 
53
53
  # as an example
54
54
  #~ tables_tab.connect SIGNAL( 'currentChanged(int)' ) { |index| puts "other current_changed: #{index}" }
55
+ load_models
55
56
  end
56
57
 
57
58
  # activated by Ctrl-D for debugging
@@ -155,15 +156,36 @@ class Browser < Qt::Widget
155
156
  Qt::Application.translate("Browser", st, nil, Qt::Application::UnicodeUTF8)
156
157
  end
157
158
 
158
- # return the list of models in $options[:models] or find them
159
- # as descendants of ActiveRecord::Base
160
- def find_models( models = $options[:models] )
161
- if models.nil? || models.empty?
162
- models = []
163
- ObjectSpace.each_object( Class ) {|x| models << x if x.superclass == ActiveRecord::Base }
164
- models
165
- else
166
- models
159
+ # return the list of descendants of ActiveRecord::Base
160
+ def find_models
161
+ models = []
162
+ ObjectSpace.each_object( Class ) {|x| models << x if x.ancestors.include?( Clevic::Record ) }
163
+ models
164
+ end
165
+
166
+ # define a default ui with plain fields for all
167
+ # columns (except id) in the model. Could combine this with
168
+ # DrySQL to automate the process.
169
+ def define_default_ui( model )
170
+ reflections = model.reflections.keys.map{|x| x.to_s}
171
+ ui_columns = model.columns.reject{|x| x.name == 'id' }.map do |x|
172
+ att = x.name.gsub( '_id', '' )
173
+ if reflections.include?( att )
174
+ att
175
+ else
176
+ x.name
177
+ end
178
+ end
179
+
180
+ Clevic::TableView.new( model, tables_tab ).create_model do
181
+ ui_columns.each do |column|
182
+ if model.reflections.has_key?( column.to_sym )
183
+ relational column.to_sym
184
+ else
185
+ plain column.to_sym
186
+ end
187
+ end
188
+ records :order => 'id'
167
189
  end
168
190
  end
169
191
 
@@ -171,17 +193,18 @@ class Browser < Qt::Widget
171
193
  #
172
194
  # models parameter can be an array of Model objects, in order of display.
173
195
  # if models is nil, find_models is called
174
- def open( *models )
175
- models = $options[:models] if models.empty?
196
+ def load_models
197
+ models = Clevic::Record.models || find_models
176
198
 
177
199
  # Add all existing model objects as tabs, one each
178
- find_models( models ).each do |model|
200
+ models.each do |model|
201
+ tab =
179
202
  if model.respond_to?( :ui )
180
- tab = model.ui( tables_tab )
181
- tab.connect( SIGNAL( 'status_text(QString)' ) ) { |msg| @layout.statusbar.show_message( msg, 20000 ) }
203
+ model.ui( tables_tab )
182
204
  else
183
- raise "Can't build ui for #{model.name}. Provide a self.ui method."
205
+ define_default_ui( model )
184
206
  end
207
+ tab.connect( SIGNAL( 'status_text(QString)' ) ) { |msg| @layout.statusbar.show_message( msg, 20000 ) }
185
208
  tables_tab.add_tab( tab, translate( model.name.humanize ) )
186
209
  end
187
210
  end
@@ -3,8 +3,6 @@ require 'active_record'
3
3
  require 'active_record/dirty.rb'
4
4
  require 'bsearch'
5
5
 
6
- require 'profiler'
7
-
8
6
  =begin rdoc
9
7
  Store the SQL order_by attributes with ascending and descending values
10
8
  =end
@@ -73,7 +71,7 @@ for each call?
73
71
  class CacheTable < Array
74
72
  # the number of records loaded in one call to the db
75
73
  attr_accessor :preload_count
76
- attr_reader :options
74
+ attr_reader :options, :model_class
77
75
 
78
76
  def initialize( model_class, find_options = {} )
79
77
  @preload_count = 20
@@ -106,13 +104,25 @@ class CacheTable < Array
106
104
  @model_class.connection.quote_column_name( field_name )
107
105
  end
108
106
 
107
+ # return a string containing the correct
108
+ # boolean value depending on the DB adapter
109
+ # because Postgres wants real true and false in complex statements, not 't' and 'f'
110
+ def sql_boolean( value )
111
+ case model_class.connection.adapter_name
112
+ when 'PostgreSQL'
113
+ value ? 'true' : 'false'
114
+ else
115
+ value ? model_class.connection.quoted_true : model_class.connection.quoted_false
116
+ end
117
+ end
118
+
109
119
  # recursively create a case statement to do the comparison
110
120
  # because and ... and ... and filters on *each* one rather than
111
121
  # consecutively.
112
122
  # operator is either '<' or '>'
113
123
  def build_recursive_comparison( operator, index = 0 )
114
124
  # end recursion
115
- return 'false' if index == @order_attributes.size
125
+ return sql_boolean( false ) if index == @order_attributes.size
116
126
 
117
127
  # fetch the current attribute
118
128
  attribute = @order_attributes[index]
@@ -120,11 +130,12 @@ class CacheTable < Array
120
130
  # build case statement, including recusion
121
131
  st = <<-EOF
122
132
  case
123
- when #{quote_column attribute} #{operator} :#{attribute} then true
133
+ when #{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
124
134
  when #{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
125
- else false
135
+ else #{sql_boolean false}
126
136
  end
127
137
  EOF
138
+ st.gsub!( /^/, ' ' * index )
128
139
  end
129
140
 
130
141
  # return a Hash containing
@@ -143,6 +154,13 @@ EOF
143
154
  # build the sql comparison where clause fragment
144
155
  sql = build_recursive_comparison( operator )
145
156
 
157
+ # only Postgres seems to understand real booleans
158
+ # everything else needs the big case statement to be compared
159
+ # to something
160
+ unless model_class.connection.adapter_name == 'PostgreSQL'
161
+ sql += " = #{sql_boolean true}"
162
+ end
163
+
146
164
  # build parameter values
147
165
  params = {}
148
166
  @order_attributes.each {|x| params[x.to_sym] = entity.send( x.attribute )}
@@ -234,10 +252,8 @@ EOF
234
252
 
235
253
  # only load one record at a time
236
254
  preload_limit( 1 ) do
237
- #~ puts "entity: #{entity.inspect}"
238
255
  # do the binary search based on what we know about the search order
239
256
  bsearch do |candidate|
240
- #~ puts "candidate: #{candidate.inspect}"
241
257
  # find using all sort attributes
242
258
  order_attributes.inject(0) do |result,attribute|
243
259
  if result == 0
@@ -8,7 +8,7 @@ connection to a particular database. Like this:
8
8
  Clevic::DbOptions.connect( $options ) do
9
9
  database :accounts
10
10
  adapter :postgresql
11
- username 'panic'
11
+ username 'accounts_user'
12
12
  end
13
13
 
14
14
  When the block ends, a check is done to see that the :database key
@@ -21,7 +21,9 @@ 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
22
  =end
23
23
  class DbOptions
24
- def initialize( options )
24
+ attr_reader :options
25
+
26
+ def initialize( options = nil )
25
27
  @options = options || {}
26
28
  # make sure the relevant entries exist, so method_missing works
27
29
  @options[:adapter] ||= ''
@@ -44,21 +46,22 @@ class DbOptions
44
46
  end
45
47
 
46
48
  # connect to db
47
- ActiveRecord::Base.establish_connection( @options )
48
- ActiveRecord::Base.logger = Logger.new(STDOUT) if @options[:verbose]
49
+ ActiveRecord::Base.establish_connection( options )
50
+ ActiveRecord::Base.logger = Logger.new(STDOUT) if options[:verbose]
49
51
  #~ ActiveRecord.colorize_logging = @options[:verbose]
50
- puts "using database #{ActiveRecord::Base.connection.raw_connection.db}" if $options[:debug]
52
+ puts "using database #{ActiveRecord::Base.connection.raw_connection.db}" if options[:debug]
53
+ self
51
54
  end
52
55
 
53
56
  # convenience method so we can do things like
54
57
  # Clevic::DbOptions.connect( $options ) do
55
58
  # database :accounts
56
59
  # adapter :postgresql
57
- # username 'panic'
60
+ # username 'accounts_user'
58
61
  # end
59
62
  # the block is evaluated in the context of the a new DbOptions
60
63
  # object.
61
- def self.connect( args, &block )
64
+ def self.connect( args = nil, &block )
62
65
  inst = self.new( args )
63
66
  # using the Rails implementation, included in Qt
64
67
  block.bind( inst )[*args]
@@ -70,7 +73,7 @@ class DbOptions
70
73
  # variable
71
74
  def method_missing(meth, *args, &block)
72
75
  if @options.has_key? meth.to_sym
73
- @options[meth.to_sym] = args[0].to_s
76
+ @options[meth.to_sym] = args[0].to_s if @options[meth.to_sym].empty?
74
77
  else
75
78
  super
76
79
  end
@@ -219,7 +219,7 @@ class DistinctDelegate < ComboDelegate
219
219
  @attribute = attribute
220
220
  @options = options
221
221
  # hackery for amateur query building in populate
222
- @options[:conditions] ||= 'true'
222
+ @options[:conditions] ||= '1=1'
223
223
  super( parent )
224
224
  end
225
225
 
@@ -259,10 +259,13 @@ class DistinctDelegate < ComboDelegate
259
259
  # to be in the select list where distinct is involved
260
260
  conn = @ar_model.connection
261
261
  query =
262
- if @options[:frequency]
263
- query_order_frequency( conn, model_index )
264
- else
265
- query_order_description( conn, model_index )
262
+ case
263
+ when @options[:description]
264
+ query_order_description( conn, model_index )
265
+ when @options[:frequency]
266
+ query_order_frequency( conn, model_index )
267
+ else
268
+ query_order_frequency( conn, model_index )
266
269
  end
267
270
  rs = conn.execute( query )
268
271
  rs.each do |row|
@@ -304,17 +307,21 @@ end
304
307
 
305
308
  # Edit a relation from an id and display a list of relevant entries.
306
309
  #
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.
310
+ # attribute is the method to call on the row entity to retrieve the related object.
309
311
  #
310
312
  # The ids of the ActiveRecord models are stored in the item data
311
313
  # and the item text is fetched from them using attribute_path.
312
314
  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('.')
315
+
316
+ def initialize( parent, attribute, options )
317
+ @model_class = ( options[:class_name] || attribute.to_s.classify ).constantize
318
+ # TODO this doesn't seem to be used
319
+ @attribute = attribute.to_s
317
320
  @options = options.clone
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 )
324
+ end
318
325
  [ :class_name, :sample, :format ].each {|x| @options.delete x }
319
326
  super( parent )
320
327
  end
@@ -338,7 +345,7 @@ class RelationalDelegate < ComboDelegate
338
345
  if item
339
346
  item_index = editor.find_data( item.id.to_variant )
340
347
  if item_index == -1
341
- editor.add_item( item[@attribute_path], item.id.to_variant )
348
+ add_to_list( editor, model_index, item )
342
349
  end
343
350
  end
344
351
  end
@@ -347,10 +354,14 @@ class RelationalDelegate < ComboDelegate
347
354
  def populate( editor, model_index )
348
355
  # add set of all possible related entities
349
356
  @model_class.find( :all, collect_finder_options( @options ) ).each do |x|
350
- editor.add_item( x[@attribute_path], x.id.to_variant )
357
+ add_to_list( editor, model_index, x )
351
358
  end
352
359
  end
353
360
 
361
+ def add_to_list( editor, model_index, item )
362
+ editor.add_item( model_index.field.transform_attribute( item ), item.id.to_variant )
363
+ end
364
+
354
365
  # send data to the editor
355
366
  def setEditorData( editor, model_index )
356
367
  if is_combo?( editor )
@@ -48,8 +48,12 @@ module Qt
48
48
  class ModelIndex
49
49
  # the value to be displayed in the gui for this index
50
50
  def gui_value
51
- return nil if entity.nil?
52
- entity.evaluate_path( attribute_path )
51
+ field.value_for( entity )
52
+ end
53
+
54
+ # return the Clevic::Field for this index
55
+ def field
56
+ @field ||= model.field_for_index( self )
53
57
  end
54
58
 
55
59
  # set the value returned from the gui, as whatever the underlying
data/lib/clevic/field.rb CHANGED
@@ -8,13 +8,14 @@ This defines a field in the UI, and how it hooks up to a field in the DB.
8
8
  class Field
9
9
  include QtFlags
10
10
 
11
- attr_accessor :attribute, :path, :label, :delegate, :class_name, :alignment, :format, :tooltip
12
- attr_writer :sample
11
+ attr_accessor :attribute, :path, :label, :delegate, :class_name
12
+ attr_accessor :alignment, :format, :tooltip, :path_block
13
+ attr_writer :sample, :read_only
13
14
 
14
15
  # attribute is the symbol for the attribute on the model_class
15
16
  def initialize( attribute, model_class, options )
16
17
  # sanity checking
17
- unless model_class.has_attribute?( attribute )
18
+ unless model_class.has_attribute?( attribute ) or model_class.instance_methods.include?( attribute.to_s )
18
19
  msg = <<EOF
19
20
  #{attribute} not found in #{model_class.name}. Possibilities are:
20
21
  #{model_class.attribute_names.join("\n")}
@@ -27,7 +28,14 @@ EOF
27
28
  @model_class = model_class
28
29
 
29
30
  options.each do |key,value|
30
- self.send( "#{key}=", value ) if respond_to?( key )
31
+ self.send( "#{key}=", value ) if respond_to?( "#{key}=" )
32
+ end
33
+
34
+ # TODO could convert everything to a block here, even paths
35
+ if options[:display].kind_of?( Proc )
36
+ @path_block = options[:display]
37
+ else
38
+ @path = options[:display]
31
39
  end
32
40
 
33
41
  # default the label
@@ -53,6 +61,31 @@ EOF
53
61
  end
54
62
  end
55
63
 
64
+ # Return the attribute value for the given entity, which may
65
+ # be an ActiveRecord instance
66
+ # entity is an ActiveRecord instance
67
+ def value_for( entity )
68
+ return nil if entity.nil?
69
+ transform_attribute( entity.send( attribute ) )
70
+ end
71
+
72
+ # apply path, or path_block, to the given
73
+ # attribute value. Otherwise just return
74
+ # attribute_value itself
75
+ def transform_attribute( attribute_value )
76
+ return nil if attribute_value.nil?
77
+ case
78
+ when !path_block.nil?
79
+ path_block.call( attribute_value )
80
+
81
+ when !path.nil?
82
+ attribute_value.evaluate_path( path.split( /\./ ) )
83
+
84
+ else
85
+ attribute_value
86
+ end
87
+ end
88
+
56
89
  # return true if it's a date, a time or a datetime
57
90
  # cache result because the type won't change in the lifetime of the field
58
91
  def is_date_time?
@@ -65,11 +98,19 @@ EOF
65
98
  @model_class.columns_hash[attribute.to_s] || @model_class.reflections[attribute]
66
99
  end
67
100
 
101
+ # return true if this field can be used in a filter
102
+ # virtual fields (ie those that don't exist in this field's
103
+ # table) can't be filtered on.
104
+ def filterable?
105
+ !meta.nil?
106
+ end
107
+
68
108
  # return the name of the field for this Field, quoted for the dbms
69
109
  def quoted_field
70
110
  @model_class.connection.quote_column_name( meta.name )
71
111
  end
72
112
 
113
+ # return the result of the attribute + the path
73
114
  def column
74
115
  [attribute.to_s, path].compact.join('.')
75
116
  end
@@ -81,6 +122,11 @@ EOF
81
122
  pieces.map{|x| x.to_sym}
82
123
  end
83
124
 
125
+ # is the field read-only. Defaults to false.
126
+ def read_only?
127
+ @read_only || false
128
+ end
129
+
84
130
  # format this value. Use strftime for date_time types, or % for everything else
85
131
  def do_format( value )
86
132
  if self.format != nil
@@ -103,7 +149,7 @@ EOF
103
149
  when :string, :text
104
150
  string_sample( 'n'*40 )
105
151
 
106
- when :date, :time, :datetime
152
+ when :date, :time, :datetime, :timestamp
107
153
  date_time_sample
108
154
 
109
155
  when :numeric, :decimal, :integer, :float
@@ -112,8 +158,11 @@ EOF
112
158
  # TODO return a width, or something like that
113
159
  when :boolean; 'W'
114
160
 
161
+ when ActiveRecord::Reflection::AssociationReflection
162
+ #TODO width for relations
163
+
115
164
  else
116
- puts "#{@model_class.name}.#{attribute} is a #{meta.type}"
165
+ puts "#{@model_class.name}.#{attribute} is a #{meta.type.inspect}"
117
166
  end
118
167
 
119
168
  if $options[:debug]