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/lib/clevic/item_delegate.rb
CHANGED
@@ -11,53 +11,52 @@ end
|
|
11
11
|
module Clevic
|
12
12
|
|
13
13
|
class ItemDelegate < Qt::ItemDelegate
|
14
|
+
attr_reader :field
|
14
15
|
|
15
|
-
def initialize( parent )
|
16
|
-
super
|
16
|
+
def initialize( parent, field )
|
17
|
+
super( parent )
|
18
|
+
@field = field
|
17
19
|
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
def editorEvent ( event, model, style_option_view_item, model_index )
|
22
|
-
#~ if $options[:debug]
|
23
|
-
#~ puts "editorEvent"
|
24
|
-
#~ puts "event: #{event.inspect}"
|
25
|
-
#~ puts "model: #{model.inspect}"
|
26
|
-
#~ puts "style_option_view_item: #{style_option_view_item.inspect}"
|
27
|
-
#~ puts "model_index: #{model_index.inspect}"
|
28
|
-
#~ end
|
29
|
-
super
|
21
|
+
def attribute
|
22
|
+
field.attribute
|
30
23
|
end
|
31
24
|
|
32
|
-
def
|
33
|
-
|
34
|
-
# not going to work here because being triggered by
|
35
|
-
# an alphanumeric keystroke (as opposed to F4)
|
36
|
-
# will result in the calendar widget being opened.
|
37
|
-
#~ Qt::CalendarWidget.new( parent_widget )
|
38
|
-
super
|
39
|
-
else
|
40
|
-
super
|
41
|
-
end
|
25
|
+
def entity_class
|
26
|
+
field.entity_class
|
42
27
|
end
|
43
28
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#~ def setModelData( editor, abstract_item_model, model_index )
|
49
|
-
#~ model_index.gui_value = editor.value
|
50
|
-
#~ emit abstract_item_model.dataChanged( model_index, model_index )
|
51
|
-
#~ end
|
29
|
+
def find_options
|
30
|
+
field.find_options
|
31
|
+
end
|
52
32
|
|
33
|
+
# Figure out where to put the editor widget, taking into
|
34
|
+
# account the sizes of the headers
|
53
35
|
def updateEditorGeometry( editor, style_option_view_item, model_index )
|
54
|
-
# figure out where to put the editor widget, taking into
|
55
|
-
# account the sizes of the headers
|
56
36
|
rect = style_option_view_item.rect
|
57
37
|
rect.set_width( [editor.size_hint.width,rect.width].max )
|
58
38
|
rect.set_height( editor.size_hint.height )
|
59
39
|
editor.set_geometry( rect )
|
60
40
|
end
|
41
|
+
|
42
|
+
# This catches the event that begins the edit process.
|
43
|
+
#~ def editorEvent ( event, model, style_option_view_item, model_index )
|
44
|
+
#~ end
|
45
|
+
|
46
|
+
# This is called when one of the EditTriggers is pressed. So
|
47
|
+
# it's only good for opening a generic keystroke editor, not
|
48
|
+
# a specific one, eg a calendar-style date editor.
|
49
|
+
#~ def createEditor( parent_widget, style_option_view_item, model_index )
|
50
|
+
#~ end
|
51
|
+
|
52
|
+
# Set the data for the given editor widget
|
53
|
+
#~ def setEditorData( editor_widget, model_index )
|
54
|
+
#~ end
|
55
|
+
|
56
|
+
# Set the data for the given model and index from the given
|
57
|
+
#~ def setModelData( editor_widget, abstract_item_model, model_index )
|
58
|
+
#~ end
|
59
|
+
|
61
60
|
end
|
62
61
|
|
63
62
|
end
|
data/lib/clevic/model_builder.rb
CHANGED
@@ -8,30 +8,57 @@ require 'clevic/field.rb'
|
|
8
8
|
module Clevic
|
9
9
|
|
10
10
|
=begin rdoc
|
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. Essentially it
|
13
|
-
defines a DSL for building a TableModel.
|
14
11
|
|
15
|
-
|
16
|
-
* :sample is used to size the columns. Will default to some hopefully sensible value from the db.
|
17
|
-
* :format is something that can be understood by strftime (for time and date
|
18
|
-
fields) or understood by % (for everything else). It can also be a Proc
|
19
|
-
that has one parameter - the current entity.
|
20
|
-
* :alignment is one of Qt::TextAlignmentRole, ie Qt::AlignRight, Qt::AlignLeft, Qt::AlignCenter
|
21
|
-
* :set is the set of strings that are accepted by a RestrictedDelegate
|
12
|
+
== View definition
|
22
13
|
|
23
|
-
|
14
|
+
Clevic::ModelBuilder defines the DSL used to create a UI definition (which is
|
15
|
+
actually a set of Clevic::Field instances), including any related tables,
|
16
|
+
restrictions on data entry, formatting and so on. The intention was to make
|
17
|
+
specifying a UI as painless as possible, with framework overhead only where
|
18
|
+
you need it.
|
24
19
|
|
25
|
-
|
20
|
+
To that end, there are 2 ways to define UIs:
|
26
21
|
|
27
|
-
|
28
|
-
|
22
|
+
- an Embedded View as part of the ActiveRecord object (which is useful if you
|
23
|
+
want minimal framework overhead). Just show me the data, dammit.
|
24
|
+
|
25
|
+
- a Separate View in a separate class (which is useful when you want several
|
26
|
+
diffent views of the same underlying table). I want a neato-nifty UI that does
|
27
|
+
(relatively) complex things.
|
28
|
+
|
29
|
+
I've tried to consistently refer to an instance of an ActiveRecord::Base subclass
|
30
|
+
as an 'entity'.
|
31
|
+
|
32
|
+
==Embedded View
|
33
|
+
Minimal embedded definition is
|
34
|
+
|
35
|
+
class Position < ActiveRecord::Base
|
36
|
+
include Clevic::Record
|
37
|
+
end
|
38
|
+
|
39
|
+
which will build a fairly sensible default UI from the
|
40
|
+
entity's metadata. Obviously you can use open classes to do
|
41
|
+
|
42
|
+
class Position
|
43
|
+
include Clevic::Record
|
44
|
+
end
|
45
|
+
|
46
|
+
where Position is previously defined to inherit from ActiveRecord::Base.
|
47
|
+
|
48
|
+
A full-featured UI for an entity called Entry (part of an accounting database)
|
49
|
+
could be defined like this:
|
50
|
+
|
51
|
+
class Entry < ActiveRecord::Base
|
52
|
+
include Clevic::Record
|
53
|
+
|
54
|
+
# ActiveRecord foreign key definition
|
29
55
|
belongs_to :debit, :class_name => 'Account', :foreign_key => 'debit_id'
|
56
|
+
# ActiveRecord foreign key definition
|
30
57
|
belongs_to :credit, :class_name => 'Account', :foreign_key => 'credit_id'
|
31
58
|
|
32
59
|
define_ui do
|
33
|
-
# :format
|
34
|
-
plain :date, :format => '%d-%h-%y'
|
60
|
+
# :format and :edit_format are optional, in fact these are the defaults
|
61
|
+
plain :date, :format => '%d-%h-%y', :edit_format => '%d-%h-%Y'
|
35
62
|
plain :start, :format => '%H:%M'
|
36
63
|
plain :amount, :format => '%.2f'
|
37
64
|
# :set is mandatory for a restricted field
|
@@ -44,20 +71,27 @@ For example, the UI for a model called Entry could be defined like this:
|
|
44
71
|
tooltip 'Is VAT included?'
|
45
72
|
end
|
46
73
|
|
47
|
-
# distinct will
|
74
|
+
# distinct will retrieve from the table all other values for this field
|
75
|
+
# and display them in the combo.
|
48
76
|
distinct :description, :conditions => 'now() - date <= interval( 1 year )'
|
49
77
|
|
50
78
|
# this is a read-only field
|
51
79
|
plain :origin, :read_only => true
|
52
80
|
|
53
|
-
#
|
81
|
+
# :format is an attribute on the related
|
54
82
|
# ActiveRecord entity, in this case an instance of Account
|
55
|
-
|
56
|
-
|
83
|
+
# :order is an ActiveRecord option to find, defining the order in which related entries will be displayed.
|
84
|
+
# :conditions is an ActiveRecord option to find, defining the subset of related entries to be displayed.
|
85
|
+
relational :debit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
|
86
|
+
relational :credit, :format => 'name', :conditions => 'active = true', :order => 'lower(name)'
|
57
87
|
|
58
|
-
# or like this to have an on-the-fly transform
|
59
|
-
#
|
60
|
-
relational
|
88
|
+
# or like this to have an on-the-fly transform. item will be an instance of Account.
|
89
|
+
# This also takes a block parameter
|
90
|
+
relational :credit do |field|
|
91
|
+
field.format = lambda {|item| item.name.downcase}
|
92
|
+
field.conditions = 'active = true'
|
93
|
+
field.order = 'lower(name)'
|
94
|
+
end
|
61
95
|
|
62
96
|
# this is a read-only display field from a related table
|
63
97
|
# the Entry class should then define a method called currency
|
@@ -72,58 +106,215 @@ For example, the UI for a model called Entry could be defined like this:
|
|
72
106
|
# You can also use a Proc for :display
|
73
107
|
plain :some_field, :display => 'currency.rate', :label => 'Exchange Rate'
|
74
108
|
|
75
|
-
# this is optional
|
109
|
+
# this is optional. By default all records in id order will be displayed.
|
76
110
|
records :order => 'date,start'
|
77
111
|
|
78
112
|
# could also be like this, where a..e are instances of Entry
|
79
113
|
records [ a,b,c,d,e ]
|
80
114
|
end
|
81
115
|
end
|
116
|
+
|
117
|
+
== Separate View
|
118
|
+
|
119
|
+
To define a separate ui class, do something like this:
|
120
|
+
class Prospect < Clevic::View
|
121
|
+
|
122
|
+
# This is the ActiveRecord::Base descendant
|
123
|
+
entity_class Position
|
124
|
+
|
125
|
+
# This must return a ModelBuilder instance, which is made easier
|
126
|
+
# by putting the block in a call to model_builder.
|
127
|
+
#
|
128
|
+
# With no parameter, the block
|
129
|
+
# will be evaluated in the context of a Clevic::ModelBuilder instance,
|
130
|
+
# otherwise the parameter will have the Clevic::ModelBuilder instance
|
131
|
+
# so you can still access the surrounding scope.
|
132
|
+
def define_ui
|
133
|
+
model_builder do |mb|
|
134
|
+
# use the define_ui block from Position
|
135
|
+
mb.exec_ui_block( Position )
|
136
|
+
|
137
|
+
# use a different recordset
|
138
|
+
mb.records :conditions => "status in ('prospect','open')", :order => 'date desc,code'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
82
142
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
143
|
+
And you can even inherit UIs:
|
144
|
+
|
145
|
+
class Extinct < Prospect
|
146
|
+
def define_ui
|
147
|
+
# reuse all UI definitions from Prospect
|
148
|
+
super
|
149
|
+
# and again another recordset
|
150
|
+
model_builder do |mb|
|
151
|
+
mb.records :conditions => "status in ('dead')", :order => 'date desc,code'
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
Obviously you can use any of the Clevic::ModelBuilder calls described above, and exemplified
|
157
|
+
in the embedded example, inside of the model_builder block.
|
158
|
+
|
159
|
+
== DSL detail
|
160
|
+
|
161
|
+
This section describes the syntax of the DSL.
|
162
|
+
|
163
|
+
=== Field Types and specifiers
|
164
|
+
|
165
|
+
There are only a few field types, with lots of options. All field definitions
|
166
|
+
start with a field type, have an attribute, and take either a hash of options,
|
167
|
+
or a block for options. If the block specifies a parameter, an instance of
|
168
|
+
Clevic::Field will be passed. If the block has no parameter, it will be
|
169
|
+
evaluated in the context of a Clevic::Field instance. All the options specified
|
170
|
+
can use DSL-style acessors (no assignment =) or assignment statement.
|
171
|
+
|
172
|
+
plain
|
173
|
+
is an ordinary editable field. Boolean values are displayed as checkboxes.
|
174
|
+
|
175
|
+
relational
|
176
|
+
displays a set of values pulled from a belongs_to (many-to-one) relationship.
|
177
|
+
In other words all the possible related entities that this one could belong_to. Some
|
178
|
+
concise representation of the related entities are displayed in a combo box.
|
179
|
+
:display is mandatory. All options applicable to ActiveRecord::Base#find can also be passed.
|
180
|
+
|
181
|
+
distinct
|
182
|
+
fetches the set of values already in the field, so you don't have to re-type them.
|
183
|
+
New values are added in the text field part of the combo box. There is some prefix matching.
|
184
|
+
|
185
|
+
restricted
|
186
|
+
is a combo box that is not editable in the text field part - the user must select
|
187
|
+
a value from the :set (an array of strings) supplied. If :set has a hash as its value, the field
|
188
|
+
will display the hash values, and the hash keys will be stored in the db.
|
189
|
+
|
190
|
+
hide
|
191
|
+
you won't see this field. Actually, it's only useful after a default_ui, or pulling the
|
192
|
+
definition from somewhere else. It may go away and be replaced by remove.
|
193
|
+
|
194
|
+
=== Attribute
|
195
|
+
|
196
|
+
The attribute symbol is required, and is the first parameter after the field type. It must refer
|
197
|
+
to a method already defined in the entity. In other words any of:
|
198
|
+
- a db column
|
199
|
+
- a relationship (belongs_to, has_many, etc)
|
200
|
+
- a plain method that takes no parameters.
|
201
|
+
|
202
|
+
will work. Named scopes might also work, but I haven't tried them yet.
|
203
|
+
|
204
|
+
You can do things like this:
|
205
|
+
|
206
|
+
plain :entries, :label => 'First Entry', :display => 'first.date', :format => '%d-%b-%y'
|
207
|
+
plain :entries, :label => 'Last Entry', :display => 'last.date', :format => '%d-%b-%y'
|
208
|
+
|
209
|
+
Where the attribute fetches a collection of related entities, and :display will cause
|
210
|
+
exactly one of those values to be passed to :format.
|
211
|
+
|
212
|
+
=== Options
|
213
|
+
|
214
|
+
Optional specifiers follow the attribute, as hash parameters, or as a block. Many of them will
|
215
|
+
accept as a value one of:
|
216
|
+
- String, some kind of value
|
217
|
+
- Symbol, referring to a method on the entity
|
218
|
+
- Proc which takes the entity as a parameter
|
219
|
+
|
220
|
+
See Clevic::Field properties for available options.
|
221
|
+
|
222
|
+
=== Menu Items
|
223
|
+
|
224
|
+
You can define view/model specific actions ( an Action is Qt talk for menu items and shortcuts).
|
225
|
+
These will be added to the Edit menu, show up on context-click in the table
|
226
|
+
display, and can have optional keyboard shortcuts:
|
227
|
+
|
228
|
+
def define_actions( table_view, action_builder )
|
229
|
+
action_builder.action :smart_copy, 'Smart Copy', :shortcut => 'Ctrl+"' do
|
230
|
+
# a method in the class containing define_actions
|
231
|
+
# view.current_index.entity will return the entity instance.
|
232
|
+
smart_copy( view )
|
233
|
+
end
|
234
|
+
|
235
|
+
action_builder.action :invoice_from_project, 'Invoice from Project', :shortcut => 'Ctrl+Shift+I' do
|
236
|
+
# a method in the class containing define_actions
|
237
|
+
invoice_from_project( view.current_index, view )
|
92
238
|
end
|
93
239
|
end
|
240
|
+
|
241
|
+
=== Notifications
|
242
|
+
|
243
|
+
The following method will be called whenever data is changed, ie a field edit is completed:
|
244
|
+
|
245
|
+
def notify_data_changed( table_view, top_left_model_index, bottom_right_model_index )
|
246
|
+
end
|
247
|
+
|
248
|
+
Key presses will be sent here:
|
249
|
+
|
250
|
+
def notify_key_press( table_view, key_press_event, current_model_index )
|
251
|
+
end
|
252
|
+
|
253
|
+
The above may also be defined as class methods on an entity class.
|
254
|
+
|
255
|
+
=== Tab Order
|
256
|
+
|
257
|
+
Using an embedded definition, tab order in the browser is defined by the order in which view definitions
|
258
|
+
are encountered. Which is really useful if you want to have several view definitions in one file and
|
259
|
+
just execute clevic on that file.
|
260
|
+
|
261
|
+
For more complex situations where your code needs to be separated into
|
262
|
+
multiple files, as is traditional and useful for most non-trivial projects,
|
263
|
+
the order can be accessed in Clevic::View.order, and specified by
|
264
|
+
|
265
|
+
Clevic::View.order = [Position, Target, Account]
|
94
266
|
|
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.
|
98
267
|
=end
|
99
268
|
class ModelBuilder
|
100
269
|
|
101
|
-
# Create a definition for
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
def initialize( model_class, can_build_default = true, &block )
|
106
|
-
@model_class = model_class
|
270
|
+
# Create a definition for entity_view (subclass of Clevic::View).
|
271
|
+
# Then execute block using self.instance_eval.
|
272
|
+
def initialize( entity_view, &block )
|
273
|
+
@entity_view = entity_view
|
107
274
|
@auto_new = true
|
108
275
|
@read_only = false
|
109
276
|
@fields = []
|
110
|
-
|
277
|
+
exec_ui_block( &block )
|
278
|
+
end
|
279
|
+
|
280
|
+
attr_accessor :entity_view
|
281
|
+
|
282
|
+
# execute a block containing method calls understood by Clevic::ModelBuilder
|
283
|
+
# arg can be something that responds to define_ui_block,
|
284
|
+
# or just the block will be executed. If both are present,
|
285
|
+
# values in the block will overwrite values in arg's block.
|
286
|
+
def exec_ui_block( arg = nil, &block )
|
287
|
+
if !arg.nil? and arg.respond_to?( :define_ui_block )
|
288
|
+
exec_ui_block( &arg.define_ui_block )
|
289
|
+
end
|
290
|
+
|
291
|
+
unless block.nil?
|
292
|
+
if block.arity == -1
|
293
|
+
instance_eval( &block )
|
294
|
+
else
|
295
|
+
block.call( self )
|
296
|
+
end
|
297
|
+
end
|
298
|
+
self
|
111
299
|
end
|
112
300
|
|
113
|
-
# The collection of
|
301
|
+
# The collection of Clevic::Field instances where visible == true.
|
302
|
+
# the visible may go away.
|
114
303
|
def fields
|
115
304
|
@fields.reject{|x| !x.visible}
|
116
305
|
end
|
117
306
|
|
118
|
-
# return the index of the named field
|
307
|
+
# return the index of the named field in the collection of fields.
|
119
308
|
def index( field_name_sym )
|
120
309
|
retval = nil
|
121
310
|
fields.each_with_index{|x,i| retval = i if x.attribute == field_name_sym.to_sym }
|
122
311
|
retval
|
123
312
|
end
|
124
313
|
|
125
|
-
#
|
126
|
-
|
314
|
+
# The ActiveRecord::Base subclass
|
315
|
+
def entity_class
|
316
|
+
@entity_view.entity_class
|
317
|
+
end
|
127
318
|
|
128
319
|
# set read_only to true
|
129
320
|
def read_only!
|
@@ -135,35 +326,36 @@ class ModelBuilder
|
|
135
326
|
@auto_new = bool
|
136
327
|
end
|
137
328
|
|
329
|
+
# should this table automatically show a new blank record?
|
138
330
|
def auto_new?; @auto_new; end
|
139
331
|
|
140
332
|
# an ordinary field, edited in place with a text box
|
141
333
|
def plain( attribute, options = {}, &block )
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
read_only_default( attribute, options )
|
146
|
-
@fields << Clevic::Field.new( attribute.to_sym, model_class, options )
|
334
|
+
read_only_default!( attribute, options )
|
335
|
+
@fields << Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
147
336
|
end
|
148
337
|
|
149
|
-
#
|
338
|
+
# Returns a Clevic::Field with a DistinctDelegate, in other words
|
339
|
+
# a combo box containing all values for this field from the table.
|
150
340
|
def distinct( attribute, options = {}, &block )
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
155
|
-
field.delegate = DistinctDelegate.new( nil, attribute, model_class, options )
|
341
|
+
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
342
|
+
field.delegate = DistinctDelegate.new( nil, field )
|
156
343
|
@fields << field
|
157
344
|
end
|
158
345
|
|
159
|
-
#
|
346
|
+
# Returns a Clevic::Field with a RestrictedDelegate,
|
347
|
+
# a combo box, but restricted to a specified set, from the :set option.
|
160
348
|
def restricted( attribute, options = {}, &block )
|
161
|
-
# get values from block, if it's there
|
162
|
-
options = gather_block( options, &block )
|
163
349
|
|
164
|
-
|
165
|
-
field
|
166
|
-
|
350
|
+
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
351
|
+
raise "field #{attribute} restricted must have a set" if field.set.nil?
|
352
|
+
|
353
|
+
# TODO this really belongs in a separate 'map' field
|
354
|
+
if field.set.is_a? Hash
|
355
|
+
field.format = lambda{|x| field.set[x]}
|
356
|
+
end
|
357
|
+
|
358
|
+
field.delegate = RestrictedDelegate.new( nil, field )
|
167
359
|
@fields << field
|
168
360
|
end
|
169
361
|
|
@@ -172,49 +364,58 @@ class ModelBuilder
|
|
172
364
|
# if options[:format] has a value, it's used either as a block
|
173
365
|
# or as a dotted path
|
174
366
|
def relational( attribute, options = {}, &block )
|
175
|
-
|
176
|
-
|
367
|
+
field = Clevic::Field.new( attribute.to_sym, entity_class, options, &block )
|
368
|
+
if field.class_name.nil?
|
369
|
+
field.class_name = entity_class.reflections[attribute].class_name || attribute.to_s.classify
|
177
370
|
end
|
178
371
|
|
179
|
-
# get values from block, if it's there
|
180
|
-
options = gather_block( options, &block )
|
181
|
-
|
182
372
|
# check after all possible options have been collected
|
183
|
-
raise ":display must be specified" if
|
184
|
-
|
185
|
-
field = Clevic::Field.new( attribute.to_sym, model_class, options )
|
186
|
-
field.delegate = RelationalDelegate.new( nil, field.attribute_path, options )
|
373
|
+
raise ":display must be specified" if field.display.nil?
|
374
|
+
field.delegate = RelationalDelegate.new( nil, field )
|
187
375
|
@fields << field
|
188
376
|
end
|
189
377
|
|
190
378
|
# mostly used in the new block to define the set of records
|
191
379
|
# for the TableModel, but may also be
|
192
380
|
# used as an accessor for records.
|
193
|
-
def records(
|
381
|
+
def records( args = {} )
|
194
382
|
if args.size == 0
|
195
383
|
get_records
|
196
384
|
else
|
197
|
-
set_records( args
|
385
|
+
set_records( args )
|
198
386
|
end
|
199
387
|
end
|
200
388
|
|
201
|
-
#
|
202
|
-
#
|
389
|
+
# Tell this field not to show up in the UI.
|
390
|
+
# Mainly intended to be called after default_ui has been called.
|
203
391
|
def hide( attribute )
|
204
392
|
field( attribute ).visible = false
|
205
393
|
end
|
206
394
|
|
207
395
|
# Build a default UI. All fields except the primary key are displayed
|
208
396
|
# as editable in the table. Any belongs_to relations are used to build
|
209
|
-
# combo boxes.
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
213
|
-
#
|
397
|
+
# combo boxes. Default ordering is the primary key.
|
398
|
+
# Subscriber is already defined elsewhere as a subclass
|
399
|
+
# of ActiveRecord::Base:
|
400
|
+
# class Subscriber
|
401
|
+
# include Clevic::Record
|
402
|
+
# define_ui do
|
403
|
+
# default_ui
|
404
|
+
# plain :password # this field does not exist in the DB
|
405
|
+
# hide :password_salt # these should be hidden
|
406
|
+
# hide :password_hash
|
407
|
+
# end
|
408
|
+
# end
|
409
|
+
#
|
410
|
+
# An attempt to use a sensible :display option for the related class. In order:
|
411
|
+
# * the name of the class
|
412
|
+
# * :name
|
413
|
+
# * :title
|
414
|
+
# * :username
|
214
415
|
def default_ui
|
215
416
|
# combine reflections and attributes into one set
|
216
|
-
reflections =
|
217
|
-
ui_columns =
|
417
|
+
reflections = entity_class.reflections.keys.map{|x| x.to_s}
|
418
|
+
ui_columns = entity_class.columns.reject{|x| x.name == entity_class.primary_key }.map do |column|
|
218
419
|
# TODO there must be a better way to do this
|
219
420
|
att = column.name.gsub( /_id$/, '' )
|
220
421
|
if reflections.include?( att )
|
@@ -230,16 +431,19 @@ class ModelBuilder
|
|
230
431
|
|
231
432
|
# build columns
|
232
433
|
ui_columns.each do |column|
|
233
|
-
if
|
434
|
+
if entity_class.reflections.has_key?( column.to_sym )
|
234
435
|
begin
|
235
|
-
reflection =
|
436
|
+
reflection = entity_class.reflections[column.to_sym]
|
236
437
|
if reflection.class == ActiveRecord::Reflection::AssociationReflection
|
237
|
-
# try to find a sensible display class. Default to to_s
|
238
438
|
related_class = reflection.class_name.constantize
|
439
|
+
|
440
|
+
# try to find a sensible display class. Default to to_s
|
239
441
|
display_method =
|
240
|
-
%w{#{
|
442
|
+
%w{#{entity_class.name} name title username}.find( lambda{ 'to_s' } ) do |m|
|
241
443
|
related_class.column_names.include?( m ) || related_class.instance_methods.include?( m )
|
242
444
|
end
|
445
|
+
|
446
|
+
# set the display method
|
243
447
|
relational column.to_sym, :display => display_method
|
244
448
|
else
|
245
449
|
plain column.to_sym
|
@@ -248,14 +452,14 @@ class ModelBuilder
|
|
248
452
|
puts $!.message
|
249
453
|
puts $!.backtrace
|
250
454
|
# just do a plain
|
251
|
-
puts "Doing plain for #{
|
455
|
+
puts "Doing plain for #{entity_class}.#{column}"
|
252
456
|
plain column.to_sym
|
253
457
|
end
|
254
458
|
else
|
255
459
|
plain column.to_sym
|
256
460
|
end
|
257
461
|
end
|
258
|
-
records :order =>
|
462
|
+
records :order => entity_class.primary_key
|
259
463
|
end
|
260
464
|
|
261
465
|
# return the named Clevic::Field object
|
@@ -264,14 +468,15 @@ class ModelBuilder
|
|
264
468
|
end
|
265
469
|
|
266
470
|
# This takes all the information collected
|
267
|
-
# by the other methods, and returns
|
471
|
+
# by the other methods, and returns a new TableModel
|
472
|
+
# with the given table_view as its parent.
|
268
473
|
def build( table_view )
|
269
474
|
# build the model with all it's collections
|
270
475
|
# using @model here because otherwise the view's
|
271
476
|
# reference to this very same model is garbage collected.
|
272
477
|
@model = Clevic::TableModel.new( table_view )
|
273
|
-
|
274
|
-
@model.
|
478
|
+
table_view.object_name = @object_name
|
479
|
+
@model.entity_view = entity_view
|
275
480
|
@model.fields = @fields
|
276
481
|
@model.read_only = @read_only
|
277
482
|
@model.auto_new = auto_new?
|
@@ -285,36 +490,12 @@ class ModelBuilder
|
|
285
490
|
@model
|
286
491
|
end
|
287
492
|
|
288
|
-
|
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
|
493
|
+
protected
|
305
494
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
495
|
+
# Add ActiveRecord :include options for foreign keys, but it takes up too much memory,
|
496
|
+
# and actually takes longer to load a data set.
|
497
|
+
#--
|
498
|
+
# TODO ActiveRecord-2.1 has smarter includes
|
318
499
|
def add_include_options
|
319
500
|
fields.each do |field|
|
320
501
|
if field.delegate.class == RelationalDelegate
|
@@ -324,9 +505,8 @@ private
|
|
324
505
|
end
|
325
506
|
end
|
326
507
|
|
327
|
-
# set a sensible read-only value if it isn't already
|
328
|
-
|
329
|
-
def read_only_default( attribute, options )
|
508
|
+
# set a sensible read-only value if it isn't already specified in options
|
509
|
+
def read_only_default!( attribute, options )
|
330
510
|
# sensible defaults for read-only-ness
|
331
511
|
options[:read_only] ||=
|
332
512
|
case
|
@@ -334,18 +514,18 @@ private
|
|
334
514
|
# it's a Proc or a Method, so we can't set it
|
335
515
|
true
|
336
516
|
|
337
|
-
when
|
517
|
+
when entity_class.column_names.include?( options[:display].to_s )
|
338
518
|
# it's a DB column, so it's not read only
|
339
519
|
false
|
340
520
|
|
341
|
-
when
|
521
|
+
when entity_class.reflections.include?( attribute )
|
342
522
|
# one-to-one relationships can be edited. many-to-one certainly can't
|
343
|
-
reflection =
|
523
|
+
reflection = entity_class.reflections[attribute]
|
344
524
|
reflection.macro != :has_one
|
345
525
|
|
346
|
-
when
|
526
|
+
when entity_class.instance_methods.include?( attribute.to_s )
|
347
527
|
# read-only if there's no setter for the attribute
|
348
|
-
!
|
528
|
+
!entity_class.instance_methods.include?( "#{attribute.to_s}=" )
|
349
529
|
else
|
350
530
|
# default to not read-only
|
351
531
|
false
|
@@ -354,8 +534,8 @@ private
|
|
354
534
|
|
355
535
|
# The collection of model objects to display in a table
|
356
536
|
# arg can either be a Hash, in which case a new CacheTable
|
357
|
-
# is created, or it can be an array
|
358
|
-
#
|
537
|
+
# is created, or it can be an array.
|
538
|
+
# Called by records( *args )
|
359
539
|
def set_records( arg )
|
360
540
|
if arg.class == Hash
|
361
541
|
# need to defer this until all fields are collected
|
@@ -365,28 +545,15 @@ private
|
|
365
545
|
end
|
366
546
|
end
|
367
547
|
|
368
|
-
#
|
369
|
-
#
|
548
|
+
# Return a collection of records. Usually this will be a CacheTable.
|
549
|
+
# Called by records( *args )
|
370
550
|
def get_records
|
371
551
|
if @records.nil?
|
372
552
|
#~ add_include_options
|
373
|
-
@options
|
374
|
-
@records = CacheTable.new( model_class, @options )
|
553
|
+
@records = CacheTable.new( entity_class, @options.merge( :auto_new => auto_new? ) )
|
375
554
|
end
|
376
555
|
@records
|
377
556
|
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
557
|
|
391
558
|
end
|
392
559
|
|