clevic 0.11.1 → 0.12.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.
@@ -0,0 +1,84 @@
1
+ require 'clevic/item_delegate.rb'
2
+
3
+ module Clevic
4
+
5
+ class TextDelegate < ItemDelegate
6
+
7
+ # Doesn't do anything useful yet, but I'm leaving
8
+ # it here so I don't have to change other code.
9
+ class TextEditor < Qt::PlainTextEdit
10
+ end
11
+
12
+ # this is overridden in Qt::ItemDelegate, but that
13
+ # always catches the return key. Which we want for text editing.
14
+ # Instead, we use Ctrl-Enter to save the edited text.
15
+ # return true if event is handled, false otherwise
16
+ def eventFilter( object, event )
17
+ if object.class == TextEditor && event.class == Qt::KeyEvent
18
+ retval =
19
+ case
20
+ when event.ctrl? && ( event.enter? || event.return? )
21
+ # close and save
22
+ # copied from QItemDelegate.
23
+ emit commitData( object )
24
+ emit closeEditor( object )
25
+ true
26
+
27
+ # send an enter or return to the text editor
28
+ when event.enter? || event.return?
29
+ object.event( event )
30
+ true
31
+ end
32
+ end
33
+ retval || super
34
+ end
35
+
36
+ # maybe open in a separate window?
37
+ def full_edit
38
+ puts "#{self.class.name} full_edit"
39
+ end
40
+
41
+ # Override the Qt method
42
+ def createEditor( parent_widget, style_option_view_item, model_index )
43
+ if false && model_index.gui_value.count("\n") == 0
44
+ # futzing about here, really
45
+ @editor = Qt::LineEdit.new( parent_widget )
46
+ else
47
+ @editor = TextEditor.new( parent_widget )
48
+ @editor.install_event_filter( self )
49
+ end
50
+ @editor
51
+ end
52
+
53
+ # Override the Qt::ItemDelegate method.
54
+ def updateEditorGeometry( editor, style_option_view_item, model_index )
55
+ rect = Qt::Rect.new( style_option_view_item.rect.top_left, style_option_view_item.rect.size )
56
+
57
+ # ask the editor for how much space it wants, and set the editor
58
+ # to that size when it displays in the table
59
+ rect.set_width( [editor.size_hint.width,rect.width].max )
60
+ rect.set_height( editor.size_hint.height )
61
+
62
+ unless editor.parent.rect.contains( rect )
63
+ # 46 because TableView returns an incorrect bottom.
64
+ # And I can't find out how to get the correct value.
65
+ rect.move_bottom( parent.contents_rect.bottom - 46 )
66
+ end
67
+ editor.set_geometry( rect )
68
+ end
69
+
70
+ # Override the Qt method to send data to the editor from the model.
71
+ def setEditorData( editor, model_index )
72
+ editor.plain_text = model_index.gui_value
73
+ end
74
+
75
+ # Send the data from the editor to the model. The data will
76
+ # be translated by translate_from_editor_text,
77
+ def setModelData( editor, abstract_item_model, model_index )
78
+ model_index.attribute_value = editor.to_plain_text
79
+ abstract_item_model.data_changed( model_index )
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -1,8 +1,8 @@
1
1
  module Clevic #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 11
5
- TINY = 1
4
+ MINOR = 12
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/clevic/view.rb CHANGED
@@ -77,8 +77,34 @@ module Clevic
77
77
  def define_actions( table_view, action_builder )
78
78
  end
79
79
 
