clevic 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,6 +17,7 @@ lib/clevic/db_options.rb
17
17
  lib/clevic/delegates.rb
18
18
  lib/clevic/extensions.rb
19
19
  lib/clevic/field.rb
20
+ lib/clevic/field_builder.rb
20
21
  lib/clevic/item_delegate.rb
21
22
  lib/clevic/model_builder.rb
22
23
  lib/clevic/model_column.rb
data/README.txt CHANGED
@@ -9,7 +9,8 @@ editing of tables in a pre-existing relational DBMS.
9
9
 
10
10
  Using ActiveRecord means Clevic supports Postgresql, Mysql and so on. It's been tested with Postgres and sqlite.
11
11
 
12
- Using Qt means it runs on Linux, Windows and OSX.
12
+ Using Qt means it runs on Linux, Windows and OSX. Thoroughly tested
13
+ in Linux, slightly tested in Windows and OSX.
13
14
 
14
15
  == FEATURES:
15
16
 
@@ -22,6 +23,7 @@ Using Qt means it runs on Linux, Windows and OSX.
22
23
  * Filter by current field.
23
24
  * search by field contents.
24
25
  * cut and paste in CSV format
26
+ * point Clevic at a Rails project and see your models in a GUI
25
27
 
26
28
  === Shortcuts:
27
29
 
@@ -37,9 +39,11 @@ Using Qt means it runs on Linux, Windows and OSX.
37
39
 
38
40
  Models and their UI representation must be defined in Ruby. A class that
39
41
  inherits from Clevic::Record (which itself inherits from ActiveRecord::Base) will provide
40
- a minimally functional UI.
41
- Beyond that, the framework provides
42
- an easy Rails-migrations-like syntax for defining more complex and useful behaviour.
42
+ a minimally functional UI. Beyond that, the framework provides
43
+ a DSL for defining more complex and useful behaviour (see Clevic::ModelBuilder).
44
+
45
+ Clevic also knows how to build a default UI with sensible defaults from
46
+ ActiveRecord::Base subclasses.
43
47
 
44
48
  In the models/ subdirectory, start with minimal_models.rb.
45
49
  account_models.rb and times_models.rb provide definitions for more real-world examples.
@@ -57,18 +61,17 @@ comments, see Clevic::Browser and Clevic::ModelBuilder.
57
61
  * leverages SQL whenever possible to handle large datasets, sorting, filtering
58
62
  etc. So it's probably not suitable for talking to a remote db across a slow link.
59
63
 
60
- === Plans
61
-
62
- * sortable by row headers
63
- * color highlighting of fields and records on definable criteria
64
-
65
64
  == PROBLEMS:
66
65
 
67
66
  See TODO file.
68
67
 
69
68
  == SYNOPSIS:
70
69
 
71
- clevic model_definition_file
70
+ clevic model_definition_file.rb
71
+
72
+ OR
73
+
74
+ clevic path_to_rails_project [model_tweaks.rb]
72
75
 
73
76
  == REQUIREMENTS:
74
77
 
data/Rakefile CHANGED
@@ -72,6 +72,7 @@ end
72
72
  desc "irb in this project's context"
73
73
  task :irb do |t|
74
74
  ARGV.shift()
75
+ ENV['RUBYLIB'] ||= ''
75
76
  ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
76
77
  exec "irb -Ilib -rclevic"
77
78
  end
@@ -124,8 +125,8 @@ Rake::RDocTask.new(:docs) do |rd|
124
125
  rd.options << "-t #{title}"
125
126
  end
126
127
 
127
- desc "Update History.txt from the SVN log"
128
- task :history do |t|
128
+ desc "Update ChangeLog from the SVN log"
129
+ task :changelog do |t|
129
130
  ARGV.shift
130
- exec "svn2cl --break-before-msg -o History.txt #{ARGV.join(' ')}"
131
+ exec "svn2cl --break-before-msg -o ChangeLog #{ARGV.join(' ')}"
131
132
  end
