clevic 0.8.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/History.txt +9 -0
  2. data/Manifest.txt +13 -10
  3. data/README.txt +6 -9
  4. data/Rakefile +35 -24
  5. data/TODO +29 -17
  6. data/bin/clevic +84 -37
  7. data/config/hoe.rb +7 -3
  8. data/lib/clevic.rb +2 -4
  9. data/lib/clevic/browser.rb +37 -49
  10. data/lib/clevic/cache_table.rb +55 -165
  11. data/lib/clevic/db_options.rb +32 -21
  12. data/lib/clevic/default_view.rb +66 -0
  13. data/lib/clevic/delegates.rb +51 -67
  14. data/lib/clevic/dirty.rb +101 -0
  15. data/lib/clevic/extensions.rb +24 -38
  16. data/lib/clevic/field.rb +400 -99
  17. data/lib/clevic/item_delegate.rb +32 -33
  18. data/lib/clevic/model_builder.rb +315 -148
  19. data/lib/clevic/order_attribute.rb +53 -0
  20. data/lib/clevic/record.rb +57 -57
  21. data/lib/clevic/search_dialog.rb +71 -67
  22. data/lib/clevic/sql_dialects.rb +33 -0
  23. data/lib/clevic/table_model.rb +73 -120
  24. data/lib/clevic/table_searcher.rb +165 -0
  25. data/lib/clevic/table_view.rb +140 -100
  26. data/lib/clevic/ui/.gitignore +1 -0
  27. data/lib/clevic/ui/browser_ui.rb +55 -56
  28. data/lib/clevic/ui/search_dialog_ui.rb +50 -51
  29. data/lib/clevic/version.rb +2 -2
  30. data/lib/clevic/view.rb +89 -0
  31. data/models/accounts_models.rb +12 -9
  32. data/models/minimal_models.rb +4 -2
  33. data/models/times_models.rb +41 -25
  34. data/models/times_sqlite_models.rb +1 -145
  35. data/models/values_models.rb +15 -16
  36. data/test/test_cache_table.rb +138 -0
  37. data/test/test_helper.rb +131 -0
  38. data/test/test_model_index_extensions.rb +22 -0
  39. data/test/test_order_attribute.rb +62 -0
  40. data/test/test_sql_dialects.rb +77 -0
  41. data/test/test_table_searcher.rb +188 -0
  42. metadata +36 -20
  43. data/bin/import-times +0 -128
  44. data/config/jamis.rb +0 -589
  45. data/env.sh +0 -1
  46. data/lib/active_record/dirty.rb +0 -87
  47. data/lib/clevic/field_builder.rb +0 -42
  48. data/website/index.html +0 -170
  49. data/website/index.txt +0 -17
  50. data/website/screenshot.png +0 -0
  51. data/website/stylesheets/screen.css +0 -131
  52. data/website/template.html.erb +0 -41
@@ -10,12 +10,14 @@ Clevic::DbOptions.connect do
10
10
  end
11
11
 
12
12
  # minimal definition to get combo boxes to show up
13
- class Entry < Clevic::Record
13
+ class Entry < ActiveRecord::Base
14
+ include Clevic::Record
14
15
  belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
15
16
  belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
16
17
  end
17
18
 
18
19
  # minimal definition to get sensible values in combo boxes
19
- class Account < Clevic::Record
20
+ class Account < ActiveRecord::Base
21
+ include Clevic::Record
20
22
  def to_s; name; end
21
23
  end
@@ -1,25 +1,28 @@
1
1
  require 'clevic.rb'
2
2
 
3
- # db connection options
4
- db = Clevic::DbOptions.connect( $options ) do
5
- # use a different db for testing, so real data doesn't get broken.
6
- database( debug? ? :times_test : :times )
7
- adapter :postgresql
8
- username 'times'
9
- end
10
-
11
3
  # model definitions
12
- class Entry < Clevic::Record
4
+ class Entry < ActiveRecord::Base
13
5
  belongs_to :invoice
14
6
  belongs_to :activity
15
7
  belongs_to :project
16
8
 