80
- # define data changed events
81
- def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
80
+ # notify
81
+ def notify_field( table_view, model_index )
82
+ ndc = model_index.field.notify_data_changed
83
+ case ndc
84
+ when Proc
85
+ ndc.call( self, table_view, model_index )
86
+
87
+ when Symbol
88
+ send( ndc, table_view, model_index )
89
+ end
90
+ end
91
+
92
+ # Define data changed events. Default is to call notify_data_changed
93
+ # for each field in the rectangular area defined by top_left and bottom_right
94
+ # (which are Qt::ModelIndex instances)
95
+ def notify_data_changed( table_view, top_left, bottom_right )
96
+ if top_left == bottom_right
97
+ # shortcut to just the one, seeing as it's probably the most common
98
+ notify_field( table_view, top_left )
99
+ else
100
+ # do the entire rectagular area
101
+ (top_left.row..bottom_right.row).each do |row_index|
102
+ (top_left.column..bottom_right.column).each do |column_index|
103
+ model_index = table_view.model.create_index( row_index, column_index )
104
+ notify_field( table_view, model_index )
105
+ end
106
+ end
107
+ end
82
108
  end
83
109
 
84
110
  # be notified of key presses
@@ -9,7 +9,7 @@ Clevic::DbOptions.connect( $options ) do
9
9
  database options[:database]
10
10
  end
11
11
  adapter :postgresql
12
- username 'accounts'
12
+ username options[:username].blank? ? 'accounts' : options[:username]
13
13
  end
14
14
 
15
15
  class Entry < ActiveRecord::Base
@@ -20,7 +20,19 @@ class Entry < ActiveRecord::Base
20
20
 
21
21
  define_ui do
22
22
  plain :date, :sample => '88-WWW-99'
23
- distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26
23
+ distinct :description do |f|
24
+ f.conditions "now() - date <= '1 year'"
25
+ f.sample( 'm' * 26 )
26
+ f.notify_data_changed = lambda do |entity_view, table_view, model_index|
27
+ if model_index.entity.credit.nil? && model_index.entity.debit.nil?
28
+ entity_view.update_from_description( model_index )
29
+
30
+ # move edit cursor to amount field
31
+ table_view.selection_model.clear
32
+ table_view.override_next_index( model_index.choppy( :column => :amount ) )
33
+ end
34
+ end
35
+ end
24
36
  relational :debit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
25
37
  relational :credit, :display => 'name', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
26
38
  plain :amount, :sample => 999999.99
@@ -32,44 +44,26 @@ class Entry < ActiveRecord::Base
32
44
  records :order => 'date, id'
33
45
  end
34
46
 
