clevic 0.6.0 → 0.7.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/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]