9
+ include Clevic::Record
10
+ def time_color
11
+ return if self.end.nil? || start.nil?
12
+ 'darkviolet' if self.end - start > 8.hours
13
+ end
14
+
15
+ def time_tooltip
16
+ return if self.end.nil? || start.nil?
17
+ 'Time interval greater than 8 hours' if self.end - start > 8.hours
18
+ end
19
+
17
20
  define_ui do
18
21
  plain :date, :sample => '28-Dec-08'
19
22
  relational :project, :display => 'project', :conditions => 'active = true', :order => 'lower(project)'
20
23
  relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
21
- plain :start
22
- plain :end
24
+ plain :start, :foreground => :time_color, :tooltip => :time_tooltip
25
+ plain :end, :foreground => lambda{|x| x.time_color}, :tooltip => :time_tooltip
23
26
  plain :description, :sample => 'This is a long string designed to hold lots of data and description'
24
27
 
25
28
  relational :activity do
@@ -51,24 +54,34 @@ class Entry < Clevic::Record
51
54
  view.sanity_check_read_only
52
55
  view.sanity_check_ditto
53
56
 
54
- if view.current_index.row > 1
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.
59
+ current_index = view.current_index
60
+ if current_index.row > 1
55
61
  # fetch previous item
56
- model = view.model
57
- previous_item = model.collection[view.current_index.row - 1]
62
+ previous_item = view.model.collection[current_index.row - 1]
58
63
 
59
64
  # copy the relevant fields
