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