clevic 0.8.0 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/Manifest.txt +13 -10
- data/README.txt +6 -9
- data/Rakefile +35 -24
- data/TODO +29 -17
- data/bin/clevic +84 -37
- data/config/hoe.rb +7 -3
- data/lib/clevic.rb +2 -4
- data/lib/clevic/browser.rb +37 -49
- data/lib/clevic/cache_table.rb +55 -165
- data/lib/clevic/db_options.rb +32 -21
- data/lib/clevic/default_view.rb +66 -0
- data/lib/clevic/delegates.rb +51 -67
- data/lib/clevic/dirty.rb +101 -0
- data/lib/clevic/extensions.rb +24 -38
- data/lib/clevic/field.rb +400 -99
- data/lib/clevic/item_delegate.rb +32 -33
- data/lib/clevic/model_builder.rb +315 -148
- data/lib/clevic/order_attribute.rb +53 -0
- data/lib/clevic/record.rb +57 -57
- data/lib/clevic/search_dialog.rb +71 -67
- data/lib/clevic/sql_dialects.rb +33 -0
- data/lib/clevic/table_model.rb +73 -120
- data/lib/clevic/table_searcher.rb +165 -0
- data/lib/clevic/table_view.rb +140 -100
- data/lib/clevic/ui/.gitignore +1 -0
- data/lib/clevic/ui/browser_ui.rb +55 -56
- data/lib/clevic/ui/search_dialog_ui.rb +50 -51
- data/lib/clevic/version.rb +2 -2
- data/lib/clevic/view.rb +89 -0
- data/models/accounts_models.rb +12 -9
- data/models/minimal_models.rb +4 -2
- data/models/times_models.rb +41 -25
- data/models/times_sqlite_models.rb +1 -145
- data/models/values_models.rb +15 -16
- data/test/test_cache_table.rb +138 -0
- data/test/test_helper.rb +131 -0
- data/test/test_model_index_extensions.rb +22 -0
- data/test/test_order_attribute.rb +62 -0
- data/test/test_sql_dialects.rb +77 -0
- data/test/test_table_searcher.rb +188 -0
- metadata +36 -20
- data/bin/import-times +0 -128
- data/config/jamis.rb +0 -589
- data/env.sh +0 -1
- data/lib/active_record/dirty.rb +0 -87
- data/lib/clevic/field_builder.rb +0 -42
- data/website/index.html +0 -170
- data/website/index.txt +0 -17
- data/website/screenshot.png +0 -0
- data/website/stylesheets/screen.css +0 -131
- data/website/template.html.erb +0 -41
data/models/minimal_models.rb
CHANGED
@@ -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 <
|
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 <
|
20
|
+
class Account < ActiveRecord::Base
|
21
|
+
include Clevic::Record
|
20
22
|
def to_s; name; end
|
21
23
|
end
|
data/models/times_models.rb
CHANGED
@@ -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 <
|
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
|
-
|
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
|
-
|
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
|
-
|
65
|
+
current_index.entity.start = previous_item.end
|
61
66
|
[:date, :project, :invoice, :activity, :module, :charge, :person].each do |attr|
|
62
|
-
|
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 =
|
67
|
-
bottom_right_index =
|
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.
|
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
|
-
|
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.
|
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 <
|
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 <
|
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 <
|
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
|
-
|
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
|
data/models/values_models.rb
CHANGED
@@ -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 <
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|