data/TODO CHANGED
@@ -1,31 +1,32 @@
1
- Make sure shortcuts don't edit a read-only field
1
+ rename model_class to entity_class, and related terminology clarification.
2
+ tests
3
+ allow directories for a setup. Problem here is tab order.
4
+ metadata for virtual fields
5
+ exception when ditto-left/right to a field with incompatible type
2
6
  generate models - DrySQL
7
+ allow moving of tabs
8
+ habtm relations
9
+ has_many :through
10
+ composed_of & aggregates
11
+ generate schema from definition? See rubyforge
3
12
  Ctrl-PgDn to last row in this column. Also extend selection
4
13
  sorting by header. See void QAbstractItemModel::sort ( int column, Qt::SortOrder order = Qt::AscendingOrder )
5
14
  - layoutChanged
6
15
 
7
- search with acts_as_searchable and hyperestraier
8
-
9
16
  undo
10
17
  - acts_as_trashable to undo deletes
11
18
  - other commands. Possibly via ActiveRecord callbacks?
12
19
  - Keep a history of changes, ie xy, new. xy, changed. x,y copied etc.
20
+ - Use Transaction::Simple?
21
+ - undo of field changes
13
22
 
14
23
  Using F4 to open list, and then selecting from the combo and exiting using Return (or tab?) doesn't set the correct value
15
24
  wrap description, and allow Access-style zooming
16
-
17
- search in relational fields doesn't work because the search value is compared to the id of the related record
18
- Use Webrick as a way to export CSV reports?
19
- See Ruport and Documatic for reports
20
-
21
- generate models - DrySQL
22
25
  Ctrl-PgDn to last row in this column. Also extend selection
23
26
  sorting by header. See void QAbstractItemModel::sort ( int column, Qt::SortOrder order = Qt::AscendingOrder )
24
27
  - layoutChanged
25
28
 
26
29
  search with acts_as_searchable and hyperestraier
27
- acts_as_trashable to undo deletes
28
- implement undo of field changes
29
30
 
30
31
  moving of columns
31
32
  /-style keyboard search by selected column, or everything if no column selected
@@ -41,7 +42,7 @@ filtering by various things. http://doc.trolltech.com/4.3/qsortfilterproxymodel.
41
42
  highlighting by various things
42
43
  cut and paste (in model format)
43
44
 
44
- drop cached model objects from CacheTable when they're not in use
45
+ drop cached model objects from CacheTable when they're not in use. WeakRef?
45
46
 
46
47
  allow scroll viewport to centre when at end of dataset
47
48
  QAbstractItemView::ScrollHint
@@ -51,6 +52,12 @@ QAbstractItemView::ScrollHint
51
52
  \value PositionAtBottom Scroll to position the item at the bottom of the viewport.
52
53
  \value PositionAtCenter Scroll to position the item at the center of the viewport.
53
54
 
55
+ Reports
56
+ -------
57
+ Use Webrick as a way to export CSV reports?
58
+ See Ruport and Documatic for reports
59
+ Ruby Datavision Bridge for reports
60
+
54
61
  OSX
55
62
  ---
56
63
  Check that qtruby4 runs on OSX and so does Clevic. It does. Very slowly on Mac Mini with Motorola.
@@ -83,7 +90,7 @@ shortcut sets, depending on which OS you're used. use QKeyEvent::matches ( QKeyS
83
90
 
84
91
  Doing data capture, sort by id, but unfilter reverts to date/id rather than entry order
85
92
  optional warnings for back-dated entries. Highlighting
86
- make sure bsearch easier to install
93
+ make bsearch easier to install
87
94
 
88
95
  handle db errors
89
96
  easier way to run models, search LOAD_PATH
@@ -97,8 +104,11 @@ store previous searches, by model & app
97
104
 
98
105
  maybe
99
106
  -----
