clevic 0.7.0 → 0.8.0
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 +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
|