60
- view.current_index.entity.start = previous_item.end
65
+ current_index.entity.start = previous_item.end
61
66
  [:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
62
- view.current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
67
+ current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
63
68
  end
64
69
 
65
70
  # tell view to update
66
- top_left_index = model.create_index( view.current_index.row, 0 )
67
- bottom_right_index = model.create_index( view.current_index.row, view.current_index.column + view.builder.fields.size )
71
+ top_left_index = current_index.choppy( :column => 0 )
72
+ bottom_right_index = current_index.choppy( :column => view.model.fields.size - 1 )
68
73
  view.dataChanged( top_left_index, bottom_right_index )
69
74
 
70
75
  # move to end time field
71
- view.override_next_index( model.create_index( view.current_index.row, view.builder.index( :end ) ) )
76
+ view.selection_model.clear
77
+ next_field =
78
+ if current_index.entity.start.blank?
79
+ :start
80
+ else
81
+ :end
82
+ end
83
+ next_index = current_index.choppy( :column => view.field_column( next_field ) )
84
+ view.override_next_index( next_index )
72
85
  end
73
86
  end
74
87
 
@@ -89,18 +102,19 @@ class Entry < Clevic::Record
89
102
  current_index.entity.invoice = invoice
90
103
 
91
104
  # update view from top_left to bottom_right
92
- model = current_index.model
93
- changed_index = model.create_index( current_index.row, view.builder.index( :invoice ) )
105
+ changed_index = current_index.choppy( :column => view.field_column( :invoice ) )
94
106
  view.dataChanged( changed_index, changed_index )
95
107
 
96
108
  # move edit cursor to start time field
97
- view.override_next_index( model.create_index( current_index.row, view.builder.index( :start ) ) )
109
+ view.selection_model.clear
110
+ view.override_next_index( current_index.choppy( :column => view.field_column( :start ) ) )
98
111
  end
99
112
  end
100
113
  end
101
114
  end
102
115
 
103
- class Invoice < Clevic::Record
116
+ class Invoice < ActiveRecord::Base
117
+ include Clevic::Record
104
118
  has_many :entries
105
119
 
106
120
  define_ui do
@@ -117,7 +131,8 @@ class Invoice < Clevic::Record
117
131
  end
118
132
  end
119
133
 
120
- class Project < Clevic::Record
134
+ class Project < ActiveRecord::Base
135
+ include Clevic::Record
121
136
  has_many :entries
122
137
 
123
138
  define_ui do
@@ -142,7 +157,8 @@ class Project < Clevic::Record
142
157
 
143
158
  end
144
159
 
145
- class Activity < Clevic::Record
160
+ class Activity < ActiveRecord::Base
161
+ include Clevic::Record
146
162
  has_many :entries
147
163
 
148
164
  # define how fields are displayed
@@ -6,149 +6,5 @@ Clevic::DbOptions.connect( $options ) do
6
6
  adapter :sqlite3
7
7
  end
8
8
 
9
- # model definitions
10
- class Entry < Clevic::Record
11
- belongs_to :invoice
12
- belongs_to :activity
13
- belongs_to :project
14
-
15
- # define how fields are displayed
16
- def self.build_table_model( model_builder )
17
- model_builder.instance_exec do
18
- plain :date, :sample => '28-Dec-08'
19
- relational :project, :display => 'project', :conditions => "active = true", :order => 'lower(project)'
20
- relational :invoice, :display => 'invoice_number', :conditions => "status = 'not sent'", :order => 'invoice_number'
21
- plain :start
22
- plain :end
23
- plain :description, :sample => 'This is a long string designed to hold lots of data and description'
24
- relational :activity, :display => 'activity', :order => 'lower(activity)', :sample => 'Troubleshooting', :conditions => 'active = #{connection.quoted_true}'
25
- distinct :module, :tooltip => 'Module or sub-project'
26
- plain :charge, :tooltip => 'Is this time billable?'
27
- distinct :person, :tooltip => 'The person who did the work'
28
-
29
- records :order => 'date, start, id'
30
- end
31
- end
9
+ require 'times_models.rb'
32
10
 
33
- # called when a key is pressed in this model's table view
34
- def self.key_press_event( event, current_index, view )
35
- case
36
- # copy almost all of the previous line
37
- when event.ctrl? && event.quote_dbl?
38
- if current_index.row > 1
39
- # fetch previous item
40
- model = current_index.model
41
- previous_item = model.collection[current_index.row - 1]
42
-
43
- # copy the relevant fields
44
- current_index.entity.start = previous_item.end
45
- [:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
46
- current_index.entity.send( "#{attr.to_s}=", previous_item.send( attr ) )
47
- end
48
-
49
- # tell view to update
50
- top_left_index = model.create_index( current_index.row, 0 )
51
- bottom_right_index = model.create_index( current_index.row, current_index.column + view.builder.fields.size )
52
- view.dataChanged( top_left_index, bottom_right_index )
53
-
54
- # move to end time field
55
- view.override_next_index( model.create_index( current_index.row, view.builder.index( :end ) ) )
56
- end
57
- # don't let anybody else handle the keypress
58
- return true
59
-
60
- when event.ctrl? && event.i?
61
- invoice_from_project( current_index, view )
62
- # don't let anybody else handle the keypress
63
- return true
64
- end
65
- end
66
-
67
- # called when data is changed in this model's table view
68
- def self.data_changed( top_left, bottom_right, view )
69
- invoice_from_project( top_left, view ) if ( top_left == bottom_right )
70
- end
71
-
72
- def self.invoice_from_project( current_index, view )
73
- # auto-complete invoice number field from project
74
- current_field = current_index.attribute
75
- if current_field == :project && current_index.entity.project != nil
76
- # most recent entry, ordered in reverse
77
- invoice = current_index.entity.project.latest_invoice
78
-
79
- unless invoice.nil?
80
- # make a reference to the invoice
81
- current_index.entity.invoice = invoice
82
-
83
- # update view from top_left to bottom_right
84
- model = current_index.model
85
- changed_index = model.create_index( current_index.row, view.builder.index( :invoice ) )
86
- view.dataChanged( changed_index, changed_index )
87
-
88
- # move edit cursor to start time field
89
- view.override_next_index( model.create_index( current_index.row, view.builder.index( :start ) ) )
90
- end
91
- end
92
- end
93
- end
94
-
95
- class Project < Clevic::Record
96
- has_many :entries
97
-
98
- def self.build_table_model( model_builder )
99
- model_builder.instance_exec do
100
- plain :project
101
- plain :description
102
- distinct :client
103
- plain :rate
104
- plain :active
105
-
106
- records :order => 'project'
107
- end
108
- end
109
-
110
- # Return the latest invoice for this project
111
- # Not part of the UI.
112
- def latest_invoice
113
- Invoice.find(
114
- :first,
115
- :conditions => ["client = ? and status = 'not sent'", self.client],
116
- :order => 'invoice_number desc'
117
- )
118
- end
119
-
120
- end
121
-
122
- class Activity < Clevic::Record
123
- has_many :entries
124
-
125
- # define how fields are displayed
126
- def self.build_table_model( model_builder )
127
- model_builder.instance_exec do
128
- plain :activity
129
- plain :active
130
-
131
- records :order => 'activity'
132
- end
133
- end
134
- end
135
-
136
- class Invoice < Clevic::Record
137
- has_many :entries
138
-
139
- # define how fields are displayed
140
- def self.build_table_model( model_builder )
141
- model_builder.instance_exec do
142
- plain :date
143
- distinct :client
144
- plain :invoice_number
145
- restricted :status, :set => ['not sent', 'sent', 'paid', 'debt', 'writeoff', 'internal']
146
- restricted :billing, :set => %w{Hours Quote Internal}
147
- plain :quote_date
148
- plain :quote_amount
149
- plain :description
150
-
151
- records :order => 'invoice_number'
152
- end
153
- end
154
- end
@@ -8,25 +8,24 @@ Clevic::DbOptions.connect( $options ) do
8
8
  end
9
9
 
10
10
  # This is a read-only view, which is currently not implemented
11
- class Value < Clevic::Record
11
+ class Value < ActiveRecord::Base
12
12
  set_table_name 'values'
13
13
  #~ has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
14
14
  #~ has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
15
15
 
16
- def self.build_table_model( model_builder )
17
- model_builder.instance_exec do
18
- read_only!
19
- plain :date
20
- plain :description
21
- plain :debit
22
- plain :credit
23
- plain :pre_vat_amount
24
- plain :cheque_number
25
- plain :vat, :label => 'VAT'
26
- plain :financial_year
27
- plain :month
28
-
29
- records :order => 'date'
30
- end
16
+ include Clevic::Record
17
+ define_ui do
18
+ read_only!
19
+ plain :date
20
+ plain :description
21
+ plain :debit
22
+ plain :credit
23
+ plain :pre_vat_amount
24
+ plain :cheque_number
25
+ plain :vat, :label => 'VAT'
26
+ plain :financial_year
27
+ plain :month
28
+
29
+ records :order => 'date'
31
30
  end
32
31
  end
@@ -0,0 +1,138 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class PopulateCachePassengers < ActiveRecord::Migration
4
+ def self.up
5
+ Passenger.create :name => 'John Anderson', :flight => Flight.find_by_number('EK211'), :row => 36, :seat => 'A', :nationality => 'UAE'
6
+ Passenger.create :name => 'Genie', :flight => Flight.find_by_number('CA001'), :row => 1, :seat => 'A', :nationality => 'Canada'
7
+ Passenger.create :name => 'Aladdin', :flight => Flight.find_by_number('CA001'), :row => 2, :seat => 'A', :nationality => 'Canada'
8
+ end
9
+
10
+ def self.down
11
+ Passenger.delete :all
12
+ end
13
+ end
14
+
15
+ # need to set up a test DB, and test data for this
16
+ class TestCacheTable < Test::Unit::TestCase
17
+ def self.startup
18
+ PopulateCachePassengers.up
19
+ end
20
+
21
+ def self.shutdown
22
+ PopulateCachePassengers.down
23
+ end
24
+
25
+
26
+ def setup
27
+ @cache_table = CacheTable.new( Passenger )
28
+ end
29
+
30
+ def teardown
31
+ end
32
+
33
+ def test_passenger_count
34
+ assert_equal 3, Passenger.count
35
+ end
36
+
37
+ should "have a sql_count equal to number of records" do
38
+ assert_equal Passenger.count, @cache_table.sql_count
39
+ end
40
+
41
+ should "have a size equal to number of records" do
42
+ assert_equal Passenger.count, @cache_table.size
43
+ end
44
+
45
+ def test_cache_loading
46
+ # test not yet cached
47
+ (0...Passenger.count).each do |i|
48
+ assert @cache_table.cached_at?(i) == false, "record #{i} should not be cached yet"
49
+ end
50
+
51
+ # test cache retrieval
52
+ (0...Passenger.count).each do |i|
53
+ assert @cache_table[i] == Passenger.find( :first, :offset => i ), "#{i}th cached record is not #{i}th db record"
54
+ end
55
+ end
56
+
57
+ def test_preload_limit_1
58
+ @cache_table.preload_limit 1 do
59
+ assert !@cache_table[0].nil?, 'First object should not be nil'
60
+ (1...Passenger.count).each do |i|
61
+ assert !@cache_table.cached_at?(i), "#{i}th object should be nil"
62
+ end
63
+ end
64
+ end
65
+
66
+ # make sure preloads are done
67
+ def test_preload_limit_default
68
+ (0...Passenger.count).each do |i|
69
+ assert !@cache_table.cached_at?(i), "record #{i} should not be cached yet"
70
+ end
71
+ @cache_table[0]
72
+ (0...Passenger.count).each do |i|
73
+ assert @cache_table.cached_at?(i), "#{i}th object should not be nil"
74
+ end
75
+ end
76
+
77
+ should 'have id as a default order attribute' do
78
+ oa = OrderAttribute.new( Passenger, 'id' )
79
+ assert_equal oa, @cache_table.order_attributes[0]
80
+ end
81
+
82
+ def test_parse_order_attributes
83
+ order_string = 'name desc, passengers.nationality asc, row'
84
+ ct = CacheTable.new Passenger, :order => order_string
85
+ assert_equal OrderAttribute.new( Passenger, 'name desc' ), ct.order_attributes[0]
86
+ assert_equal OrderAttribute.new( Passenger, 'nationality' ), ct.order_attributes[1]
87
+ assert_equal OrderAttribute.new( Passenger, 'row asc' ), ct.order_attributes[2]
88
+ end
89
+
90
+ should 'not have new record on empty' do
91
+ # without auto_new
92
+ (0...Passenger.count).each do |i|
93
+ @cache_table.delete_at 0
94
+ @cache_table.delete_at 0
95
+ @cache_table.delete_at 0
96
+ end
97
+ assert_equal 0, @cache_table.size
98
+ end
99
+
100
+ should 'have new record on empty' do
101
+ #with auto_new
102
+ @cache_table = @cache_table.renew( :auto_new => true )
103
+ assert !@cache_table.options.has_key?( :auto_new ), "CacheTable should not have :auto_new in options"
104
+ (0...Passenger.count).each do |i|
105
+ @cache_table.delete_at 0
106
+ @cache_table.delete_at 0
107
+ @cache_table.delete_at 0
108
+ end
109
+
110
+ assert_equal 1, @cache_table.size
111
+ end
112
+
113
+ should 'return nil for a nil parameter' do
114
+ assert_nil @cache_table.index_for_entity( nil )
115
+ end
116
+
117
+ should 'return nil for an empty set' do
118
+ cache_table = @cache_table.renew( :conditions => "nationality = 'nothing'" )
119
+ assert_nil cache_table.index_for_entity( Passenger.find( :first ) )
120
+ end
121
+
122
+ def test_index_for_entity
123
+ # test in ascending order
124
+ first_passenger = Passenger.find :first
125
+ index = @cache_table.index_for_entity( first_passenger )
126
+ assert_equal 0, index, 'first passenger should have an index of 0'
127
+
128
+ # test in descending order
129
+ @cache_table = @cache_table.renew( :order => 'id desc' )
130
+ last_passenger = Passenger.find :first, :order => 'id desc'
131
+ assert_equal 0, @cache_table.index_for_entity( last_passenger ), "last passenger in reverse order should have an index of 0"
132
+
133
+ # test with two order fields
134
+ @cache_table = @cache_table.renew( :order => 'nationality, row' )
135
+ passenger = Passenger.find :first, :order => 'nationality, row'
136
+ assert_equal 0, @cache_table.index_for_entity( passenger ), "passenger in (nationality, row) order should have an index of 0"
137
+ end
138
+ end