107
+ acts_as_shellable looks nices
108
+ consolidate read-only-ness checks
100
109
  Look at DataMapper. Not suitable - need to declare properties.
101
110
  use rubigen for creating model definition files
111
+ ActiveMDB for migrating.
102
112
  allow moving of rows
103
113
  discontiguous copying of entities/csv
104
114
  multi-row copying
@@ -106,6 +116,11 @@ pasting of csv, into rectangular regions
106
116
  collect a set of data requests to the model, and do them in one SQL query. See EntryTableView#moveCursor
107
117
  Use SQL cursors for find & find_next?
108
118
  Use roo to parse spreadsheets?
119
+ Criteria for SQL statement building http://criteria.rubyforge.org/
120
+ SqlCache
121
+ SqlStatement
122
+ Csv2Sql
123
+ QueryBuilder
109
124
 
110
125
  Accounts
111
126
  --------
data/bin/clevic CHANGED
@@ -1,13 +1,12 @@
1
1
  #! /usr/bin/ruby
2
2
 
3
+ require 'pathname'
4
+
3
5
  require 'clevic/browser.rb'
6
+ require 'clevic/db_options.rb'
4
7
  require 'optparse'
8
+ require 'active_support'
5
9
 
6
- # find and require variations on file_path
7
- def require_if( file_path )
8
- require file_path if File.exist?( file_path ) || File.exist?( file_path + '.rb' )
9
- end
10
-
11
10
  $options = {}
12
11
  oparser = OptionParser.new
13
12
  oparser.banner = <<BANNER
@@ -22,6 +21,7 @@ oparser.separator ''
22
21
  oparser.on( '-H', '--host HOST', 'RDBMS host', String ) { |o| $options[:host] = o }
23
22
  oparser.on( '-u', '--user USERNAME', String ) { |o| $options[:username] = o }
24
23
  oparser.on( '-p', '--pass PASSWORD', String ) { |o| $options[:password] = o }
24
+ oparser.on( '-P', '--profile PROFILE', String ) { |o| $options[:profile] = 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 }
27
27
  oparser.on( '-D', '--debug' ) { |o| $options[:debug] = true }
@@ -38,14 +38,67 @@ if $options[:debug]
38
38
  pp $options
39
39
  end
40
40
 
41
- if args.size > 0
42
- $options[:definition] = args.shift
43
- require_if "#{$options[:definition]}_models"
44
- require_if $options[:definition]
45
- else
46
- raise "no model definition file specified"
41
+ class Pathname
42
+ # require this pathname if it exists
43
+ def require_if_exists
44
+ require realpath.to_s if exist?
45
+ end
47
46
  end
48
47
 
48
+ def load_rails_models( root, config, models )
49
+ # initialize Rails
50
+ load config + 'environment.rb'
51
+ #~ puts "RAILS_ROOT: #{RAILS_ROOT.inspect}"
52
+ require 'initializer.rb'
53
+ Rails::Initializer.run do |config|
54
+ config.frameworks -= [ :action_mailer, :action_pack, :active_resource ]
55
+ end
56
+
57
+ # load lib/ files
58
+ $: << ( root + 'lib' ).realpath.to_s
59
+ ( root + 'lib' ).children.each do |filename|
60
+ puts "filename: #{filename.inspect}"
61
+ load filename if filename.file?
62
+ end
63
+
64
+ # TODO check for Dirty in 2.1.x
65
+ ActiveRecord::Base.send(:include, ActiveRecord::Dirty)
66
+
67
+ # load models
68
+ models.children.each_with_index do |filename,i|
69
+ #~ %w{subscriber}.map{|x| models + "#{x}.rb"}.each_with_index do |filename,i|
70
+ begin
71
+ load filename if filename.file?
72
+ rescue Exception => e
73
+ puts "Error loading #{filename.basename.to_s}: #{e.message.inspect}"
74
+ puts e.backtrace
75
+ end
76
+ end
77
+ end
78
+
79
+ def load_models( pathname )
80
+ if pathname.directory?
81
+ config = pathname + 'config'
82
+ app = pathname + 'app'
83
+ models = app + 'models'
84
+ # check if this is a Rails directory
85
+ if config.exist? && app.exist? && models.exist?
86
+ # this is probably a Rails project"
87
+ load_rails_models( pathname, config, models )
88
+ end
89
+ else
90
+ # assume we have a single file, and try some variations
91
+ ( pathname + '.rb' ).require_if_exists
92
+ pathname.require_if_exists
93
+ ( pathname + '_models' ).require_if_exists
94
+ ( pathname + '_models.rb' ).require_if_exists
95
+ end
96
+ end
97
+
98
+ # load model files
99
+ raise "no model definition file specified" if args.empty?
100
+ args.each { |arg| load_models( Pathname.new( arg ) ) }
101
+
49
102
  app = Qt::Application.new( args )
