clevic 0.5.1

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/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