clevic 0.13.0.b3 → 0.13.0.b5
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 +21 -0
- data/Manifest.txt +91 -85
- data/README.txt +33 -18
- data/Rakefile +2 -3
- data/TODO +8 -14
- data/bin/clevic +18 -20
- data/lib/clevic.rb +7 -1
- data/lib/clevic/action_builder.rb +4 -1
- data/lib/clevic/ar_methods.rb +72 -57
- data/lib/clevic/attribute_list.rb +4 -0
- data/lib/clevic/cache_table.rb +43 -69
- data/lib/clevic/dataset_roller.rb +22 -0
- data/lib/clevic/delegate.rb +11 -5
- data/lib/clevic/delegates/combo_delegate.rb +156 -0
- data/lib/clevic/delegates/distinct_delegate.rb +48 -0
- data/lib/clevic/delegates/relational_delegate.rb +59 -0
- data/lib/clevic/delegates/set_delegate.rb +31 -0
- data/lib/clevic/field.rb +39 -55
- data/lib/clevic/field_valuer.rb +23 -10
- data/lib/clevic/filter_command.rb +22 -36
- data/lib/clevic/framework.rb +37 -0
- data/lib/clevic/generic_format.rb +5 -1
- data/lib/clevic/many_field.rb +28 -3
- data/lib/clevic/model_builder.rb +27 -32
- data/lib/clevic/ordered_dataset.rb +45 -0
- data/lib/clevic/qt.rb +4 -1
- data/lib/clevic/qt/action_builder.rb +9 -1
- data/lib/clevic/qt/browser.rb +1 -1
- data/lib/clevic/qt/clipboard.rb +3 -3
- data/lib/clevic/qt/combo_delegate.rb +25 -89
- data/lib/clevic/qt/delegate.rb +25 -0
- data/lib/clevic/qt/distinct_delegate.rb +5 -23
- data/lib/clevic/qt/extensions.rb +8 -1
- data/lib/clevic/qt/qt_combo_box.rb +58 -0
- data/lib/clevic/qt/relational_delegate.rb +18 -58
- data/lib/clevic/qt/set_delegate.rb +4 -34
- data/lib/clevic/qt/simplest_delegate.rb +19 -0
- data/lib/clevic/qt/table_model.rb +10 -10
- data/lib/clevic/qt/table_view.rb +7 -23
- data/lib/clevic/qt/text_delegate.rb +2 -2
- data/lib/clevic/qt/ui/browser_ui.rb +1 -1
- data/lib/clevic/qt/ui/search_dialog_ui.rb +1 -1
- data/lib/clevic/rails_models_loaders.rb +13 -0
- data/lib/clevic/record.rb +2 -2
- data/lib/clevic/sampler.rb +85 -39
- data/lib/clevic/sequel_ar_adapter.rb +1 -28
- data/lib/clevic/sequel_clevic.rb +68 -0
- data/lib/clevic/sequel_meta.rb +1 -13
- data/lib/clevic/subclasses.rb +18 -0
- data/lib/clevic/swing.rb +2 -1
- data/lib/clevic/swing/action.rb +27 -3
- data/lib/clevic/swing/action_builder.rb +0 -2
- data/lib/clevic/swing/browser.rb +1 -10
- data/lib/clevic/swing/combo_delegate.rb +45 -133
- data/lib/clevic/swing/delegate.rb +2 -0
- data/lib/clevic/swing/distinct_delegate.rb +2 -14
- data/lib/clevic/swing/relational_delegate.rb +2 -20
- data/lib/clevic/swing/set_delegate.rb +13 -28
- data/lib/clevic/swing/table_view.rb +1 -1
- data/lib/clevic/table_model.rb +3 -4
- data/lib/clevic/table_searcher.rb +10 -31
- data/lib/clevic/table_view.rb +97 -43
- data/lib/clevic/ui/browser_ui.rb +133 -0
- data/lib/clevic/ui/search_dialog_ui.rb +106 -0
- data/lib/clevic/version.rb +2 -2
- data/models/accounts_models.rb +24 -21
- data/models/times_models.rb +34 -28
- data/models/times_psql_models.rb +9 -3
- data/models/times_sqlite_models.rb +24 -1
- data/sql/times_sqlite.sql +3 -3
- data/tasks/clevic.rake +2 -2
- data/test/test_cache_table.rb +9 -19
- data/test/test_table_searcher.rb +2 -5
- metadata +95 -91
- data/lib/clevic/order_attribute.rb +0 -63
- data/lib/clevic/qt/boolean_delegate.rb +0 -8
- data/lib/clevic/qt/delegates.rb +0 -1
- data/lib/clevic/qt/item_delegate.rb +0 -66
- data/lib/clevic/sql_dialects.rb +0 -33
- data/tasks/website.rake +0 -25
- data/test/test_order_attribute.rb +0 -62
- data/test/test_sql_dialects.rb +0 -77
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'clevic/delegates/combo_delegate.rb'
|
2
|
+
|
3
|
+
module Clevic
|
4
|
+
|
5
|
+
# Provide a list of all values in this field,
|
6
|
+
# and allow new values to be entered.
|
7
|
+
# :frequency can be set as an option. Boolean. If it's true
|
8
|
+
# the options are sorted in order of most frequently used first.
|
9
|
+
class DistinctDelegate
|
10
|
+
|
11
|
+
def needs_combo?
|
12
|
+
# works except when there is a null in the column
|
13
|
+
dataset.count > 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# TODO move away from ar_methods. Partly done.
|
17
|
+
# TODO ordering by either recentness, or frequency. OR both.
|
18
|
+
# TODO make sure nil is in the list. And the current item is at the top.
|
19
|
+
# TODO and the current item is in the list, even if it's older
|
20
|
+
# we only use the first column, so use the second
|
21
|
+
# column to sort by, since SQL requires the order by clause
|
22
|
+
# to be in the select list where distinct is involved
|
23
|
+
def dataset
|
24
|
+
base_dataset =
|
25
|
+
unless field.find_options.empty?
|
26
|
+
puts "conditions and order are deprecated. Use dataset instead."
|
27
|
+
require 'clevic/ar_methods'
|
28
|
+
field.entity_class.plugin :ar_methods
|
29
|
+
field.entity_class.translate( field.find_options )
|
30
|
+
else
|
31
|
+
field.dataset_roller.dataset
|
32
|
+
end
|
33
|
+
|
34
|
+
# now pull out the field and the distinct values
|
35
|
+
base_dataset. \
|
36
|
+
distinct. \
|
37
|
+
select( field.attribute ). \
|
38
|
+
order( field.attribute ). \
|
39
|
+
naked
|
40
|
+
end
|
41
|
+
|
42
|
+
def population
|
43
|
+
dataset.map( field.attribute )
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'clevic/delegates/combo_delegate'
|
2
|
+
require 'clevic/dataset_roller.rb'
|
3
|
+
|
4
|
+
module Clevic
|
5
|
+
|
6
|
+
# Display a collection of possible related entities in the combo box.
|
7
|
+
# TODO this should be a module
|
8
|
+
class RelationalDelegate
|
9
|
+
def needs_combo?
|
10
|
+
dataset.count > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty_set_message
|
14
|
+
"There must be records in #{field.related_class} for this field to be editable."
|
15
|
+
end
|
16
|
+
|
17
|
+
def population
|
18
|
+
# dataset contains the set of all possible related entities,
|
19
|
+
|
20
|
+
# dataset is defined in Delegate
|
21
|
+
# entity is set in init_component
|
22
|
+
# field and entity are used by FieldValuer
|
23
|
+
|
24
|
+
# including the current entity.
|
25
|
+
# Could also use
|
26
|
+
# dataset.or( entity_class.primary_key => entity_key.pk )
|
27
|
+
# but that would put current entity in the list somewhere
|
28
|
+
# other than the top, which seems to be the most sensible
|
29
|
+
# place for it. Could also create a special enumerator
|
30
|
+
# which gives back the entity first, followed by the dataset.
|
31
|
+
dataset.all.with do |values|
|
32
|
+
# make sure there's only one instance of the current value,
|
33
|
+
# and make sure it's at the top of the list
|
34
|
+
values.delete( attribute_value )
|
35
|
+
values.unshift( attribute_value )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# don't allow new values
|
40
|
+
def restricted?; true; end
|
41
|
+
|
42
|
+
protected
|
43
|
+
# Return an instance of the ORM dataset,
|
44
|
+
# right now that's Sequel::Dataset.
|
45
|
+
# This exists because convincing this functionality to
|
46
|
+
# coexist in the same method as dataset would be tricky.
|
47
|
+
def dataset
|
48
|
+
unless field.find_options.empty?
|
49
|
+
require 'clevic/ar_methods'
|
50
|
+
field.related_class.plugin :ar_methods
|
51
|
+
field.related_class.translate( field.find_options )
|
52
|
+
else
|
53
|
+
field.dataset_roller.dataset
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'clevic/delegates/combo_delegate.rb'
|
2
|
+
|
3
|
+
module Clevic
|
4
|
+
|
5
|
+
# A Combo box which allows a set of values. May or may not
|
6
|
+
# be restricted to the set.
|
7
|
+
# TODO this should be a module
|
8
|
+
class SetDelegate
|
9
|
+
# options must contain a :set => [ ... ] to specify the set of values.
|
10
|
+
def initialize( field )
|
11
|
+
raise "SetDelegate must have a :set in options" if field.set.nil?
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def needs_combo?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def restricted?
|
20
|
+
field.restricted || false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Items here could either be single values,
|
24
|
+
# or two-value arrays (from a hash-like set), so use key as db value
|
25
|
+
# and value as display value
|
26
|
+
def population
|
27
|
+
field.set_for( entity )
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/lib/clevic/field.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'gather'
|
2
2
|
require 'clevic/sampler.rb'
|
3
3
|
require 'clevic/generic_format.rb'
|
4
|
+
require 'clevic/dataset_roller.rb'
|
5
|
+
require 'clevic/many_field.rb'
|
4
6
|
|
5
7
|
module Clevic
|
6
8
|
|
@@ -136,7 +138,7 @@ class Field
|
|
136
138
|
# An Enumerable of allowed values for restricted fields. If each yields
|
137
139
|
# two values (like it does for a Hash), the
|
138
140
|
# first will be stored in the db, and the second displayed in the UI.
|
139
|
-
# If it's a proc,
|
141
|
+
# If it's a proc, that must return an Enumerable as above.
|
140
142
|
property :set
|
141
143
|
|
142
144
|
##
|
@@ -150,6 +152,7 @@ class Field
|
|
150
152
|
# Only for the distinct field type. The values will be sorted either with the
|
151
153
|
# most used values first (:frequency => true) or in
|
152
154
|
# alphabetical order (:description => true).
|
155
|
+
# FIXME re-implement this with Dataset
|
153
156
|
property :frequency, :description
|
154
157
|
|
155
158
|
##
|
@@ -160,7 +163,8 @@ class Field
|
|
160
163
|
|
161
164
|
##
|
162
165
|
# The property used for finding the field, ie by TableModel#field_column.
|
163
|
-
# Defaults to the attribute.
|
166
|
+
# Defaults to the attribute. If there are several display fields based on
|
167
|
+
# one db field, their attribute will be the same, but their id must be different.
|
164
168
|
property :id
|
165
169
|
|
166
170
|
##
|
@@ -168,11 +172,26 @@ class Field
|
|
168
172
|
# Either a proc( clevic_view, table_view, model_index ) or a symbol
|
169
173
|
# for a method( view, model_index ) on the Clevic::View object.
|
170
174
|
property :notify_data_changed
|
175
|
+
|
176
|
+
##
|
177
|
+
# This is the dataset of related objects.
|
178
|
+
# Called in configuration for a field that works with a relationship.
|
179
|
+
# dataset.filter( :blah => 'etc' ).order( :interesting_field )
|
180
|
+
def dataset
|
181
|
+
dataset_roller
|
182
|
+
end
|
183
|
+
|
184
|
+
# TODO Still getting the Builder/Built conflict
|
185
|
+
def dataset_roller
|
186
|
+
# related class if it's an association, entity_class otherwise
|
187
|
+
@dataset_roller ||= DatasetRoller.new( ( association? ? related_class : entity_class ).dataset )
|
188
|
+
end
|
171
189
|
|
172
190
|
# The list of properties for ActiveRecord options.
|
173
191
|
# There are actually from ActiveRecord::Base.VALID_FIND_OPTIONS, but it's protected.
|
174
192
|
# Each element becomes a property.
|
175
|
-
# TODO
|
193
|
+
# TODO deprecate these
|
194
|
+
# TODO warn or raise if these are used together with a dataset call
|
176
195
|
AR_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset, :order, :select, :readonly, :group, :from, :lock ]
|
177
196
|
AR_FIND_OPTIONS.each{|x| property x}
|
178
197
|
|
@@ -188,6 +207,10 @@ class Field
|
|
188
207
|
end
|
189
208
|
end
|
190
209
|
|
210
|
+
# The model object (eg TableModel) this field is part of.
|
211
|
+
# Set to TableModel by ModelBuilder#build
|
212
|
+
attr_accessor :model
|
213
|
+
|
191
214
|
# The UI delegate class for the field. The delegate class knows how to create a UI
|
192
215
|
# for this field using whatever GUI toolkit is selected
|
193
216
|
attr_accessor :delegate
|
@@ -243,36 +266,6 @@ EOF
|
|
243
266
|
default_display! if association?
|
244
267
|
end
|
245
268
|
|
246
|
-
# x_to_many fields are by definition collections of other entities
|
247
|
-
def many( &block )
|
248
|
-
if block
|
249
|
-
many_view( &block )
|
250
|
-
else
|
251
|
-
many_view do |mb|
|
252
|
-
# TODO should fetch this from one of the field definitions
|
253
|
-
mb.plain related_attribute
|
254
|
-
end
|
255
|
-
end
|
256
|
-
end
|
257
|
-
|
258
|
-
def many_builder
|
259
|
-
@many_view.builder
|
260
|
-
end
|
261
|
-
|
262
|
-
def many_fields
|
263
|
-
many_builder.fields
|
264
|
-
end
|
265
|
-
|
266
|
-
# return an instance of Clevic::View that represents the many items
|
267
|
-
# for this field
|
268
|
-
def many_view( &block )
|
269
|
-
@many_view ||= View.new( :entity_class => related_class, &block )
|
270
|
-
end
|
271
|
-
|
272
|
-
# The model object (eg TableModel) this field is part of.
|
273
|
-
# Set to TableModel by ModelBuilder#build
|
274
|
-
attr_accessor :model
|
275
|
-
|
276
269
|
# Return the attribute value for the given Object Relational Model instance, or nil
|
277
270
|
# if entity is nil. Will call transform_attribute.
|
278
271
|
def value_for( entity )
|
@@ -315,13 +308,6 @@ EOF
|
|
315
308
|
entity_class.meta[attribute]
|
316
309
|
end
|
317
310
|
|
318
|
-
# return the type of this attribute. Usually one of :string, :integer, :float
|
319
|
-
# or some entity class
|
320
|
-
# TODO remove
|
321
|
-
def attribute_type
|
322
|
-
meta.type
|
323
|
-
end
|
324
|
-
|
325
311
|
# return true if this field can be used in a filter
|
326
312
|
# virtual fields (ie those that don't exist in this field's
|
327
313
|
# table) can't be used to filter on.
|
@@ -337,7 +323,7 @@ EOF
|
|
337
323
|
# return the class object of a related class if this is a relational
|
338
324
|
# field, otherwise nil
|
339
325
|
def related_class
|
340
|
-
return nil unless entity_class.meta.has_key?( attribute )
|
326
|
+
return nil unless association? && entity_class.meta.has_key?( attribute )
|
341
327
|
@related_class ||= eval( entity_class.meta[attribute].class_name || attribute.to_s.classify )
|
342
328
|
end
|
343
329
|
|
@@ -363,26 +349,24 @@ EOF
|
|
363
349
|
do_generic_format( edit_format, value )
|
364
350
|
end
|
365
351
|
|
366
|
-
#
|
352
|
+
# Set or return a sample for the field which can be used to size the UI field widget.
|
367
353
|
def sample( *args )
|
368
354
|
if !args.empty?
|
369
355
|
@sample = args.first
|
370
356
|
self
|
371
357
|
else
|
372
358
|
if @sample.nil?
|
373
|
-
|
374
|
-
@sample
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
@sample ||= self.label
|
385
|
-
end
|
359
|
+
begin
|
360
|
+
@sample ||= Sampler.new( self ) do |value|
|
361
|
+
do_format( value )
|
362
|
+
end.compute
|
363
|
+
rescue
|
364
|
+
puts "for #{entity_class.name}"
|
365
|
+
puts $!.message
|
366
|
+
puts $!.backtrace
|
367
|
+
ensure
|
368
|
+
# if we don't know how to figure it out from the data, just return the label size
|
369
|
+
@sample ||= self.label
|
386
370
|
end
|
387
371
|
end
|
388
372
|
@sample
|
data/lib/clevic/field_valuer.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Clevic
|
2
|
-
#
|
3
|
-
#
|
2
|
+
# To be included in something that responds to entity and field methods.
|
3
|
+
# Used for getting values from the entity based on the definitions
|
4
4
|
# in the field.
|
5
5
|
module FieldValuer
|
6
6
|
|
@@ -26,13 +26,19 @@ module Clevic
|
|
26
26
|
self.attribute_value =
|
27
27
|
case
|
28
28
|
# allow flexibility in entering dates. For example
|
29
|
-
# 16jun, 16-jun, 16
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# year
|
29
|
+
# 16jun, 16-jun, 16/jun would be accepted here.
|
30
|
+
# Previous year is used if data capture is in January
|
31
|
+
# or February, and date.month is aug-dec.
|
33
32
|
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})[ /-]?(\w{3})$}
|
34
|
-
|
35
|
-
|
33
|
+
today = Date.today
|
34
|
+
date = Date.parse( "#$1 #$2 #{Time.now.year.to_s}" )
|
35
|
+
|
36
|
+
# change year to last year
|
37
|
+
if (1..2).include?( today.month ) and (8..12).include?( date.month )
|
38
|
+
date = Date.civil( Date.today.year - 1, date.month, date.day )
|
39
|
+
end
|
40
|
+
date
|
41
|
+
|
36
42
|
# if a digit only is entered, fetch month and year from
|
37
43
|
# previous row
|
38
44
|
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{1,2})$}
|
@@ -44,8 +50,10 @@ module Clevic
|
|
44
50
|
value
|
45
51
|
end
|
46
52
|
|
47
|
-
#
|
53
|
+
# Mostly to fix date strings that have come
|
48
54
|
# out of the db and been formatted
|
55
|
+
# Accepts dd mmm yy.
|
56
|
+
# Assumes 20 as the century.
|
49
57
|
when [:date,:datetime].include?( field.meta.type ) && value =~ %r{^(\d{2})[ /-](\w{3})[ /-](\d{2})$}
|
50
58
|
Date.parse( "#$1 #$2 20#$3" )
|
51
59
|
|
@@ -60,6 +68,7 @@ module Clevic
|
|
60
68
|
# do various transforms
|
61
69
|
case
|
62
70
|
# accept a space or a comma instead of a . for floats
|
71
|
+
# as long as there are only 2 decimal places
|
63
72
|
when value =~ /(.*?)(\d)[ ,](\d{2})$/
|
64
73
|
"#$1#$2.#$3"
|
65
74
|
else
|
@@ -72,7 +81,7 @@ module Clevic
|
|
72
81
|
end
|
73
82
|
|
74
83
|
def find_related( attribute, value )
|
75
|
-
candidates = field.related_class.
|
84
|
+
candidates = field.related_class.filter( attribute.to_sym => value ).all
|
76
85
|
if candidates.size != 1
|
77
86
|
raise "#{candidates.size} != 1 candidates for #{value}: #{candidates.inspect}"
|
78
87
|
end
|
@@ -152,6 +161,10 @@ module Clevic
|
|
152
161
|
end
|
153
162
|
end
|
154
163
|
|
164
|
+
def valuer( entity )
|
165
|
+
Valuer.new(field,entity)
|
166
|
+
end
|
167
|
+
|
155
168
|
def self.valuer( field, entity )
|
156
169
|
Valuer.new(field,entity)
|
157
170
|
end
|
@@ -1,41 +1,32 @@
|
|
1
1
|
module Clevic
|
2
2
|
class FilterCommand
|
3
|
-
#
|
4
|
-
#
|
5
|
-
def initialize( table_view,
|
3
|
+
# filter_block will be passed a Dataset to filter.
|
4
|
+
# filter_message will be displayed.
|
5
|
+
def initialize( table_view, message, &filter_block )
|
6
6
|
@table_view = table_view
|
7
|
-
@
|
8
|
-
@
|
9
|
-
|
10
|
-
# Better make the status message now, before the indexes become invalid
|
11
|
-
@status_message =
|
12
|
-
if filter_indexes.empty?
|
13
|
-
# no indexes, so use filter_conditions.
|
14
|
-
"Filtered on #{filter_conditions.inspect}"
|
15
|
-
else
|
16
|
-
"Filtered on #{filter_indexes.first.field.label} = #{filter_indexes.first.display_value}"
|
17
|
-
end
|
7
|
+
@message = message
|
8
|
+
@filter_block = filter_block
|
18
9
|
end
|
19
10
|
|
11
|
+
attr_reader :message
|
12
|
+
|
20
13
|
# Do the filtering. Return true if successful, false otherwise.
|
21
14
|
def doit
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
@table_view.model.reload_data( @filter_conditions )
|
32
|
-
rescue Exception => e
|
33
|
-
puts
|
34
|
-
puts e.message
|
35
|
-
puts e.backtrace
|
36
|
-
false
|
37
|
-
end
|
15
|
+
# store current dataset
|
16
|
+
@previous_dataset = @table_view.model.cache_table.dataset
|
17
|
+
|
18
|
+
# store auto_new
|
19
|
+
@auto_new = @table_view.model.auto_new
|
20
|
+
|
21
|
+
# reload cache table with new conditions
|
22
|
+
@table_view.model.auto_new = false
|
23
|
+
@table_view.model.reload_data( &@filter_block )
|
38
24
|
true
|
25
|
+
rescue Exception => e
|
26
|
+
puts
|
27
|
+
puts e.message
|
28
|
+
puts e.backtrace
|
29
|
+
false
|
39
30
|
end
|
40
31
|
|
41
32
|
def undo
|
@@ -43,12 +34,7 @@ module Clevic
|
|
43
34
|
@table_view.model.auto_new = @auto_new
|
44
35
|
|
45
36
|
# reload cache table with stored AR conditions
|
46
|
-
@table_view.model.reload_data( @
|
47
|
-
end
|
48
|
-
|
49
|
-
# return a message based on the conditions
|
50
|
-
def status_message
|
51
|
-
@status_message
|
37
|
+
@table_view.model.reload_data( @previous_dataset )
|
52
38
|
end
|
53
39
|
end
|
54
40
|
end
|