50
103
 
51
104
  # show UI
@@ -8,7 +8,7 @@ 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.4.1'],
11
+ ['qtext', '>=0.5.0'],
12
12
  ['activerecord', '=2.0.2'],
13
13
  ['fastercsv', '>=1.2.3']
14
14
  # This isn't always installed from gems
@@ -1,22 +1,13 @@
1
1
  require 'clevic/search_dialog.rb'
2
2
  require 'clevic/ui/browser_ui.rb'
3
+ require 'clevic/record.rb'
4
+ require 'clevic.rb'
3
5
 
4
6
  module Clevic
5
7
 
6
8
  =begin rdoc
7
- The main application class. Each model for display should have a self.ui method
8
- which returns a Clevic::TableView instance, usually in conjunction with
9
- a ModelBuilder.
10
-
11
- Clevic::TableView.new( Entry, parent ).create_model.new( Entry, parent ).create_model
12
- .
13
- .
14
- .
15
- end
16
-
17
- Model instances may also implement <tt>self.key_press_event( event, current_index, view )</tt>
18
- and <tt>self.data_changed( top_left_index, bottom_right_index, view )</tt> methods so that
19
- they can respond to editing events and do Neat Stuff.
9
+ The main application class. Display as many tabs as there are Clevic::Record or ActiveRecord::Base
10
+ subclasses.
20
11
  =end
21
12
  class Browser < Qt::Widget
22
13
  slots *%w{dump() refresh_table() filter_by_current(bool) next_tab() previous_tab() current_changed(int)}
@@ -38,26 +29,48 @@ class Browser < Qt::Widget
38
29
  @layout.main_widget.layout.add_widget @tables_tab
39
30
  @tables_tab.tab_bar.focus_policy = Qt::NoFocus
40
31
 
41
- # connect slots
42
- @layout.action_dump.connect SIGNAL( 'triggered()' ), &method( :dump )
43
- @layout.action_refresh.connect SIGNAL( 'triggered()' ), &method( :refresh_table )
44
- @layout.action_filter.connect SIGNAL( 'triggered(bool)' ), &method( :filter_by_current )
32
+ # hide the file menu, for now
33
+ @layout.menubar.remove_action( @layout.menu_file.menu_action )
34
+
35
+ # tab navigation
45
36
  @layout.action_next.connect SIGNAL( 'triggered()' ), &method( :next_tab )
46
37
  @layout.action_previous.connect SIGNAL( 'triggered()' ), &method( :previous_tab )
47
- @layout.action_find.connect SIGNAL( 'triggered()' ), &method( :find )
48
- @layout.action_find_next.connect SIGNAL( 'triggered()' ), &method( :find_next )
49
- @layout.action_new_row.connect SIGNAL( 'triggered()' ), &method( :new_row )
38
+
39
+ # dump model for current tab
40
+ @layout.action_dump.visible = $options[:debug]
41
+ @layout.action_dump.connect SIGNAL( 'triggered()' ), &method( :dump )
50
42
 
