clevic 0.8.0 → 0.11.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.
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