clevic 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +371 -0
- data/INSTALL +10 -0
- data/Manifest.txt +30 -0
- data/README.txt +94 -0
- data/Rakefile +100 -0
- data/TODO +131 -0
- data/accounts_models.rb +122 -0
- data/bin/clevic +64 -0
- data/lib/active_record/dirty.rb +87 -0
- data/lib/clevic.rb +4 -0
- data/lib/clevic/browser.rb +195 -0
- data/lib/clevic/cache_table.rb +281 -0
- data/lib/clevic/db_options.rb +21 -0
- data/lib/clevic/delegates.rb +383 -0
- data/lib/clevic/extensions.rb +133 -0
- data/lib/clevic/field.rb +181 -0
- data/lib/clevic/item_delegate.rb +62 -0
- data/lib/clevic/model_builder.rb +171 -0
- data/lib/clevic/model_column.rb +23 -0
- data/lib/clevic/search_dialog.rb +77 -0
- data/lib/clevic/table_model.rb +431 -0
- data/lib/clevic/table_view.rb +479 -0
- data/lib/clevic/ui/browser.ui +201 -0
- data/lib/clevic/ui/browser_ui.rb +176 -0
- data/lib/clevic/ui/icon.png +0 -0
- data/lib/clevic/ui/search_dialog.ui +216 -0
- data/lib/clevic/ui/search_dialog_ui.rb +106 -0
- data/sql/accounts.sql +302 -0
- data/sql/times.sql +197 -0
- data/times_models.rb +163 -0
- metadata +93 -0
data/Rakefile
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'hoe'
|
4
|
+
require 'lib/clevic/version.rb'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
Hoe.new( 'clevic', Clevic::VERSION ) do |s|
|
8
|
+
s.author = "John Anderson"
|
9
|
+
s.email = "john at semiosix dot com"
|
10
|
+
end
|
11
|
+
|
12
|
+
# generate a _ui.rb filename from a .ui filename
|
13
|
+
def ui_rb_file( ui_file )
|
14
|
+
ui_file.gsub( /\.ui$/, '_ui.rb' )
|
15
|
+
end
|
16
|
+
|
17
|
+
# list of .ui files
|
18
|
+
UI_FILES = FileList.new( 'lib/clevic/ui/*.ui' )
|
19
|
+
CLEAN.include( 'ChangeLog', 'lib/clevic/ui/*.rb' )
|
20
|
+
|
21
|
+
UI_FILES.each do |ui_file|
|
22
|
+
# make tasks to generate _ui.rb files
|
23
|
+
file ui_rb_file( ui_file ) => [ ui_file ] do |t|
|
24
|
+
sh "rbuic4 #{t.prerequisites} -o #{t.name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# make tasks to start designer when the ui file is named
|
28
|
+
desc "Start Qt designer with #{ui_file}"
|
29
|
+
namespace :ui do |n|
|
30
|
+
task Pathname.new(ui_file).basename.to_s.ext do |t|
|
31
|
+
sh "designer #{ui_file}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
desc 'Generate all _ui.rb files'
|
37
|
+
task :ui => UI_FILES.map{|x| ui_rb_file( x ) }
|
38
|
+
|
39
|
+
namespace :ui do
|
40
|
+
desc 'Start Qt designer with the argument, or all .ui files.'
|
41
|
+
task :design do |t|
|
42
|
+
ARGV.shift()
|
43
|
+
if ARGV.size == 0
|
44
|
+
# start designer with all ui files
|
45
|
+
sh "designer #{UI_FILES.join(' ')}"
|
46
|
+
else
|
47
|
+
# start designer with all files that match an argument
|
48
|
+
sh "designer #{ ARGV.map{|x| UI_FILES.grep( /\/#{x}/ ) }.join(' ') }"
|
49
|
+
end
|
50
|
+
true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "Runs Clevic in warning mode, with test databases and debug flag on"
|
55
|
+
task :run => :ui do |t|
|
56
|
+
ARGV.shift()
|
57
|
+
exec "ruby -w -Ilib bin/clevic -D #{ARGV.join(' ')}"
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "Runs Clevic in debug mode, with test databases"
|
61
|
+
task :debug => :ui do |t|
|
62
|
+
ARGV.shift()
|
63
|
+
exec "ruby -w -rdebug -Ilib bin/clevic -D #{ARGV.join(' ')}"
|
64
|
+
end
|
65
|
+
|
66
|
+
desc "irb in this project's context"
|
67
|
+
task :irb do |t|
|
68
|
+
ARGV.shift()
|
69
|
+
ENV['RUBYLIB'] += ":#{File.expand_path('.')}/lib"
|
70
|
+
exec "irb -Ilib -rclevic"
|
71
|
+
end
|
72
|
+
|
73
|
+
# generate tasks for all model definition files
|
74
|
+
MODELS_LIST = FileList.new( '**/*models.rb' )
|
75
|
+
|
76
|
+
def short_model( model_file )
|
77
|
+
Pathname.new( model_file ).basename.to_s.gsub( /_models.rb/, '' )
|
78
|
+
end
|
79
|
+
|
80
|
+
MODELS_LIST.each do |model_file|
|
81
|
+
# generate irb contexts
|
82
|
+
desc "irb with #{model_file}"
|
83
|
+
namespace :irb do
|
84
|
+
task short_model( model_file ) do |t|
|
85
|
+
ARGV.shift()
|
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
|
+
desc "run clevic with #{model_file}"
|
94
|
+
task short_model( model_file ) => :ui do |t|
|
95
|
+
ARGV.shift()
|
96
|
+
exec "ruby -w -Ilib bin/clevic -D #{model_file} #{ARGV.join(' ')}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
task :package => :ui
|
data/TODO
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
Times Ctrl-Shift-" should not copy date if it already exists, and should not copy time if it's a different date.
|
2
|
+
Times Ctrl-Shift-" after it's done, tab doesn't change fields
|
3
|
+
Times look up invoice for project leaves the wrong fields highlighted, and focus in the wrong field.
|
4
|
+
|
5
|
+
Using F4 to open list, and then selecting from the combo and exiting using Return (or tab?) doesn't set the correct value
|
6
|
+
wrap description, and allow Access-style zooming
|
7
|
+
Undo deletes and other commands. Possibly via ActiveRecord callbacks.
|
8
|
+
|
9
|
+
Keep a history of changes, ie xy, new. xy, changed. x,y copied etc.
|
10
|
+
|
11
|
+
OSX
|
12
|
+
---
|
13
|
+
Check that qtruby4 runs on OSX and so does Clevic. It does. Very slowly on Leilani's Mini.
|
14
|
+
|
15
|
+
windows
|
16
|
+
-------
|
17
|
+
Ctrl-; date formatting goes 07--08 instead of 07-Apr-08. But typing the full month will be OK.
|
18
|
+
|
19
|
+
empty database
|
20
|
+
--------------
|
21
|
+
resize fields for first record, while it's being entered. use Qt::ExpandingLineEdit for ComboDelegate? Doesn't exist in Ruby bindings
|
22
|
+
|
23
|
+
editing
|
24
|
+
-------
|
25
|
+
F2 for standard edit, F4 for calendar edit
|
26
|
+
Only move for data_changed if field was exited with tab, not enter.
|
27
|
+
make sure record is saved when changing tabs
|
28
|
+
tooltips for tabs
|
29
|
+
Help in general for new data capture people
|
30
|
+
Help to right of tabs
|
31
|
+
messages for wrong dates etc
|
32
|
+
numeric months
|
33
|
+
|
34
|
+
Combos
|
35
|
+
------
|
36
|
+
shortlist combos by prefix. See Qt Examples.
|
37
|
+
turn on/off smart filters for relational delegates. Like selecting only distincts in the last year.
|
38
|
+
context menu for delegates, ie sort order, last used, etc
|
39
|
+
|
40
|
+
shortcut sets, depending on which OS you're used. use QKeyEvent::matches ( QKeySequence::StandardKey key )
|
41
|
+
|
42
|
+
Doing data capture, sort by id, but unfilter reverts to date/id rather than entry order
|
43
|
+
optional warnings for back-dated entries. Highlighting
|
44
|
+
make sure bsearch easier to install
|
45
|
+
|
46
|
+
ORDER BY allow functions, ie lower(project)
|
47
|
+
handle db errors
|
48
|
+
test with sqlite
|
49
|
+
easier way to run models, search LOAD_PATH
|
50
|
+
|
51
|
+
generate models - DrySQL
|
52
|
+
Ctrl-PgDn to last row in this column. Also extend selection
|
53
|
+
sorting by header. See void QAbstractItemModel::sort ( int column, Qt::SortOrder order = Qt::AscendingOrder )
|
54
|
+
- layoutChanged
|
55
|
+
|
56
|
+
search with acts_as_searchable and hyperestraier
|
57
|
+
acts_as_trashable to undo deletes
|
58
|
+
implement undo of field changes
|
59
|
+
|
60
|
+
cache belongs_to associations, during loading, ie don't affect ability to pick up
|
61
|
+
changes more or less instantly.
|
62
|
+
|
63
|
+
moving of columns
|
64
|
+
/-style keyboard search by selected column, or everything if no column selected
|
65
|
+
/-style filtering?
|
66
|
+
|
67
|
+
for dates, add year if not specified, with 6 months on either side range. Configurable?
|
68
|
+
value formatting not in model
|
69
|
+
copy a field from a mouse-selection (ctrl-b maybe)
|
70
|
+
hiding of fields
|
71
|
+
|
72
|
+
save context menu settings, filter settings, etc
|
73
|
+
filtering by various things. http://doc.trolltech.com/4.3/qsortfilterproxymodel.html
|
74
|
+
highlighting by various things
|
75
|
+
cut and paste (in model and csv format)
|
76
|
+
|
77
|
+
drop cached model objects from CacheTable when they're not in use
|
78
|
+
|
79
|
+
allow scroll viewport to centre when at end of dataset
|
80
|
+
QAbstractItemView::ScrollHint
|
81
|
+
|
82
|
+
\value EnsureVisible Scroll to ensure that the item is visible.
|
83
|
+
\value PositionAtTop Scroll to position the item at the top of the viewport.
|
84
|
+
\value PositionAtBottom Scroll to position the item at the bottom of the viewport.
|
85
|
+
\value PositionAtCenter Scroll to position the item at the center of the viewport.
|
86
|
+
|
87
|
+
|
88
|
+
preferences
|
89
|
+
-----------
|
90
|
+
store previous searches, by model & app
|
91
|
+
|
92
|
+
|
93
|
+
maybe
|
94
|
+
-----
|
95
|
+
use rubigen for creating apps
|
96
|
+
allow moving of rows
|
97
|
+
discontiguous copying of entities/csv
|
98
|
+
multi-row copying
|
99
|
+
pasting of csv, into rectangular regions
|
100
|
+
collect a set of data requests to the model, and do them in one SQL query. See EntryTableView#moveCursor
|
101
|
+
Use SQL cursors for find & find_next?
|
102
|
+
|
103
|
+
Accounts
|
104
|
+
--------
|
105
|
+
paste of "common" records with different dates
|
106
|
+
restricted type for Account Type record
|
107
|
+
|
108
|
+
Times
|
109
|
+
-----
|
110
|
+
|
111
|
+
warnings on overlap times (in status bar)
|
112
|
+
warnings on large intervals (in status bar)
|
113
|
+
|
114
|
+
db
|
115
|
+
--
|
116
|
+
|
117
|
+
times
|
118
|
+
|
119
|
+
alter table invoices rename column type to billing;
|
120
|
+
alter table entries add primary key (id);
|
121
|
+
alter table entries alter column id set not null
|
122
|
+
select max(id) from entries;
|
123
|
+
create sequence entries_id_seq start with 11694;
|
124
|
+
alter table entries alter column id set default nextval('entries_id_seq');
|
125
|
+
update entries set module = null where module = '';
|
126
|
+
update entries set module = 'admin' where module = 'Admin';
|
127
|
+
|
128
|
+
alter table projects rename active to old_active;
|
129
|
+
alter table projects add column active boolean;
|
130
|
+
update projects set active = ( old_active = 1 );
|
131
|
+
alter table projects drop column old_active;
|
data/accounts_models.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'clevic.rb'
|
2
|
+
|
3
|
+
# db connection options
|
4
|
+
$options ||= {}
|
5
|
+
$options[:database] ||= $options[:debug] ? 'accounts_test' : 'accounts'
|
6
|
+
$options[:adapter] ||= 'postgresql'
|
7
|
+
$options[:host] ||= 'localhost'
|
8
|
+
$options[:username] ||= 'panic'
|
9
|
+
$options[:password] ||= ''
|
10
|
+
|
11
|
+
class Entry < ActiveRecord::Base
|
12
|
+
include ActiveRecord::Dirty
|
13
|
+
belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
|
14
|
+
belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
|
15
|
+
|
16
|
+
# define how fields will be displayed
|
17
|
+
def self.ui( parent )
|
18
|
+
Clevic::TableView.new( self, parent ).create_model do
|
19
|
+
plain :date, :sample => '88-WWW-99'
|
20
|
+
distinct :description, :conditions => "now() - date <= '1 year'", :sample => 'm' * 26, :frequency => true
|
21
|
+
relational :debit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
|
22
|
+
relational :credit, 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
|
23
|
+
plain :amount, :sample => 999999.99
|
24
|
+
distinct :category
|
25
|
+
plain :cheque_number
|
26
|
+
plain :active, :sample => 'WW'
|
27
|
+
plain :vat, :label => 'VAT', :sample => 'WW', :tooltip => 'Does this include VAT?'
|
28
|
+
|
29
|
+
records :order => 'date, id'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# called when data is changed in the UI
|
34
|
+
def self.data_changed( top_left, bottom_right, view )
|
35
|
+
if top_left == bottom_right
|
36
|
+
update_credit_debit( top_left, view )
|
37
|
+
else
|
38
|
+
puts "top_left: #{top_left.inspect}"
|
39
|
+
puts "bottom_right: #{bottom_right.inspect}"
|
40
|
+
puts "can't do data_changed for a range"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# check that the current field is :descriptions, then
|
45
|
+
# copy the values for the credit and debit fields
|
46
|
+
# from the previous similar entry
|
47
|
+
def self.update_credit_debit( current_index, view )
|
48
|
+
return if !current_index.valid?
|
49
|
+
current_field = current_index.attribute
|
50
|
+
if current_field == :description
|
51
|
+
# most recent entry, ordered in reverse
|
52
|
+
similar = self.find(
|
53
|
+
:first,
|
54
|
+
:conditions => ["#{current_field} = ?", 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 update view from top_left to bottom_right
|
64
|
+
model = current_index.model
|
65
|
+
top_left_index = model.create_index( current_index.row, 0 )
|
66
|
+
bottom_right_index = model.create_index( current_index.row, view.builder.fields.size )
|
67
|
+
view.dataChanged( top_left_index, bottom_right_index )
|
68
|
+
|
69
|
+
# move edit cursor to amount field
|
70
|
+
view.selection_model.clear
|
71
|
+
view.override_next_index( model.create_index( current_index.row, view.builder.index( :amount ) ) )
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Account < ActiveRecord::Base
|
78
|
+
include ActiveRecord::Dirty
|
79
|
+
has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
|
80
|
+
has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
|
81
|
+
|
82
|
+
# define how fields are displayed
|
83
|
+
def self.ui( parent )
|
84
|
+
Clevic::TableView.new( self, parent ).create_model do
|
85
|
+
plain :name
|
86
|
+
restricted :vat, :label => 'VAT', :set => %w{ yes no all }
|
87
|
+
plain :account_type
|
88
|
+
plain :pastel_number, :alignment => Qt::AlignRight, :label => 'Pastel'
|
89
|
+
plain :fringe, :format => "%.1f"
|
90
|
+
plain :active
|
91
|
+
|
92
|
+
records :order => 'name,account_type'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# order of tab display
|
98
|
+
$options[:models] = [ Entry, Account ]
|
99
|
+
|
100
|
+
# This is a read-only view, which is currently not implemented
|
101
|
+
#~ class Values < ActiveRecord::Base
|
102
|
+
#~ include ActiveRecord::Dirty
|
103
|
+
#~ set_table_name 'values'
|
104
|
+
#~ has_many :debits, :class_name => 'Entry', :foreign_key => 'debit_id'
|
105
|
+
#~ has_many :credits, :class_name => 'Entry', :foreign_key => 'credit_id'
|
106
|
+
#~ def self.ui( parent )
|
107
|
+
#~ Clevic::TableView.new( self, parent ).create_model do
|
108
|
+
#~ readonly
|
109
|
+
#~ plain :date
|
110
|
+
#~ plain :description
|
111
|
+
#~ plain :debit
|
112
|
+
#~ plain :credit
|
113
|
+
#~ plain :pre_vat_amount
|
114
|
+
#~ plain :cheque_number
|
115
|
+
#~ plain :vat, :label => 'VAT'
|
116
|
+
#~ plain :financial_year
|
117
|
+
#~ plain :month
|
118
|
+
|
119
|
+
#~ records :order => 'date'
|
120
|
+
#~ end
|
121
|
+
#~ end
|
122
|
+
#~ end
|
data/bin/clevic
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#! /usr/bin/ruby
|
2
|
+
|
3
|
+
require 'clevic/browser.rb'
|
4
|
+
require 'ruby-debug'
|
5
|
+
|
6
|
+
# fetch command line options
|
7
|
+
require 'optparse'
|
8
|
+
|
9
|
+
# find and require variations on file_path
|
10
|
+
def require_if( file_path )
|
11
|
+
require file_path if File.exist?( file_path ) || File.exist?( file_path + '.rb' )
|
12
|
+
end
|
13
|
+
|
14
|
+
$options = {}
|
15
|
+
oparser = OptionParser.new
|
16
|
+
oparser.on( '-H', '--host HOST', 'RDBMS host', String ) { |o| $options[:host] = o }
|
17
|
+
oparser.on( '-u', '--user USERNAME', String ) { |o| $options[:user] = o }
|
18
|
+
oparser.on( '-p', '--pass PASSWORD', String ) { |o| $options[:password] = o }
|
19
|
+
oparser.on( '-t', '--table TABLE', 'Table to display', String ) { |o| $options[:table] = o }
|
20
|
+
oparser.on( '-d', '--database DATABASE', 'Database name', String ) { |o| $options[:database] = o }
|
21
|
+
oparser.on( '-D', '--debug' ) { |o| $options[:debug] = true }
|
22
|
+
oparser.on( '-v', '--verbose' ) { |o| $options[:verbose] = true }
|
23
|
+
oparser.on( '-h', '-?', '--help' ) do |o|
|
24
|
+
puts oparser.to_s
|
25
|
+
exit( 0 )
|
26
|
+
end
|
27
|
+
|
28
|
+
args = oparser.parse( ARGV )
|
29
|
+
|
30
|
+
if args.size > 0
|
31
|
+
$options[:definition] = args.shift
|
32
|
+
require_if "#{$options[:definition]}_models"
|
33
|
+
require_if $options[:definition]
|
34
|
+
else
|
35
|
+
raise "no model definition file specified"
|
36
|
+
end
|
37
|
+
|
38
|
+
app = Qt::Application.new( args )
|
39
|
+
|
40
|
+
if $options[:debug]
|
41
|
+
require 'pp'
|
42
|
+
#~ puts "$options: #{$options.inspect}"
|
43
|
+
#~ puts args.inspect
|
44
|
+
end
|
45
|
+
|
46
|
+
if !$options.has_key?( :database )
|
47
|
+
raise "Please define $options[:database]"
|
48
|
+
end
|
49
|
+
|
50
|
+
# connect to db
|
51
|
+
require 'clevic/db_options.rb'
|
52
|
+
|
53
|
+
puts "using database #{ActiveRecord::Base.connection.raw_connection.db}" if $options[:debug]
|
54
|
+
|
55
|
+
# show UI
|
56
|
+
main_window = Qt::MainWindow.new
|
57
|
+
browser = Clevic::Browser.new( main_window )
|
58
|
+
browser.open
|
59
|
+
# this must come after Clevic::Browser.new
|
60
|
+
main_window.window_title = $options[:database]
|
61
|
+
main_window.show
|
62
|
+
# make sure any partially edited records are saved when the window is closed
|
63
|
+
app.connect( SIGNAL('lastWindowClosed()') ) { browser.save_all }
|
64
|
+
app.exec
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# Track unsaved changes.
|
3
|
+
module Dirty
|
4
|
+
def self.included(base)
|
5
|
+
base.attribute_method_suffix '_changed?', '_change', '_original'
|
6
|
+
base.alias_method_chain :read_attribute, :dirty
|
7
|
+
base.alias_method_chain :write_attribute, :dirty
|
8
|
+
base.alias_method_chain :save, :dirty
|
9
|
+
end
|
10
|
+
|
11
|
+
# Do any attributes have unsaved changes?
|
12
|
+
# person.changed? # => false
|
13
|
+
# person.name = 'bob'
|
14
|
+
# person.changed? # => true
|
15
|
+
def changed?
|
16
|
+
!changed_attributes.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
# List of attributes with unsaved changes.
|
20
|
+
# person.changed # => []
|
21
|
+
# person.name = 'bob'
|
22
|
+
# person.changed # => ['name']
|
23
|
+
def changed
|
24
|
+
changed_attributes.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
# Map of changed attrs => [original value, new value]
|
28
|
+
# person.changes # => {}
|
29
|
+
# person.name = 'bob'
|
30
|
+
# person.changes # => { 'name' => ['bill', 'bob'] }
|
31
|
+
def changes
|
32
|
+
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Clear changed attributes after they are saved.
|
37
|
+
def save_with_dirty(*args) #:nodoc:
|
38
|
+
save_without_dirty(*args)
|
39
|
+
ensure
|
40
|
+
changed_attributes.clear
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
# Map of change attr => original value.
|
45
|
+
def changed_attributes
|
46
|
+
@changed_attributes ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Wrap read_attribute to freeze its result.
|
51
|
+
def read_attribute_with_dirty(attr)
|
52
|
+
read_attribute_without_dirty(attr).freeze
|
53
|
+
end
|
54
|
+
|
55
|
+
# Wrap write_attribute to remember original attribute value.
|
56
|
+
def write_attribute_with_dirty(attr, value)
|
57
|
+
attr = attr.to_s
|
58
|
+
|
59
|
+
# The attribute already has an unsaved change.
|
60
|
+
unless changed_attributes.include?(attr)
|
61
|
+
old = read_attribute(attr)
|
62
|
+
|
63
|
+
# Remember the original value if it's different.
|
64
|
+
changed_attributes[attr] = old unless old == value
|
65
|
+
end
|
66
|
+
|
67
|
+
# Carry on.
|
68
|
+
write_attribute_without_dirty(attr, value)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Handle *_changed? for method_missing.
|
73
|
+
def attribute_changed?(attr)
|
74
|
+
changed_attributes.include?(attr)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Handle *_change for method_missing.
|
78
|
+
def attribute_change(attr)
|
79
|
+
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Handle *_original for method_missing.
|
83
|
+
def attribute_original(attr)
|
84
|
+
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|