51
43
  tables_tab.connect SIGNAL( 'currentChanged(int)' ), &method( :current_changed )
52
44
 
53
- # as an example
54
- #~ tables_tab.connect SIGNAL( 'currentChanged(int)' ) { |index| puts "other current_changed: #{index}" }
55
45
  load_models
46
+ update_menus
56
47
  end
57
48
 
58
- # activated by Ctrl-D for debugging
49
+ def update_menus
50
+ # update edit menu
51
+ @layout.menu_edit.clear
52
+
53
+ # do the model-specific menu items first
54
+ table_view.model_actions.each do |action|
55
+ @layout.menu_edit.add_action( action )
56
+ end
57
+
58
+ # now do the generic edit items
59
+ table_view.edit_actions.each do |action|
60
+ @layout.menu_edit.add_action( action )
61
+ end
62
+
63
+ # update search menu
64
+ @layout.menu_search.clear
65
+ table_view.search_actions.each do |action|
66
+ @layout.menu_search.add_action( action )
67
+ end
68
+ end
69
+
70
+ # activated by Ctrl-Shift-D for debugging
59
71
  def dump
60
- puts "table_view.model: #{table_view.model.inspect}" if table_view.class == Clevic::TableView
72
+ puts "table_view.model: #{table_view.model.inspect}"
73
+ puts "table_view.model.model_class: #{table_view.model.model_class.inspect}"
61
74
  end
62
75
 
63
76
  # return the Clevic::TableView object in the currently displayed tab
@@ -69,57 +82,6 @@ class Browser < Qt::Widget
69
82
  @tables_tab
70
83
  end
71
84
 
72
- # display a search dialog, and find the entered text
73
- def find
74
- @search_dialog ||= SearchDialog.new
75
- result = @search_dialog.exec( table_view.current_index.gui_value )
76
-
77
- override_cursor( Qt::BusyCursor ) do
78
- case result
79
- when Qt::Dialog::Accepted
80
- search_for = @search_dialog.search_text
81
- table_view.search( @search_dialog )
82
- when Qt::Dialog::Rejected
83
- puts "Don't search"
84
- else
85
- puts "unknown dialog code #{result}"
86
- end
87
- end
88
- end
89
-
90
- def find_next
91
- if @search_dialog.nil?
92
- @layout.statusbar.show_message( 'No previous find' )
93
- else
94
- override_cursor( Qt::BusyCursor ) do
95
- save_from_start = @search_dialog.from_start?
96
- @search_dialog.from_start = false
97
- table_view.search( @search_dialog )
98
- @search_dialog.from_start = save_from_start
99
- end
100
- end
101
- end
102
-
103
- # force a complete reload of the current tab's data
104
- def refresh_table
105
- override_cursor( Qt::BusyCursor ) do
106
- table_view.model.reload_data
107
- end
108
- end
109
-
110
- # toggle the filter, based on current selection.
111
- def filter_by_current( bool_filter )
112
- # TODO if there's no selection, use the current index instead
113
- table_view.filter_by_indexes( table_view.selection_model.selected_indexes )
114
-
115
- # set the checkbox in the menu item
116
- @layout.action_filter.checked = table_view.filtered
117
-
118
- # update the tab, so there's a visual indication of filtering
119
- tab_title = table_view.filtered ? translate( '| ' + table_view.model_class.name.humanize ) : translate( table_view.model_class.name.humanize )
120
- tables_tab.set_tab_text( tables_tab.current_index, tab_title )
121
- end
122
-
123
85
  # slot to handle Ctrl-Tab and move to next tab, or wrap around
124
86
  def next_tab
125
87
  tables_tab.current_index =
@@ -140,15 +102,11 @@ class Browser < Qt::Widget
140
102
  end
141
103
  end
142
104
 
143
- def new_row
144
- table_view.model.add_new_item
145
- end
146
-
147
105
  # slot to handle the currentChanged signal from tables_tab, and