35
- # called when data is changed in the UI
36
- def self.data_changed( top_left, bottom_right, view )
37
- if top_left == bottom_right
38
- update_credit_debit( top_left, view )
39
- else
40
- puts "top_left: #{top_left.inspect}"
41
- puts "bottom_right: #{bottom_right.inspect}"
42
- puts "can't do data_changed for a range"
43
- end
44
- end
45
-
46
- # check that the current field is :descriptions, then
47
- # copy the values for the credit and debit fields
48
- # from the previous similar entry
49
- def self.update_credit_debit( current_index, view )
50
- return unless current_index.valid?
51
- current_field = current_index.attribute
52
- if current_field == :description
53
- # most recent entry, ordered in reverse
54
- similar = self.find(
55
- :first,
56
- :conditions => ["#{current_field} = ?", current_index.attribute_value],
57
- :order => 'date desc'
58
- )
59
- if similar != nil
60
- # set the values
61
- current_index.entity.debit = similar.debit
62
- current_index.entity.credit = similar.credit
63
- current_index.entity.category = similar.category
64
-
65
- # emit signal to update view from top_left to bottom_right
66
- top_left_index = current_index.choppy( :column => 0 )
67
- bottom_right_index = current_index.choppy( :column => view.model.column_count - 1 )
68
- view.dataChanged( top_left_index, bottom_right_index )
69
-
70
- # move edit cursor to amount field
71
- view.selection_model.clear
72
- view.override_next_index( current_index.choppy( :column => view.field_column( :amount ) ) )
47
+ # Copy the values for the credit and debit fields
48
+ # from the previous similar entry with a similar description
49
+ def self.update_from_description( current_index )
50
+ return if current_index.attribute_value.nil?
51
+ # most recent entry, ordered in reverse
52
+ similar = self.find(
53
+ :first,
54
+ :conditions => ["#{current_index.attribute} = ?", 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 that whole row has changed
64
+ current_index.model.data_changed do |change|
65
+ change.top_left = current_index.choppy( :column => 0 )
66
+ change.bottom_right = current_index.choppy( :column => current_index.model.column_count - 1 )
73
67
  end
74
68
  end
75
69
  end
@@ -19,11 +19,35 @@ class Entry < ActiveRecord::Base
19
19
 
20
20
  define_ui do
21
21
  plain :date, :sample => '28-Dec-08'
22
- relational :project, :display => 'project', :conditions => 'active = true', :order => 'lower(project)'
22
+
23
+ # The project field
24
+ relational :project do |field|
25
+ field.display = 'project'
26
+ field.conditions = 'active = true'
27
+ field.order = 'lower(project)'
28
+
29
+ # handle data changed events. In this case,
30
+ # auto-fill-in the invoice field.
31
+ field.notify_data_changed do |entity_view, table_view, model_index|
32
+ if model_index.entity.invoice.nil?
33
+ entity_view.invoice_from_project( table_view, model_index ) do
34
+ # move here next if the invoice was changed
35
+ table_view.override_next_index model_index.choppy( :column => :start )
36
+ end
37
+ end
38
+ end
39
+ end
40
+
23
41
  relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
42
+
43
+ # call time_color method for foreground color value
24
44
  plain :start, :foreground => :time_color, :tooltip => :time_tooltip
45
+
46
+ # another way to call time_color method for foreground color value
25
47
  plain :end, :foreground => lambda{|x| x.time_color}, :tooltip => :time_tooltip
26
- plain :description, :sample => 'This is a long string designed to hold lots of data and description'
48
+
49
+ # multiline text
50
+ text :description, :sample => 'This is a long string designed to hold lots of data and description'
27
51
 
28
52
  relational :activity do
29
53
  display 'activity'
@@ -34,18 +58,25 @@ class Entry < ActiveRecord::Base
34
58
 
35
59
  distinct :module, :tooltip => 'Module or sub-project'
36
60
  plain :charge, :tooltip => 'Is this time billable?'
37
- distinct :person, :tooltip => 'The person who did the work'
61
+ distinct :person, :default => 'John', :tooltip => 'The person who did the work'
38
62
 
39
63
  records :order => 'date, start, id'
40
64
  end
41
65
 
42
- def self.actions( view, action_builder )
66
+ def self.define_actions( view, action_builder )
43
67
  action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
44
68
  smart_copy( view )
45
69
  end
46
70
 
47
71
  action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
48
- invoice_from_project( view.current_index, view )
72
+ invoice_from_project( view, view.current_index ) do
73
+ # execute the block if the invoice is changed
74
+
75
+ # save this before selection model is cleared
76
+ current_index = view.current_index
77
+ view.selection_model.clear
78
+ view.current_index = current_index.choppy( :column => :start )
79
+ end
49
80
  end
50
81
  end
51
82
 
@@ -54,63 +85,68 @@ class Entry < ActiveRecord::Base
54
85
  view.sanity_check_read_only
55
86
  view.sanity_check_ditto
56
87
 
57
- # need a reference to current_index here, because selection_model.clear will invalidate
58
- # view.current_index. And anyway, its shorter and easier to read.
88
+ # need a reference to current_index here, because selection_model.clear will
89
+ # invalidate view.current_index. And anyway, its shorter and easier to read.
59
90
  current_index = view.current_index
60
- if current_index.row > 1
91
+ if current_index.row >= 1
61
92
  # fetch previous item
62
93
  previous_item = view.model.collection[current_index.row - 1]
63
94
 
64
95
  # copy the relevant fields
65
- current_index.entity.start = previous_item.end
66
- [:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
96
+ current_index.entity.date = previous_item.date if current_index.entity.date.blank?
97
+ # depends on previous line
98
+ current_index.entity.start = previous_item.end if current_index.entity.date == previous_item.date
99
+
100
+ # copy rest of fields
101
+ [:project, :invoice, :activity, :module, :charge, :person].each do |attr|
67
102
  current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
68
103
  end
69
104
 
70
105
  # tell view to update
71
- top_left_index = current_index.choppy( :column => 0 )
72
- bottom_right_index = current_index.choppy( :column => view.model.fields.size - 1 )
73
- view.dataChanged( top_left_index, bottom_right_index )
106
+ view.model.data_changed do |change|
107
+ change.top_left = current_index.choppy( :column => 0 )
108
+ change.bottom_right = current_index.choppy( :column => view.model.fields.size - 1 )
109
+ end
74
110
 
75
- # move to end time field
76
- view.selection_model.clear
111
+ # move to the first empty time field
77
112
  next_field =
78
113
  if current_index.entity.start.blank?
79
114
  :start
80
115
  else
81
116
  :end
82
117
  end
83
- next_index = current_index.choppy( :column => view.field_column( next_field ) )
84
- view.override_next_index( next_index )
118
+
119
+ # next cursor location
120
+ view.selection_model.clear
121
+ view.current_index = current_index.choppy( :column => next_field )
85
122
  end
86
123
  end
87
124
 
88
- # called when data is changed in this model's table view
89
- def self.data_changed( top_left, bottom_right, view )
90
- invoice_from_project( top_left, view ) if ( top_left == bottom_right )
91
- end
92
-
93
- # auto-complete invoice number field from project
94
- def self.invoice_from_project( current_index, view )
95
- current_field = current_index.attribute
96
- if [:project,:invoice].include?( current_field ) && current_index.entity.project != nil
125
+ # Auto-complete invoice number field from project.
126
+ # &block will be executed if an invoice was assigned
127
+ # If block takes one parameter, pass the new invoice.
128
+ def self.invoice_from_project( table_view, current_index, &block )
129
+ if current_index.entity.project != nil
97
130
  # most recent entry, ordered in reverse
98
131
  invoice = current_index.entity.project.latest_invoice
99
-
100
132
  unless invoice.nil?
101
133
  # make a reference to the invoice
102
134
  current_index.entity.invoice = invoice
103
135
 
104
136
  # update view from top_left to bottom_right
105
- changed_index = current_index.choppy( :column => view.field_column( :invoice ) )
106
- view.dataChanged( changed_index, changed_index )
137
+ table_view.model.data_changed( current_index.choppy( :column => :invoice ) )
107
138
 
108
- # move edit cursor to start time field
109
- view.selection_model.clear
110
- view.override_next_index( current_index.choppy( :column => view.field_column( :start ) ) )
139
+ unless block.nil?
140
+ if block.arity == 1
141
+ block.call( invoice )
142
+ else
143
+ block.call
144
+ end
145
+ end
111
146
  end
112
147
  end
113
148
  end
149
+
114
150
  end
115
151
 
116
152
  class Invoice < ActiveRecord::Base
@@ -123,7 +159,7 @@ class Invoice < ActiveRecord::Base
123
159
  plain :invoice_number
124
160
  restricted :status, :set => ['not sent', 'sent', 'paid', 'debt', 'writeoff', 'internal']
125
161
  restricted :billing, :set => %w{Hours Quote Internal}
126
- plain :quote_date
162
+ plain :quote_date, :format => '%d-%b-%y', :edit_format => '%d-%b-%Y', :tooltip => 'the date and time when the quote was supplied', :default => lambda{|x| DateTime.now}
127
163
  plain :quote_amount
128
164
  plain :description
129
165
 
data/tasks/clevic.rake ADDED
@@ -0,0 +1,111 @@
1
+ task :package => :ui
2
+
3
+ desc "Update ChangeLog from the SVN log"
4
+ task :changelog do |t|
5
+ ARGV.shift
6
+ exec "svn2cl --break-before-msg -o ChangeLog #{ARGV.join(' ')}"
7
+ end
8
+
9
+ # generate a _ui.rb filename from a .ui filename
10
+ def ui_rb_file( ui_file )
11
+ ui_file.gsub( /\.ui$/, '_ui.rb' )
12
+ end
13
+
14
+ # list of .ui files
15
+ UI_FILES = FileList.new( 'lib/clevic/ui/*.ui' )
16
+ CLEAN.include( 'ChangeLog', 'coverage', 'profiling' )
17
+ CLOBBER.include( 'ChangeLog', 'pkg', 'lib/clevic/ui/*_ui.rb' )
18
+
19
+ UI_FILES.each do |ui_file|
20
+ # make tasks to generate _ui.rb files
21
+ file ui_rb_file( ui_file ) => [ ui_file ] do |t|
22
+ sh "rbuic4 #{t.prerequisites} -o #{t.name}"
23
+ end
24
+
25
+ # make tasks to start designer when the ui file is named
26
+ desc "Start Qt designer with #{ui_file}"
27
+ namespace :ui do |n|
28
+ task Pathname.new(ui_file).basename.to_s.ext do |t|
29
+ sh "designer #{ui_file}"
30
+ end
31
+ end
32
+ end
33
+
34
+ desc 'Generate all _ui.rb files'
35
+ task :ui => UI_FILES.map{|x| ui_rb_file( x ) }
36
+
37
+ namespace :ui do
38
+ desc 'Start Qt designer with the argument, or all .ui files.'
39
+ task :design do |t|
40
+ ARGV.shift()
41
+ if ARGV.size == 0
42
+ # start designer with all ui files
43
+ sh "designer #{UI_FILES.join(' ')}"
44
+ else
45
+ # start designer with all files that match an argument
46
+ sh "designer #{ ARGV.map{|x| UI_FILES.grep( /\/#{x}/ ) }.join(' ') }"
47
+ end
48
+ true
49
+ end
50
+ end
51
+
52
+ desc "Runs Clevic in normal mode, with live database."
53
+ task :run => :ui do |t|
54
+ ARGV.shift()
55
+ exec "ruby -Ilib bin/clevic #{ARGV.join(' ')}"
56
+ end
57
+
58
+ desc "Runs Clevic in debug mode, with test databases"
59
+ task :debug => :ui do |t|
60
+ ARGV.shift()
61
+ exec "ruby -w -rdebug -Ilib bin/clevic -D #{ARGV.join(' ')}"
62
+ end
63
+
64
+ desc "irb in this project's context"
65
+ task :irb do |t|
66
+ ARGV.shift()
67
+ ENV['RUBYLIB'] ||= ''
68
+ ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
69
+ exec "irb -Ilib -rclevic"
70
+ end
71
+
72
+ # generate tasks for all model definition files
73
+ MODELS_LIST = FileList.new( '**/*models.rb' )
74
+
75
+ def short_model( model_file )
76
+ Pathname.new( model_file ).basename.to_s.gsub( /_models.rb/, '' )
77
+ end
78
+
79
+ MODELS_LIST.each do |model_file|
80
+ # generate irb contexts
81
+ desc "irb with #{model_file}"
82
+ namespace :irb do
83
+ task short_model( model_file ) do |t|
84
+ ARGV.shift()
85
+ ARGV.shift() if ARGV[0] == '--'
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
+ namespace :run do
94
+ desc "run clevic with #{model_file}"
95
+ task short_model( model_file ) => :ui do |t|
96
+ ARGV.shift()
97
+ ARGV.shift() if ARGV[0] == '--'
98
+ cmd = "ruby -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
99
+ puts "cmd: #{cmd.inspect}"
100
+ exec cmd
101
+ end
102
+ end
103
+
104
+ namespace :warn do
105
+ desc "run clevic with #{model_file} and warnings on"
106
+ task short_model( model_file ) => :ui do |t|
107
+ ARGV.shift()
108
+ exec "ruby -w -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
109
+ end
110
+ end
111
+ end