clevic 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'rake/clean'
3
+ require 'hoe'
4
+ require 'lib/clevic/version.rb'
5
+ require 'pathname'
6
+
7
+ Hoe.new( 'clevic', Clevic::VERSION ) do |s|
8
+ s.author = "John Anderson"
9
+ s.email = "john at semiosix dot com"
10
+ end
11
+
12
+ # generate a _ui.rb filename from a .ui filename
13
+ def ui_rb_file( ui_file )
14
+ ui_file.gsub( /\.ui$/, '_ui.rb' )
15
+ end
16
+
17
+ # list of .ui files
18
+ UI_FILES = FileList.new( 'lib/clevic/ui/*.ui' )
19
+ CLEAN.include( 'ChangeLog', 'lib/clevic/ui/*.rb' )
20
+
21
+ UI_FILES.each do |ui_file|
22
+ # make tasks to generate _ui.rb files
23
+ file ui_rb_file( ui_file ) => [ ui_file ] do |t|
24
+ sh "rbuic4 #{t.prerequisites} -o #{t.name}"
25
+ end
26
+
27
+ # make tasks to start designer when the ui file is named
28
+ desc "Start Qt designer with #{ui_file}"
29
+ namespace :ui do |n|
30
+ task Pathname.new(ui_file).basename.to_s.ext do |t|
31
+ sh "designer #{ui_file}"
32
+ end
33
+ end
34
+ end
35
+
36
+ desc 'Generate all _ui.rb files'
37
+ task :ui => UI_FILES.map{|x| ui_rb_file( x ) }
38
+
39
+ namespace :ui do
40
+ desc 'Start Qt designer with the argument, or all .ui files.'
41
+ task :design do |t|
42
+ ARGV.shift()
43
+ if ARGV.size == 0
44
+ # start designer with all ui files
45
+ sh "designer #{UI_FILES.join(' ')}"
46
+ else
47
+ # start designer with all files that match an argument
48
+ sh "designer #{ ARGV.map{|x| UI_FILES.grep( /\/#{x}/ ) }.join(' ') }"
49
+ end
50
+ true
51
+ end
52
+ end
53
+
54
+ desc "Runs Clevic in warning mode, with test databases and debug flag on"
55
+ task :run => :ui do |t|
56
+ ARGV.shift()
57
+ exec "ruby -w -Ilib bin/clevic -D #{ARGV.join(' ')}"
58
+ end
59
+
60
+ desc "Runs Clevic in debug mode, with test databases"
61
+ task :debug => :ui do |t|
62
+ ARGV.shift()
63
+ exec "ruby -w -rdebug -Ilib bin/clevic -D #{ARGV.join(' ')}"
64
+ end
65
+
66
+ desc "irb in this project's context"
67
+ task :irb do |t|
68
+ ARGV.shift()
69
+ ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
70
+ exec "irb -Ilib -rclevic"
71
+ end
72
+
73
+ # generate tasks for all model definition files
74
+ MODELS_LIST = FileList.new( '**/*models.rb' )
75
+
76
+ def short_model( model_file )
77
+ Pathname.new( model_file ).basename.to_s.gsub( /_models.rb/, '' )
78
+ end
79
+
80
+ MODELS_LIST.each do |model_file|
81
+ # generate irb contexts
82
+ desc "irb with #{model_file}"
83
+ namespace :irb do
84
+ task short_model( model_file ) do |t|
85
+ ARGV.shift()
86
+ ENV['RUBYLIB'] ||= '.'
87
+ ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
88
+ exec "irb -Ilib -rclevic -r#{model_file} -rclevic/db_options.rb"
89
+ end
90
+ end
91
+
92
+ # generate runs
93
+ desc "run clevic with #{model_file}"
94
+ task short_model( model_file ) => :ui do |t|
95
+ ARGV.shift()
96
+ exec "ruby -w -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
97
+ end
98
+ end
99
+
100
+ task :package => :ui
data/TODO ADDED
@@ -0,0 +1,131 @@
1
+ Times Ctrl-Shift-" should not copy date if it already exists, and should not copy time if it's a different date.
2
+ Times Ctrl-Shift-" after it's done, tab doesn't change fields
3
+ Times look up invoice for project leaves the wrong fields highlighted, and focus in the wrong field.
4
+
5
+ Using F4 to open list, and then selecting from the combo and exiting using Return (or tab?) doesn't set the correct value
6
+ wrap description, and allow Access-style zooming
7
+ Undo deletes and other commands. Possibly via ActiveRecord callbacks.
8
+
9
+ Keep a history of changes, ie xy, new. xy, changed. x,y copied etc.
10
+
11
+ OSX
12
+ ---
13
+ Check that qtruby4 runs on OSX and so does Clevic. It does. Very slowly on Leilani's Mini.
14
+
15
+ windows
16
+ -------
17
+ Ctrl-; date formatting goes 07--08 instead of 07-Apr-08. But typing the full month will be OK.
18
+
19
+ empty database
20
+ --------------
21
+ resize fields for first record, while it's being entered. use Qt::ExpandingLineEdit for ComboDelegate? Doesn't exist in Ruby bindings
22
+
23
+ editing
24
+ -------
25
+ F2 for standard edit, F4 for calendar edit
26
+ Only move for data_changed if field was exited with tab, not enter.
27
+ make sure record is saved when changing tabs
28
+ tooltips for tabs
29
+ Help in general for new data capture people
30
+ Help to right of tabs
31
+ messages for wrong dates etc
32
+ numeric months
33
+
34
+ Combos
35
+ ------
36
+ shortlist combos by prefix. See Qt Examples.
37
+ turn on/off smart filters for relational delegates. Like selecting only distincts in the last year.
38
+ context menu for delegates, ie sort order, last used, etc
39
+
40
+ shortcut sets, depending on which OS you're used. use QKeyEvent::matches ( QKeySequence::StandardKey key )
41
+
42
+ Doing data capture, sort by id, but unfilter reverts to date/id rather than entry order
43
+ optional warnings for back-dated entries. Highlighting
44
+ make sure bsearch easier to install
45
+
46
+ ORDER BY allow functions, ie lower(project)
47
+ handle db errors
48
+ test with sqlite
49
+ easier way to run models, search LOAD_PATH
50
+
51
+ generate models - DrySQL
52
+ Ctrl-PgDn to last row in this column. Also extend selection
53
+ sorting by header. See void QAbstractItemModel::sort ( int column, Qt::SortOrder order = Qt::AscendingOrder )
54
+ - layoutChanged
55
+
56
+ search with acts_as_searchable and hyperestraier
57
+ acts_as_trashable to undo deletes
58
+ implement undo of field changes
59
+
60
+ cache belongs_to associations, during loading, ie don't affect ability to pick up
61
+ changes more or less instantly.
62
+
63
+ moving of columns
64
+ /-style keyboard search by selected column, or everything if no column selected
65
+ /-style filtering?
66
+
67
+ for dates, add year if not specified, with 6 months on either side range. Configurable?
68
+ value formatting not in model
69
+ copy a field from a mouse-selection (ctrl-b maybe)
70
+ hiding of fields
71
+
72
+ save context menu settings, filter settings, etc
73
+ filtering by various things. http://doc.trolltech.com/4.3/qsortfilterproxymodel.html
74
+ highlighting by various things
75
+ cut and paste (in model and csv format)
76
+
77
+ drop cached model objects from CacheTable when they're not in use
78
+
79
+ allow scroll viewport to centre when at end of dataset
80
+ QAbstractItemView::ScrollHint
81
+
82
+ \value EnsureVisible Scroll to ensure that the item is visible.
83
+ \value PositionAtTop Scroll to position the item at the top of the viewport.
84
+ \value PositionAtBottom Scroll to position the item at the bottom of the viewport.
85
+ \value PositionAtCenter Scroll to position the item at the center of the viewport.
86
+
87
+
88
+ preferences
89
+ -----------
90
+ store previous searches, by model & app
91
+
92
+
93
+ maybe
94
+ -----
95
+ use rubigen for creating apps
96
+ allow moving of rows
97
+ discontiguous copying of entities/csv
98
+ multi-row copying
99
+ pasting of csv, into rectangular regions
100
+ collect a set of data requests to the model, and do them in one SQL query. See EntryTableView#moveCursor
101
+ Use SQL cursors for find & find_next?
102
+
103
+ Accounts
104
+ --------
105
+ paste of "common" records with different dates
106
+ restricted type for Account Type record
107
+
108
+ Times
109
+ -----
110
+
111
+ warnings on overlap times (in status bar)
112
+ warnings on large intervals (in status bar)
113
+
114
+ db
115
+ --
116
+
117
+ times
118
+
119
+ alter table invoices rename column type to billing;
120
+ alter table entries add primary key (id);
121
+ alter table entries alter column id set not null
122
+ select max(id) from entries;
123
+ create sequence entries_id_seq start with 11694;
124
+ alter table entries alter column id set default nextval('entries_id_seq');
125
+ update entries set module = null where module = '';
126
+ update entries set module = 'admin' where module = 'Admin';
127
+
128
+ alter table projects rename active to old_active;
129
+ alter table projects add column active boolean;
130
+ update projects set active = ( old_active = 1 );
131
+ alter table projects drop column old_active;
@@ -0,0 +1,122 @@
1
+ require 'clevic.rb'
2
+
3
+ # db connection options
4
+ $options ||= {}
5
+ $options[:database] ||= $options[:debug] ? 'accounts_test' : 'accounts'
6
+ $options[:adapter] ||= 'postgresql'
7
+ $options[:host] ||= 'localhost'
8
+ $options[:username] ||= 'panic'
9
+ $options[:password] ||= ''
10
+
11
+ class Entry < ActiveRecord::Base
12
+ include ActiveRecord::Dirty
13
+ belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
14
+ belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
15
+
16
+ # define how fields will be displayed
17
+ def self.ui( parent )
18
+ Clevic::TableView.new( self, parent ).create_model do
19
+ plain :date, :sample => '88-WWW-99'
20
+ distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26, :frequency => true
21
+ relational :debit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
22
+ relational :credit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
23
+ plain :amount, :sample => 999999.99
24
+ distinct :category
25
+ plain :cheque_number
26
+ plain :active, :sample => 'WW'
27
+ plain :vat, :label => 'VAT', :sample => 'WW', :tooltip => 'Does this include VAT?'
28
+
29
+ records :order => 'date, id'
30
+ end
31
+ end
32
+
33
+ # called when data is changed in the UI
34
+ def self.data_changed( top_left, bottom_right, view )
35
+ if top_left == bottom_right
36
+ update_credit_debit( top_left, view )
37
+ else
38
+ puts "top_left: #{top_left.inspect}"
39
+ puts "bottom_right: #{bottom_right.inspect}"
40
+ puts "can't do data_changed for a range"
41
+ end
42
+ end
43
+
44
+ # check that the current field is :descriptions, then
45
+ # copy the values for the credit and debit fields
46
+ # from the previous similar entry
47
+ def self.update_credit_debit( current_index, view )
48
+ return if !current_index.valid?
49
+ current_field = current_index.attribute
50
+ if current_field == :description
51
+ # most recent entry, ordered in reverse
52
+ similar = self.find(
53
+ :first,
54
+ :conditions => ["#{current_field} = ?", current_index.attribute_value],
55
+ :order => 'date desc'
56
+ )
57
+ if similar != nil
58
+ # set the values
59
+ current_index.entity.debit = similar.debit
60
+ current_index.entity.credit = similar.credit
61
+ current_index.entity.category = similar.category
62
+
63
+ # emit signal to update view from top_left to bottom_right
64
+ model = current_index.model
65
+ top_left_index = model.create_index( current_index.row, 0 )
66
+ bottom_right_index = model.create_index( current_index.row, view.builder.fields.size )
67
+ view.dataChanged( top_left_index, bottom_right_index )
68
+
69
+ # move edit cursor to amount field
70
+ view.selection_model.clear
71
+ view.override_next_index( model.create_index( current_index.row, view.builder.index( :amount ) ) )
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ class Account < ActiveRecord::Base
78
+ include ActiveRecord::Dirty
79
+ has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
80
+ has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
81
+
82
+ # define how fields are displayed
83
+ def self.ui( parent )
84
+ Clevic::TableView.new( self, parent ).create_model do
85
+ plain :name
86
+ restricted :vat, :label => 'VAT', :set => %w{ yes no all }
87
+ plain :account_type
88
+ plain :pastel_number, :alignment => Qt::AlignRight, :label => 'Pastel'
89
+ plain :fringe, :format => "%.1f"
90
+ plain :active
91
+
92
+ records :order => 'name,account_type'
93
+ end
94
+ end
95
+ end
96
+
97
+ # order of tab display
98
+ $options[:models] = [ Entry, Account ]
99
+
100
+ # This is a read-only view, which is currently not implemented
101
+ #~ class Values < ActiveRecord::Base
102
+ #~ include ActiveRecord::Dirty
103
+ #~ set_table_name 'values'
104
+ #~ has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
105
+ #~ has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
106
+ #~ def self.ui( parent )
107
+ #~ Clevic::TableView.new( self, parent ).create_model do
108
+ #~ readonly
109
+ #~ plain :date
110
+ #~ plain :description
111
+ #~ plain :debit
112
+ #~ plain :credit
113
+ #~ plain :pre_vat_amount
114
+ #~ plain :cheque_number
115
+ #~ plain :vat, :label => 'VAT'
116
+ #~ plain :financial_year
117
+ #~ plain :month
118
+
119
+ #~ records :order => 'date'
120
+ #~ end
121
+ #~ end
122
+ #~ end
data/bin/clevic ADDED
@@ -0,0 +1,64 @@
1
+ #! /usr/bin/ruby
2
+
3
+ require 'clevic/browser.rb'
4
+ require 'ruby-debug'
5
+
6
+ # fetch command line options
7
+ require 'optparse'
8
+
9
+ # find and require variations on file_path
10
+ def require_if( file_path )
11
+ require file_path if File.exist?( file_path ) || File.exist?( file_path + '.rb' )
12
+ end
13
+
14
+ $options = {}
15
+ oparser = OptionParser.new
16
+ oparser.on( '-H', '--host HOST', 'RDBMS host', String ) { |o| $options[:host] = o }
17
+ oparser.on( '-u', '--user USERNAME', String ) { |o| $options[:user] = o }
18
+ oparser.on( '-p', '--pass PASSWORD', String ) { |o| $options[:password] = o }
19
+ oparser.on( '-t', '--table TABLE', 'Table to display', String ) { |o| $options[:table] = o }
20
+ oparser.on( '-d', '--database DATABASE', 'Database name', String ) { |o| $options[:database] = o }
21
+ oparser.on( '-D', '--debug' ) { |o| $options[:debug] = true }
22
+ oparser.on( '-v', '--verbose' ) { |o| $options[:verbose] = true }
23
+ oparser.on( '-h', '-?', '--help' ) do |o|
24
+ puts oparser.to_s
25
+ exit( 0 )
26
+ end
27
+
28
+ args = oparser.parse( ARGV )
29
+
30
+ if args.size > 0
31
+ $options[:definition] = args.shift
32
+ require_if "#{$options[:definition]}_models"
33
+ require_if $options[:definition]
34
+ else
35
+ raise "no model definition file specified"
36
+ end
37
+
38
+ app = Qt::Application.new( args )
39
+
40
+ if $options[:debug]
41
+ require 'pp'
42
+ #~ puts "$options: #{$options.inspect}"
43
+ #~ puts args.inspect
44
+ end
45
+
46
+ if !$options.has_key?( :database )
47
+ raise "Please define $options[:database]"
48
+ end
49
+
50
+ # connect to db
51
+ require 'clevic/db_options.rb'
52
+
53
+ puts "using database #{ActiveRecord::Base.connection.raw_connection.db}" if $options[:debug]
54
+
55
+ # show UI
56
+ main_window = Qt::MainWindow.new
57
+ browser = Clevic::Browser.new( main_window )
58
+ browser.open
59
+ # this must come after Clevic::Browser.new
60
+ main_window.window_title = $options[:database]
61
+ main_window.show
62
+ # make sure any partially edited records are saved when the window is closed
63
+ app.connect( SIGNAL('lastWindowClosed()') ) { browser.save_all }
64
+ app.exec
@@ -0,0 +1,87 @@
1
+ module ActiveRecord
2
+ # Track unsaved changes.
3
+ module Dirty
4
+ def self.included(base)
5
+ base.attribute_method_suffix '_changed?', '_change', '_original'
6
+ base.alias_method_chain :read_attribute, :dirty
7
+ base.alias_method_chain :write_attribute, :dirty
8
+ base.alias_method_chain :save, :dirty
9
+ end
10
+
11
+ # Do any attributes have unsaved changes?
12
+ # person.changed? # => false
13
+ # person.name = 'bob'
14
+ # person.changed? # => true
15
+ def changed?
16
+ !changed_attributes.empty?
17
+ end
18
+
19
+ # List of attributes with unsaved changes.
20
+ # person.changed # => []
21
+ # person.name = 'bob'
22
+ # person.changed # => ['name']
23
+ def changed
24
+ changed_attributes.keys
25
+ end
26
+
27
+ # Map of changed attrs => [original value, new value]
28
+ # person.changes # => {}
29
+ # person.name = 'bob'
30
+ # person.changes # => { 'name' => ['bill', 'bob'] }
31
+ def changes
32
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
33
+ end
34
+
35
+
36
+ # Clear changed attributes after they are saved.
37
+ def save_with_dirty(*args) #:nodoc:
38
+ save_without_dirty(*args)
39
+ ensure
40
+ changed_attributes.clear
41
+ end
42
+
43
+ private
44
+ # Map of change attr => original value.
45
+ def changed_attributes
46
+ @changed_attributes ||= {}
47
+ end
48
+
49
+
50
+ # Wrap read_attribute to freeze its result.
51
+ def read_attribute_with_dirty(attr)
52
+ read_attribute_without_dirty(attr).freeze
53
+ end
54
+
55
+ # Wrap write_attribute to remember original attribute value.
56
+ def write_attribute_with_dirty(attr, value)
57
+ attr = attr.to_s
58
+
59
+ # The attribute already has an unsaved change.
60
+ unless changed_attributes.include?(attr)
61
+ old = read_attribute(attr)
62
+
63
+ # Remember the original value if it's different.
64
+ changed_attributes[attr] = old unless old == value
65
+ end
66
+
67
+ # Carry on.
68
+ write_attribute_without_dirty(attr, value)
69
+ end
70
+
71
+
72
+ # Handle *_changed? for method_missing.
73
+ def attribute_changed?(attr)
74
+ changed_attributes.include?(attr)
75
+ end
76
+
77
+ # Handle *_change for method_missing.
78
+ def attribute_change(attr)
79
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
80
+ end
81
+
82
+ # Handle *_original for method_missing.
83
+ def attribute_original(attr)
84
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
85
+ end
86
+ end
87
+ end