148
106
  # set focus on the grid
149
107
  def current_changed( current_tab_index )
150
- tables_tab.current_widget.setFocus
151
- @layout.action_filter.checked = table_view.filtered
108
+ update_menus
109
+ tables_tab.current_widget.set_focus
152
110
  end
153
111
 
154
112
  # shortcut for the Qt translate call
@@ -156,37 +114,20 @@ class Browser < Qt::Widget
156
114
  Qt::Application.translate("Browser", st, nil, Qt::Application::UnicodeUTF8)
157
115
  end
158
116
 
159
- # return the list of descendants of ActiveRecord::Base
117
+ # return the list of descendants of ActiveRecord::Base, or
118
+ # of Clevic::Record
160
119
  def find_models
161
120
  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
121
+ ObjectSpace.each_object( Class ) do |x|
122
+ if x.ancestors.include?( ActiveRecord::Base )
123
+ case
124
+ when x == ActiveRecord::Base; # don't include this
125
+ when x == Clevic::Record; # don't include this
126
+ else; models << x
186
127
  end
187
128
  end
188
- records :order => 'id'
189
129
  end
130
+ models.sort{|a,b| a.name <=> b.name}
190
131
  end
191
132
 
192
133
  # Create the tabs, each with a collection for a particular model class.
@@ -194,18 +135,47 @@ class Browser < Qt::Widget
194
135
  # models parameter can be an array of Model objects, in order of display.
195
136
  # if models is nil, find_models is called
196
137
  def load_models
197
- models = Clevic::Record.models || find_models
138
+ models = Clevic::Record.models
139
+ models = find_models if models.empty?
198
140
 
199
141
  # Add all existing model objects as tabs, one each
200
- models.each do |model|
201
- tab =
202
- if model.respond_to?( :ui )
203
- model.ui( tables_tab )
204
- else
205
- define_default_ui( model )
142
+ models.each do |model_class|
143
+ begin
144
+ next unless model_class.table_exists?
145
+
146
+ # create the the table_view and the table_model for the model_class
147
+ tab =
148
+ if model_class.respond_to?( :ui )
149
+ puts "Entity#ui deprecated. Use build_table_model instead."
150
+ model_class.ui( tables_tab )
151
+ else
152
+ Clevic::TableView.new( model_class, tables_tab )
153
+ end
154
+
155
+ # show status messages
156
+ tab.connect( SIGNAL( 'status_text(QString)' ) ) { |msg| @layout.statusbar.show_message( msg, 10000 ) }
157
+
158
+ # add a new tab
159
+ tables_tab.add_tab( tab, translate( model_class.name.demodulize.tableize.humanize ) )
160
+
161
+ # add the table to the Table menu
162
+ action = Qt::Action.new( @layout.menu_model )
163
+ action.text = translate( model_class.name.demodulize.tableize.humanize )
164
+ action.connect SIGNAL( 'triggered()' ) do
165
+ tables_tab.current_widget = tab
166
+ end
167
+ @layout.menu_model.add_action( action )
168
+
169
+ # handle filter status changed, so we can provide a visual indication
170
+ tab.connect SIGNAL( 'filter_status(bool)' ) do |status|
171
+ # update the tab, so there's a visual indication of filtering
172
+ tab_title = ( tab.filtered ? '| ' : '' ) + translate( model_class.name.humanize )
173
+ tables_tab.set_tab_text( tables_tab.current_index, tab_title )
174
+ end
175
+ rescue Exception => e
176
+ puts e.backtrace if $options[:debug]
177
+ puts "Model #{model_class} will not be available: #{e.message}"
206
178
  end
207
- tab.connect( SIGNAL( 'status_text(QString)' ) ) { |msg| @layout.statusbar.show_message( msg, 20000 ) }
208
- tables_tab.add_tab( tab, translate( model.name.humanize ) )
209
179
  end
210
180
  end
211
181