clevic 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +12 -999
- data/Manifest.txt +1 -0
- data/README.txt +13 -10
- data/Rakefile +4 -3
- data/TODO +28 -13
- data/bin/clevic +64 -11
- data/config/hoe.rb +1 -1
- data/lib/clevic/browser.rb +86 -116
- data/lib/clevic/cache_table.rb +14 -4
- data/lib/clevic/db_options.rb +13 -8
- data/lib/clevic/delegates.rb +3 -1
- data/lib/clevic/extensions.rb +16 -0
- data/lib/clevic/field.rb +13 -3
- data/lib/clevic/field_builder.rb +42 -0
- data/lib/clevic/model_builder.rb +285 -92
- data/lib/clevic/record.rb +45 -2
- data/lib/clevic/table_model.rb +137 -40
- data/lib/clevic/table_view.rb +345 -150
- data/lib/clevic/ui/browser.ui +18 -73
- data/lib/clevic/ui/browser_ui.rb +57 -99
- data/lib/clevic/ui/search_dialog_ui.rb +51 -50
- data/lib/clevic/version.rb +1 -1
- data/models/accounts_models.rb +21 -26
- data/models/minimal_models.rb +2 -0
- data/models/times_models.rb +78 -79
- data/models/times_sqlite_models.rb +8 -8
- data/models/values_models.rb +7 -6
- data/website/index.html +24 -27
- metadata +4 -3
data/lib/clevic/cache_table.rb
CHANGED
@@ -130,11 +130,12 @@ class CacheTable < Array
|
|
130
130
|
# build case statement, including recusion
|
131
131
|
st = <<-EOF
|
132
132
|
case
|
133
|
-
when #{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
|
134
|
-
when #{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
|
133
|
+
when #{model_class.table_name}.#{quote_column attribute} #{operator} :#{attribute} then #{sql_boolean true}
|
134
|
+
when #{model_class.table_name}.#{quote_column attribute} = :#{attribute} then #{build_recursive_comparison( operator, index+1 )}
|
135
135
|
else #{sql_boolean false}
|
136
136
|
end
|
137
137
|
EOF
|
138
|
+
# indent
|
138
139
|
st.gsub!( /^/, ' ' * index )
|
139
140
|
end
|
140
141
|
|
@@ -168,8 +169,12 @@ EOF
|
|
168
169
|
end
|
169
170
|
|
170
171
|
# add an id to options[:order] if it's not in there
|
171
|
-
# also create @order_attributes
|
172
|
+
# also create @order_attributes, and @auto_new
|
172
173
|
def sanitise_options( options )
|
174
|
+
# save this for later
|
175
|
+
@auto_new = options[:auto_new]
|
176
|
+
options.delete :auto_new
|
177
|
+
|
173
178
|
options[:order] ||= ''
|
174
179
|
@order_attributes = options[:order].split( /, */ ).map{|x| OrderAttribute.new(@model_class, x)}
|
175
180
|
|
@@ -281,9 +286,14 @@ EOF
|
|
281
286
|
end
|
282
287
|
end
|
283
288
|
|
289
|
+
def auto_new?
|
290
|
+
@auto_new
|
291
|
+
end
|
292
|
+
|
293
|
+
# make sure there's always at least one empty record
|
284
294
|
def delete_at( index )
|
285
295
|
retval = super
|
286
|
-
if self.size == 0
|
296
|
+
if self.size == 0 && auto_new?
|
287
297
|
self << @model_class.new
|
288
298
|
end
|
289
299
|
retval
|
data/lib/clevic/db_options.rb
CHANGED
@@ -12,8 +12,8 @@ connection to a particular database. Like this:
|
|
12
12
|
end
|
13
13
|
|
14
14
|
When the block ends, a check is done to see that the :database key
|
15
|
-
exists. If not
|
16
|
-
ActiveRecord are performed.
|
15
|
+
exists. If not, an exception is thrown. Finally the relevant calls to
|
16
|
+
establish the ActiveRecord connection are performed.
|
17
17
|
|
18
18
|
Method calls are translated to insertions into a hash with the same
|
19
19
|
key as the method being called. The hash is initialised
|
@@ -25,9 +25,10 @@ class DbOptions
|
|
25
25
|
|
26
26
|
def initialize( options = nil )
|
27
27
|
@options = options || {}
|
28
|
+
|
28
29
|
# make sure the relevant entries exist, so method_missing works
|
29
30
|
@options[:adapter] ||= ''
|
30
|
-
@options[:host] ||= ''
|
31
|
+
@options[:host] ||= 'localhost'
|
31
32
|
@options[:username] ||= ''
|
32
33
|
@options[:password] ||= ''
|
33
34
|
@options[:database] ||= ''
|
@@ -61,6 +62,7 @@ class DbOptions
|
|
61
62
|
# end
|
62
63
|
# the block is evaluated in the context of the a new DbOptions
|
63
64
|
# object.
|
65
|
+
# TODO use instance_eval
|
64
66
|
def self.connect( args = nil, &block )
|
65
67
|
inst = self.new( args )
|
66
68
|
# using the Rails implementation, included in Qt
|
@@ -87,10 +89,13 @@ end
|
|
87
89
|
|
88
90
|
end
|
89
91
|
|
90
|
-
# workaround for the date freeze issue
|
91
|
-
|
92
|
-
|
93
|
-
|
92
|
+
# workaround for the date freeze issue, if it exists
|
93
|
+
begin
|
94
|
+
Date.new.freeze.to_s
|
95
|
+
rescue TypeError
|
96
|
+
class Date
|
97
|
+
def freeze
|
98
|
+
self
|
99
|
+
end
|
94
100
|
end
|
95
101
|
end
|
96
|
-
|
data/lib/clevic/delegates.rb
CHANGED
@@ -365,7 +365,9 @@ class RelationalDelegate < ComboDelegate
|
|
365
365
|
# send data to the editor
|
366
366
|
def setEditorData( editor, model_index )
|
367
367
|
if is_combo?( editor )
|
368
|
-
|
368
|
+
unless model_index.attribute_value.nil?
|
369
|
+
editor.current_index = editor.find_data( model_index.attribute_value.id.to_variant )
|
370
|
+
end
|
369
371
|
editor.line_edit.select_all
|
370
372
|
end
|
371
373
|
end
|
data/lib/clevic/extensions.rb
CHANGED
@@ -132,6 +132,22 @@ module Qt
|
|
132
132
|
|
133
133
|
attr_writer :entity
|
134
134
|
|
135
|
+
# return true if validation failed for this indexes field
|
136
|
+
def has_errors?
|
137
|
+
# virtual fields don't have metadata
|
138
|
+
if metadata.nil?
|
139
|
+
false
|
140
|
+
else
|
141
|
+
entity.errors.invalid?( field_name.to_sym )
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# return a collection of errors. Unlike AR, this
|
146
|
+
# will always return an array that will have zero, one
|
147
|
+
# or many elements.
|
148
|
+
def errors
|
149
|
+
[ entity.errors[field_name.to_sym] ].flatten
|
150
|
+
end
|
135
151
|
end
|
136
152
|
|
137
153
|
end
|
data/lib/clevic/field.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'qtext/flags.rb'
|
2
2
|
|
3
|
+
require 'clevic/field_builder.rb'
|
4
|
+
|
3
5
|
module Clevic
|
4
6
|
|
5
7
|
=begin rdoc
|
@@ -10,6 +12,8 @@ class Field
|
|
10
12
|
|
11
13
|
attr_accessor :attribute, :path, :label, :delegate, :class_name
|
12
14
|
attr_accessor :alignment, :format, :tooltip, :path_block
|
15
|
+
attr_accessor :visible
|
16
|
+
|
13
17
|
attr_writer :sample, :read_only
|
14
18
|
|
15
19
|
# attribute is the symbol for the attribute on the model_class
|
@@ -26,6 +30,7 @@ EOF
|
|
26
30
|
# set values
|
27
31
|
@attribute = attribute
|
28
32
|
@model_class = model_class
|
33
|
+
@visible = true
|
29
34
|
|
30
35
|
options.each do |key,value|
|
31
36
|
self.send( "#{key}=", value ) if respond_to?( "#{key}=" )
|
@@ -61,9 +66,8 @@ EOF
|
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
64
|
-
# Return the attribute value for the given entity, which
|
69
|
+
# Return the attribute value for the given entity, which will probably
|
65
70
|
# be an ActiveRecord instance
|
66
|
-
# entity is an ActiveRecord instance
|
67
71
|
def value_for( entity )
|
68
72
|
return nil if entity.nil?
|
69
73
|
transform_attribute( entity.send( attribute ) )
|
@@ -86,6 +90,11 @@ EOF
|
|
86
90
|
end
|
87
91
|
end
|
88
92
|
|
93
|
+
# return true if this is a field for a related table, false otherwise.
|
94
|
+
def is_association?
|
95
|
+
meta.type == ActiveRecord::Reflection::AssociationReflection
|
96
|
+
end
|
97
|
+
|
89
98
|
# return true if it's a date, a time or a datetime
|
90
99
|
# cache result because the type won't change in the lifetime of the field
|
91
100
|
def is_date_time?
|
@@ -93,7 +102,8 @@ EOF
|
|
93
102
|
end
|
94
103
|
|
95
104
|
# return ActiveRecord::Base.columns_hash[attribute]
|
96
|
-
# in other words an ActiveRecord::ConnectionAdapters::Column object
|
105
|
+
# in other words an ActiveRecord::ConnectionAdapters::Column object,
|
106
|
+
# or an ActiveRecord::Reflection::AssociationReflection object
|
97
107
|
def meta
|
98
108
|
@model_class.columns_hash[attribute.to_s] || @model_class.reflections[attribute]
|
99
109
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Clevic
|
2
|
+
|
3
|
+
class BlankSlate
|
4
|
+
keep_methods = %w( __send__ __id__ send class inspect instance_eval instance_variables )
|
5
|
+
instance_methods.each do |m|
|
6
|
+
undef_method(m) unless keep_methods.include?(m)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class FieldBuilder < BlankSlate
|
11
|
+
def initialize( hash = {} )
|
12
|
+
@hash = hash
|
13
|
+
end
|
14
|
+
|
15
|
+
# modified from Jim Freeze's article
|
16
|
+
def self.dsl_accessor(*symbols)
|
17
|
+
symbols.each do |sym|
|
18
|
+
line, st = __LINE__, <<EOF
|
19
|
+
def #{sym}(*val)
|
20
|
+
if val.empty?
|
21
|
+
@hash[#{sym.to_sym.inspect}]
|
22
|
+
else
|
23
|
+
@hash[#{sym.to_sym.inspect}] = val.size == 1 ? val[0] : val
|
24
|
+
end
|
25
|
+
end
|
26
|
+
EOF
|
27
|
+
class_eval st, __FILE__, line + 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# originally from Jim Freeze's article
|
32
|
+
def method_missing(sym, *args)
|
33
|
+
self.class.dsl_accessor sym
|
34
|
+
send(sym, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
@hash
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
data/lib/clevic/model_builder.rb
CHANGED
@@ -9,67 +9,110 @@ module Clevic
|
|
9
9
|
|
10
10
|
=begin rdoc
|
11
11
|
This is used to define a set of fields in a UI, any related tables,
|
12
|
-
restrictions on data entry, formatting and that kind of thing.
|
12
|
+
restrictions on data entry, formatting and that kind of thing. Essentially it
|
13
|
+
defines a DSL for building a TableModel.
|
13
14
|
|
14
15
|
Optional specifiers are:
|
15
16
|
* :sample is used to size the columns. Will default to some hopefully sensible value from the db.
|
16
17
|
* :format is something that can be understood by strftime (for time and date
|
17
|
-
fields) or understood by % (for everything else)
|
18
|
+
fields) or understood by % (for everything else). It can also be a Proc
|
19
|
+
that has one parameter - the current entity.
|
18
20
|
* :alignment is one of Qt::TextAlignmentRole, ie Qt::AlignRight, Qt::AlignLeft, Qt::AlignCenter
|
19
21
|
* :set is the set of strings that are accepted by a RestrictedDelegate
|
20
22
|
|
21
23
|
In the case of relational fields, all other options are passed to ActiveRecord::Base#find
|
22
24
|
|
23
|
-
For example,
|
25
|
+
For example, the UI for a model called Entry could be defined like this:
|
24
26
|
|
25
|
-
Clevic::
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
27
|
+
# inherit from Clevic::Record, which itself inherits from ActiveRecord::Base
|
28
|
+
class Entry < Clevic::Record
|
29
|
+
belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
|
30
|
+
belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
|
31
|
+
|
32
|
+
define_ui do
|
33
|
+
# :format is optional
|
34
|
+
plain :date, :format => '%d-%h-%y'
|
35
|
+
plain :start, :format => '%H:%M'
|
36
|
+
plain :amount, :format => '%.2f'
|
37
|
+
# :set is mandatory for a restricted field
|
38
|
+
restricted :vat, :label => 'VAT', :set => %w{ yes no all }, :tooltip => 'Is VAT included?'
|
39
|
+
|
40
|
+
# alternately with a block for readability
|
41
|
+
restricted :vat do
|
42
|
+
label 'VAT'
|
43
|
+
set %w{ yes no all }
|
44
|
+
tooltip 'Is VAT included?'
|
45
|
+
end
|
46
|
+
|
47
|
+
# distinct will show other values for this field in the combo
|
48
|
+
distinct :description, :conditions => 'now() - date <= interval( 1 year )'
|
49
|
+
|
50
|
+
# this is a read-only field
|
51
|
+
plain :origin, :read_only => true
|
52
|
+
|
53
|
+
# for these, :format will be a dotted attribute accessor for the related
|
54
|
+
# ActiveRecord entity, in this case an instance of Account
|
55
|
+
relational :debit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
|
56
|
+
relational :credit, :format => 'name', :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)'
|
57
|
+
|
58
|
+
# or like this to have an on-the-fly transform
|
59
|
+
# item will be an instance of Account
|
60
|
+
relational :credit, :format => lambda {|item| item.name.downcase}, :class_name => 'Account', :conditions => 'active = true', :order => 'lower(name)', :sample => 'Leilani Member Loan'
|
61
|
+
|
62
|
+
# this is a read-only display field from a related table
|
63
|
+
# the Entry class should then define a method called currency
|
64
|
+
# which returns an object that responds to 'short'.
|
65
|
+
# You can also use a Proc for :display
|
66
|
+
plain :currency, :display => 'short', :label => 'Currency'
|
67
|
+
|
68
|
+
# this is a read-only display field from a related table
|
69
|
+
# the Entry class should then define a method called currency
|
70
|
+
# which returns an object that responds to 'currency', which
|
71
|
+
# returns an object that responds to 'rate'.
|
72
|
+
# You can also use a Proc for :display
|
73
|
+
plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
|
74
|
+
|
75
|
+
# this is optional
|
76
|
+
records :order => 'date,start'
|
77
|
+
|
78
|
+
# could also be like this, where a..e are instances of Entry
|
79
|
+
records [ a,b,c,d,e ]
|
80
|
+
end
|
64
81
|
end
|
82
|
+
|
83
|
+
For ActiveRecord::Base classes, ModelBuilder knows how to build a
|
84
|
+
fairly sensible default UI. For small tweaks, something like this
|
85
|
+
can be used (where Subscriber is already defined elsewhere as a subclass
|
86
|
+
of ActiveRecord::Base):
|
87
|
+
class Subscriber
|
88
|
+
post_default_ui do
|
89
|
+
plain :password # this field does not exist in the DB
|
90
|
+
hide :password_salt # these should be hidden
|
91
|
+
hide :password_hash
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
Subclasses of Clevic::Record may also implement <tt>self.key_press_event( event, current_index, view )</tt>
|
96
|
+
and <tt>self.data_changed( top_left_index, bottom_right_index, view )</tt> methods so that
|
97
|
+
they can respond to editing events and do Neat Stuff.
|
65
98
|
=end
|
66
99
|
class ModelBuilder
|
67
|
-
# The collection of Clevic::Field objects
|
68
|
-
attr_reader :fields
|
69
100
|
|
70
|
-
|
71
|
-
|
101
|
+
# Create a definition for model_class (subclass of ActiveRecord::Base
|
102
|
+
# or Clevic::Record). Then execute block using self.instance_eval.
|
103
|
+
# The builder will construct a default TableModel from the model_class
|
104
|
+
# unless can_build_default == false
|
105
|
+
def initialize( model_class, can_build_default = true, &block )
|
106
|
+
@model_class = model_class
|
107
|
+
@auto_new = true
|
108
|
+
@read_only = false
|
72
109
|
@fields = []
|
110
|
+
init_from_model( model_class, can_build_default, &block )
|
111
|
+
end
|
112
|
+
|
113
|
+
# The collection of visible Clevic::Field objects
|
114
|
+
def fields
|
115
|
+
@fields.reject{|x| !x.visible}
|
73
116
|
end
|
74
117
|
|
75
118
|
# return the index of the named field
|
@@ -79,29 +122,48 @@ class ModelBuilder
|
|
79
122
|
retval
|
80
123
|
end
|
81
124
|
|
82
|
-
# the
|
83
|
-
|
84
|
-
|
125
|
+
# the ActiveRecord::Base or Clevic::Record class
|
126
|
+
attr_reader :model_class
|
127
|
+
|
128
|
+
# set read_only to true
|
129
|
+
def read_only!
|
130
|
+
@read_only = true
|
131
|
+
end
|
132
|
+
|
133
|
+
# should this table automatically show a new blank record?
|
134
|
+
def auto_new( bool )
|
135
|
+
@auto_new = bool
|
85
136
|
end
|
137
|
+
|
138
|
+
def auto_new?; @auto_new; end
|
86
139
|
|
87
140
|
# an ordinary field, edited in place with a text box
|
88
|
-
def plain( attribute, options = {} )
|
89
|
-
|
141
|
+
def plain( attribute, options = {}, &block )
|
142
|
+
# get values from block, if it's there
|
143
|
+
options = gather_block( options, &block )
|
144
|
+
|
145
|
+
read_only_default( attribute, options )
|
90
146
|
@fields << Clevic::Field.new( attribute.to_sym, model_class, options )
|
91
147
|
end
|
92
148
|
|
93
149
|
# edited with a combo box containing all previous entries in this field
|
94
|
-
def distinct( attribute, options = {} )
|
150
|
+
def distinct( attribute, options = {}, &block )
|
151
|
+
# get values from block, if it's there
|
152
|
+
options = gather_block( options, &block )
|
153
|
+
|
95
154
|
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
96
|
-
field.delegate = DistinctDelegate.new(
|
155
|
+
field.delegate = DistinctDelegate.new( nil, attribute, model_class, options )
|
97
156
|
@fields << field
|
98
157
|
end
|
99
158
|
|
100
159
|
# edited with a combo box, but restricted to a specified set
|
101
|
-
def restricted( attribute, options = {} )
|
160
|
+
def restricted( attribute, options = {}, &block )
|
161
|
+
# get values from block, if it's there
|
162
|
+
options = gather_block( options, &block )
|
163
|
+
|
102
164
|
raise "restricted must have a set" unless options.has_key?( :set )
|
103
165
|
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
104
|
-
field.delegate = RestrictedDelegate.new(
|
166
|
+
field.delegate = RestrictedDelegate.new( nil, attribute, model_class, options )
|
105
167
|
@fields << field
|
106
168
|
end
|
107
169
|
|
@@ -110,29 +172,24 @@ class ModelBuilder
|
|
110
172
|
# if options[:format] has a value, it's used either as a block
|
111
173
|
# or as a dotted path
|
112
174
|
def relational( attribute, options = {}, &block )
|
113
|
-
options[:display] ||= 'to_s'
|
114
175
|
unless options.has_key? :class_name
|
115
176
|
options[:class_name] = model_class.reflections[attribute].class_name || attribute.to_s.classify
|
116
177
|
end
|
117
|
-
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
118
178
|
|
119
|
-
|
179
|
+
# get values from block, if it's there
|
180
|
+
options = gather_block( options, &block )
|
181
|
+
|
182
|
+
# check after all possible options have been collected
|
183
|
+
raise ":display must be specified" if options[:display].nil?
|
184
|
+
|
185
|
+
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
186
|
+
field.delegate = RelationalDelegate.new( nil, field.attribute_path, options )
|
120
187
|
@fields << field
|
121
188
|
end
|
122
189
|
|
123
|
-
#
|
124
|
-
#
|
125
|
-
|
126
|
-
@fields.each do |field|
|
127
|
-
if field.delegate.class == RelationalDelegate
|
128
|
-
@options[:include] ||= []
|
129
|
-
@options[:include] << field.attribute
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
# mostly used in the create_model block, but may also be
|
135
|
-
# used as an accessor for records
|
190
|
+
# mostly used in the new block to define the set of records
|
191
|
+
# for the TableModel, but may also be
|
192
|
+
# used as an accessor for records.
|
136
193
|
def records( *args )
|
137
194
|
if args.size == 0
|
138
195
|
get_records
|
@@ -141,37 +198,159 @@ class ModelBuilder
|
|
141
198
|
end
|
142
199
|
end
|
143
200
|
|
144
|
-
#
|
145
|
-
#
|
146
|
-
def
|
201
|
+
# make sure this field doesn't show up
|
202
|
+
# mainly intended to be called after default_ui has been called
|
203
|
+
def hide( attribute )
|
204
|
+
field( attribute ).visible = false
|
205
|
+
end
|
206
|
+
|
207
|
+
# Build a default UI. All fields except the primary key are displayed
|
208
|
+
# as editable in the table. Any belongs_to relations are used to build
|
209
|
+
# combo boxes.
|
210
|
+
# Try to use a sensible :display option for the related class. In order:
|
211
|
+
# the name of the class, name, title, username
|
212
|
+
# order by the primary key. The class can use post_default_ui( &block )
|
213
|
+
# to do small tweaks.
|
214
|
+
def default_ui
|
215
|
+
# combine reflections and attributes into one set
|
216
|
+
reflections = model_class.reflections.keys.map{|x| x.to_s}
|
217
|
+
ui_columns = model_class.columns.reject{|x| x.name == model_class.primary_key }.map do |column|
|
218
|
+
# TODO there must be a better way to do this
|
219
|
+
att = column.name.gsub( /_id$/, '' )
|
220
|
+
if reflections.include?( att )
|
221
|
+
att
|
222
|
+
else
|
223
|
+
column.name
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# don't create an empty record, because sometimes there are
|
228
|
+
# validations that will cause trouble
|
229
|
+
auto_new false
|
230
|
+
|
231
|
+
# build columns
|
232
|
+
ui_columns.each do |column|
|
233
|
+
if model_class.reflections.has_key?( column.to_sym )
|
234
|
+
begin
|
235
|
+
reflection = model_class.reflections[column.to_sym]
|
236
|
+
if reflection.class == ActiveRecord::Reflection::AssociationReflection
|
237
|
+
# try to find a sensible display class. Default to to_s
|
238
|
+
related_class = reflection.class_name.constantize
|
239
|
+
display_method =
|
240
|
+
%w{#{model_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
|
241
|
+
related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
|
242
|
+
end
|
243
|
+
relational column.to_sym, :display => display_method
|
244
|
+
else
|
245
|
+
plain column.to_sym
|
246
|
+
end
|
247
|
+
rescue
|
248
|
+
puts $!.message
|
249
|
+
puts $!.backtrace
|
250
|
+
# just do a plain
|
251
|
+
puts "Doing plain for #{model_class}.#{column}"
|
252
|
+
plain column.to_sym
|
253
|
+
end
|
254
|
+
else
|
255
|
+
plain column.to_sym
|
256
|
+
end
|
257
|
+
end
|
258
|
+
records :order => model_class.primary_key
|
259
|
+
end
|
260
|
+
|
261
|
+
# return the named Clevic::Field object
|
262
|
+
def field( attribute )
|
263
|
+
@fields.find {|x| x.attribute == attribute }
|
264
|
+
end
|
265
|
+
|
266
|
+
# This takes all the information collected
|
267
|
+
# by the other methods, and returns the new TableModel
|
268
|
+
def build( table_view )
|
147
269
|
# build the model with all it's collections
|
148
270
|
# using @model here because otherwise the view's
|
149
271
|
# reference to this very same model is garbage collected.
|
150
|
-
|
151
|
-
@model =
|
152
|
-
@model.
|
153
|
-
@model.
|
154
|
-
@model.
|
155
|
-
@model.
|
156
|
-
|
272
|
+
@model = Clevic::TableModel.new( table_view )
|
273
|
+
@model.object_name = model_class.name
|
274
|
+
@model.model_class = model_class
|
275
|
+
@model.fields = @fields
|
276
|
+
@model.read_only = @read_only
|
277
|
+
@model.auto_new = auto_new?
|
278
|
+
|
279
|
+
# set parent for all delegates
|
280
|
+
fields.each {|x| x.delegate.parent = table_view unless x.delegate.nil? }
|
157
281
|
|
158
282
|
# the data
|
159
283
|
@model.collection = records
|
160
|
-
# fill in an empty record for data entry
|
161
|
-
@model.collection << model_class.new if @model.collection.size == 0
|
162
284
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
285
|
+
@model
|
286
|
+
end
|
287
|
+
|
288
|
+
private
|
289
|
+
|
290
|
+
def init_from_model( model_class, can_build_default, &block )
|
291
|
+
if model_class.respond_to?( :build_table_model )
|
292
|
+
# call build_table_model
|
293
|
+
method = model_class.method :build_table_model
|
294
|
+
method.call( builder )
|
295
|
+
elsif !model_class.define_ui_block.nil?
|
296
|
+
#define_ui is used, so use that block
|
297
|
+
instance_eval( &model_class.define_ui_block )
|
298
|
+
elsif can_build_default
|
299
|
+
# build a default UI
|
300
|
+
default_ui
|
301
|
+
|
302
|
+
# allow for smallish changes to a default build
|
303
|
+
instance_eval( &model_class.post_default_ui_block ) unless model_class.post_default_ui_block.nil?
|
304
|
+
end
|
305
|
+
|
306
|
+
# the local block adds to the previous definitions
|
307
|
+
unless block.nil?
|
308
|
+
if block.arity == 0
|
309
|
+
instance_eval( &block )
|
310
|
+
else
|
311
|
+
yield( builder )
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# add AR :include options for foreign keys, but it takes up too much memory,
|
317
|
+
# and actually takes longer to load a data set
|
318
|
+
def add_include_options
|
319
|
+
fields.each do |field|
|
320
|
+
if field.delegate.class == RelationalDelegate
|
321
|
+
@options[:include] ||= []
|
322
|
+
@options[:include] << field.attribute
|
323
|
+
end
|
167
324
|
end
|
168
|
-
|
169
|
-
# give the built model back to the view class
|
170
|
-
# see above comment about @model
|
171
|
-
@table_view.model = @model
|
172
325
|
end
|
173
326
|
|
174
|
-
|
327
|
+
# set a sensible read-only value if it isn't already
|
328
|
+
# specified in options doesn't alread
|
329
|
+
def read_only_default( attribute, options )
|
330
|
+
# sensible defaults for read-only-ness
|
331
|
+
options[:read_only] ||=
|
332
|
+
case
|
333
|
+
when options[:display].respond_to?( :call )
|
334
|
+
# it's a Proc or a Method, so we can't set it
|
335
|
+
true
|
336
|
+
|
337
|
+
when model_class.column_names.include?( options[:display].to_s )
|
338
|
+
# it's a DB column, so it's not read only
|
339
|
+
false
|
340
|
+
|
341
|
+
when model_class.reflections.include?( attribute )
|
342
|
+
# one-to-one relationships can be edited. many-to-one certainly can't
|
343
|
+
reflection = model_class.reflections[attribute]
|
344
|
+
reflection.macro != :has_one
|
345
|
+
|
346
|
+
when model_class.instance_methods.include?( attribute.to_s )
|
347
|
+
# read-only if there's no setter for the attribute
|
348
|
+
!model_class.instance_methods.include?( "#{attribute.to_s}=" )
|
349
|
+
else
|
350
|
+
# default to not read-only
|
351
|
+
false
|
352
|
+
end
|
353
|
+
end
|
175
354
|
|
176
355
|
# The collection of model objects to display in a table
|
177
356
|
# arg can either be a Hash, in which case a new CacheTable
|
@@ -191,10 +370,24 @@ private
|
|
191
370
|
def get_records
|
192
371
|
if @records.nil?
|
193
372
|
#~ add_include_options
|
373
|
+
@options[:auto_new] = auto_new?
|
194
374
|
@records = CacheTable.new( model_class, @options )
|
195
375
|
end
|
196
376
|
@records
|
197
377
|
end
|
378
|
+
# update options with the values in block, using FieldBuilder
|
379
|
+
# to evaluate block
|
380
|
+
|
381
|
+
def gather_block( options, &block )
|
382
|
+
unless block.nil?
|
383
|
+
fb = FieldBuilder.new( options )
|
384
|
+
fb.instance_eval( &block )
|
385
|
+
fb.to_hash
|
386
|
+
else
|
387
|
+
options
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
198
391
|
end
|
199
392
|
|
200
393
|
end
|