netzke-basepack 0.4.2 → 0.5.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/.autotest +1 -0
- data/.gitignore +6 -0
- data/{CHANGELOG → CHANGELOG.rdoc} +26 -0
- data/README.rdoc +11 -11
- data/Rakefile +37 -11
- data/TODO.rdoc +8 -0
- data/VERSION +1 -0
- data/javascripts/basepack.js +71 -28
- data/lib/app/models/netzke_auto_column.rb +56 -0
- data/lib/netzke-basepack.rb +5 -3
- data/lib/netzke/accordion_panel.rb +69 -67
- data/lib/netzke/active_record/basepack.rb +104 -0
- data/lib/netzke/active_record/data_accessor.rb +33 -0
- data/lib/netzke/basic_app.rb +233 -124
- data/lib/netzke/border_layout_panel.rb +97 -98
- data/lib/netzke/configuration_panel.rb +24 -0
- data/lib/netzke/data_accessor.rb +71 -0
- data/lib/netzke/ext.rb +6 -0
- data/lib/netzke/field_model.rb +1 -1
- data/lib/netzke/fields_configurator.rb +62 -37
- data/lib/netzke/form_panel.rb +161 -51
- data/lib/netzke/form_panel_api.rb +74 -0
- data/lib/netzke/form_panel_js.rb +129 -0
- data/lib/netzke/grid_panel.rb +385 -80
- data/lib/netzke/grid_panel_api.rb +352 -0
- data/lib/netzke/grid_panel_extras/javascripts/rows-dd.js +280 -0
- data/lib/netzke/grid_panel_js.rb +721 -0
- data/lib/netzke/masquerade_selector.rb +53 -0
- data/lib/netzke/panel.rb +9 -0
- data/lib/netzke/plugins/configuration_tool.rb +121 -0
- data/lib/netzke/property_editor.rb +95 -7
- data/lib/netzke/property_editor_extras/helper_model.rb +55 -34
- data/lib/netzke/search_panel.rb +62 -0
- data/lib/netzke/tab_panel.rb +97 -37
- data/lib/netzke/table_editor.rb +49 -44
- data/lib/netzke/tree_panel.rb +15 -16
- data/lib/netzke/wrapper.rb +29 -5
- data/netzke-basepack.gemspec +151 -19
- data/stylesheets/basepack.css +5 -0
- data/test/app_root/app/models/book.rb +1 -1
- data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +1 -1
- data/test/unit/accordion_panel_test.rb +1 -2
- data/test/unit/active_record_basepack_test.rb +54 -0
- data/test/unit/grid_panel_test.rb +8 -12
- data/test/unit/helper_model_test.rb +30 -0
- metadata +69 -78
- data/Manifest +0 -86
- data/TODO +0 -3
- data/lib/app/models/netzke_hash_record.rb +0 -180
- data/lib/app/models/netzke_layout_item.rb +0 -11
- data/lib/netzke/ar_ext.rb +0 -269
- data/lib/netzke/configuration_tool.rb +0 -80
- data/lib/netzke/container.rb +0 -77
- data/lib/netzke/db_fields.rb +0 -44
- data/lib/netzke/fields_configurator_old.rb +0 -62
- data/lib/netzke/form_panel_extras/interface.rb +0 -56
- data/lib/netzke/form_panel_extras/js_builder.rb +0 -134
- data/lib/netzke/grid_panel_extras/interface.rb +0 -206
- data/lib/netzke/grid_panel_extras/js_builder.rb +0 -352
- data/test/unit/ar_ext_test.rb +0 -53
- data/test/unit/netzke_hash_record_test.rb +0 -52
- data/test/unit/netzke_layout_item_test.rb +0 -28
data/lib/netzke/form_panel.rb
CHANGED
@@ -1,39 +1,57 @@
|
|
1
1
|
module Netzke
|
2
|
+
# = FormPanel
|
3
|
+
#
|
4
|
+
# Represents Ext.form.FormPanel
|
5
|
+
#
|
6
|
+
# == Configuration
|
7
|
+
# * <tt>:data_class_name</tt> - name of the ActiveRecord model that provides data to this GridPanel.
|
8
|
+
# * <tt>:record</tt> - record to be displayd in the form. Takes precedence over <tt>:record_id</tt>
|
9
|
+
# * <tt>:record_id</tt> - id of the record to be displayd in the form. Also see <tt>:record</tt>
|
10
|
+
#
|
11
|
+
# In the <tt>:ext_config</tt> hash (see Netzke::Base) the following FormPanel specific options are available:
|
12
|
+
#
|
13
|
+
# * <tt>:mode</tt> - when set to <tt>:config</tt>, FormPanel loads in configuration mode
|
2
14
|
class FormPanel < Base
|
15
|
+
include Netzke::FormPanelJs # javascript (client-side)
|
16
|
+
include Netzke::FormPanelApi # API (server-side)
|
17
|
+
include Netzke::DataAccessor # some code shared between GridPanel, FormPanel, and other widgets that use database attributes
|
18
|
+
|
3
19
|
# Class-level configuration with defaults
|
4
20
|
def self.config
|
5
21
|
set_default_config({
|
6
|
-
:
|
7
|
-
|
8
|
-
:
|
22
|
+
:config_tool_available => true,
|
23
|
+
|
24
|
+
:default_config => {
|
25
|
+
:ext_config => {
|
26
|
+
:bbar => %w{ apply },
|
27
|
+
:tools => %w{ }
|
28
|
+
},
|
29
|
+
:persistent_config => false
|
30
|
+
}
|
9
31
|
})
|
10
32
|
end
|
11
|
-
|
12
|
-
include Netzke::FormPanelExtras::JsBuilder
|
13
|
-
include Netzke::FormPanelExtras::Interface
|
14
|
-
include Netzke::DbFields # database field operations
|
15
33
|
|
16
|
-
#
|
17
|
-
|
34
|
+
# Extra javascripts
|
35
|
+
def self.include_js
|
36
|
+
[
|
37
|
+
"#{File.dirname(__FILE__)}/form_panel_extras/javascripts/xcheckbox.js"
|
38
|
+
]
|
39
|
+
end
|
18
40
|
|
19
|
-
|
41
|
+
api :submit, :load, :get_combobox_options
|
20
42
|
|
21
|
-
|
22
|
-
|
43
|
+
attr_accessor :record
|
44
|
+
|
45
|
+
def initialize(*args)
|
46
|
+
super
|
47
|
+
apply_helpers
|
48
|
+
@record = config[:record] || data_class && data_class.find_by_id(config[:record_id])
|
23
49
|
end
|
24
50
|
|
25
|
-
|
26
|
-
|
27
|
-
{
|
28
|
-
:ext_config => {
|
29
|
-
:config_tool => self.class.config[:config_tool_enabled_by_default],
|
30
|
-
},
|
31
|
-
|
32
|
-
:persistent_layout => self.class.config[:persistent_layout_enabled_by_default],
|
33
|
-
:persistent_config => self.class.config[:persistent_config_enabled_by_default]
|
34
|
-
}
|
51
|
+
def data_class
|
52
|
+
@data_class ||= config[:data_class_name] && config[:data_class_name].constantize
|
35
53
|
end
|
36
|
-
|
54
|
+
|
37
55
|
def configuration_widgets
|
38
56
|
res = []
|
39
57
|
|
@@ -41,64 +59,156 @@ module Netzke
|
|
41
59
|
:name => 'fields',
|
42
60
|
:widget_class_name => "FieldsConfigurator",
|
43
61
|
:active => true,
|
44
|
-
:widget => self
|
45
|
-
|
62
|
+
:widget => self,
|
63
|
+
:persistent_config => true
|
64
|
+
}
|
46
65
|
|
47
66
|
res << {
|
48
67
|
:name => 'general',
|
49
68
|
:widget_class_name => "PropertyEditor",
|
50
|
-
:
|
69
|
+
:widget => self,
|
51
70
|
:ext_config => {:title => false}
|
52
71
|
}
|
53
72
|
|
54
73
|
res
|
55
74
|
end
|
56
75
|
|
57
|
-
def tools
|
58
|
-
%w{ refresh }
|
59
|
-
end
|
60
|
-
|
61
76
|
def actions
|
62
77
|
{
|
63
78
|
:apply => {:text => 'Apply'}
|
64
79
|
}
|
65
80
|
end
|
66
81
|
|
67
|
-
def
|
68
|
-
|
82
|
+
def columns
|
83
|
+
@columns ||= get_columns.convert_keys{|k| k.to_sym}
|
69
84
|
end
|
70
85
|
|
71
|
-
def fields
|
72
|
-
@fields ||= get_fields
|
73
|
-
end
|
74
|
-
|
75
86
|
# parameters used to instantiate the JS object
|
76
87
|
def js_config
|
77
88
|
res = super
|
78
|
-
res.merge!(:
|
89
|
+
res.merge!(:clmns => columns)
|
79
90
|
res.merge!(:data_class_name => config[:data_class_name])
|
80
|
-
res.merge!(:record_data => config[:record].to_array(fields)) if config[:record]
|
81
91
|
res
|
82
92
|
end
|
83
93
|
|
84
|
-
|
94
|
+
# columns to be displayed by the FieldConfigurator (which is GridPanel-based)
|
95
|
+
def self.config_columns
|
96
|
+
[
|
97
|
+
{:name => :name, :type => :string, :editor => :combobox, :width => 200},
|
98
|
+
{:name => :hidden, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Excl"},
|
99
|
+
{:name => :disabled, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Dis"},
|
100
|
+
{:name => :xtype, :type => :string},
|
101
|
+
{:name => :value, :type => :string},
|
102
|
+
{:name => :field_label, :type => :string},
|
103
|
+
{:name => :input_type, :type => :string}
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.property_fields
|
108
|
+
res = [
|
109
|
+
{:name => :ext_config__title, :type => :string},
|
110
|
+
{:name => :ext_config__header, :type => :boolean, :default => true}
|
111
|
+
# {:name => :ext_config__bbar, :type => :json},
|
112
|
+
# {:name => :ext_config__prohibit_create, :type => :boolean},
|
113
|
+
# {:name => :ext_config__prohibit_update, :type => :boolean},
|
114
|
+
# {:name => :ext_config__prohibit_delete, :type => :boolean},
|
115
|
+
# {:name => :ext_config__prohibit_read, :type => :boolean}
|
116
|
+
]
|
117
|
+
|
118
|
+
res
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# Normalized columns
|
123
|
+
def normalized_columns
|
124
|
+
@normalized_columns ||= normalize_columns(columns)
|
125
|
+
end
|
85
126
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
127
|
+
|
128
|
+
def get_columns
|
129
|
+
if config[:persistent_config]
|
130
|
+
persistent_config['layout__columns'] ||= default_columns
|
131
|
+
res = normalize_array_of_columns(persistent_config['layout__columns'])
|
91
132
|
else
|
92
|
-
|
93
|
-
end
|
133
|
+
res = default_columns
|
134
|
+
end
|
135
|
+
|
136
|
+
# merge values for each field if the record is specified
|
137
|
+
@record && res.map! do |c|
|
138
|
+
value = @record.send(normalize_column(c)[:name])
|
139
|
+
value.nil? ? c : normalize_column(c).merge(:value => value)
|
140
|
+
end
|
141
|
+
|
142
|
+
res
|
94
143
|
end
|
95
|
-
|
96
144
|
|
97
|
-
|
98
|
-
|
99
|
-
|
145
|
+
XTYPE_MAP = {
|
146
|
+
:integer => :numberfield,
|
147
|
+
:boolean => :xcheckbox,
|
148
|
+
:date => :datefield,
|
149
|
+
:datetime => :xdatetime,
|
150
|
+
:text => :textarea,
|
151
|
+
:json => :jsonfield
|
152
|
+
# :string => :textfield
|
153
|
+
}
|
100
154
|
|
101
|
-
|
155
|
+
def default_columns
|
156
|
+
# columns specified in widget's config
|
157
|
+
columns_from_config = config[:columns] && normalize_columns(config[:columns])
|
102
158
|
|
159
|
+
if columns_from_config
|
160
|
+
# reverse-merge each column hash from config with each column hash from exposed_attributes (columns from config have higher priority)
|
161
|
+
for c in columns_from_config
|
162
|
+
corresponding_exposed_column = predefined_columns.find{ |k| k[:name] == c[:name] }
|
163
|
+
c.reverse_merge!(corresponding_exposed_column) if corresponding_exposed_column
|
164
|
+
end
|
165
|
+
columns_for_create = columns_from_config
|
166
|
+
elsif predefined_columns
|
167
|
+
# we didn't have columns configured in widget's config, so, use the columns from the data class
|
168
|
+
columns_for_create = predefined_columns
|
169
|
+
else
|
170
|
+
raise ArgumentError, "No columns specified for widget '#{id_name}'"
|
171
|
+
end
|
172
|
+
|
173
|
+
columns_for_create.map! do |c|
|
174
|
+
if data_class
|
175
|
+
# Try to figure out the configuration from data class
|
176
|
+
# detect :assoc__method
|
177
|
+
if c[:name].to_s.index('__')
|
178
|
+
assoc_name, method = c[:name].to_s.split('__').map(&:to_sym)
|
179
|
+
if assoc = data_class.reflect_on_association(assoc_name)
|
180
|
+
assoc_column = assoc.klass.columns_hash[method.to_s]
|
181
|
+
assoc_method_type = assoc_column.try(:type)
|
182
|
+
if assoc_method_type
|
183
|
+
c[:xtype] ||= XTYPE_MAP[assoc_method_type] == :xcheckbox ? :xcheckbox : :combobox
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# detect association column (e.g. :category_id)
|
189
|
+
if assoc = data_class.reflect_on_all_associations.detect{|a| a.primary_key_name.to_sym == c[:name]}
|
190
|
+
c[:xtype] ||= :combobox
|
191
|
+
assoc_method = %w{name title label id}.detect{|m| (assoc.klass.instance_methods + assoc.klass.column_names).include?(m) } || assoc.klass.primary_key
|
192
|
+
c[:name] = "#{assoc.name}__#{assoc_method}".to_sym
|
193
|
+
end
|
194
|
+
c[:hidden] = true if c[:name] == data_class.primary_key.to_sym && c[:hidden].nil? # hide ID column by default
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
# detect column type
|
199
|
+
type = c[:type] || data_class && data_class.columns_hash[c[:name].to_s].try(:type) || :string
|
200
|
+
c[:type] ||= type
|
201
|
+
|
202
|
+
c[:xtype] ||= XTYPE_MAP[type] unless XTYPE_MAP[type].nil?
|
203
|
+
|
204
|
+
# if the column is finally simply {:name => "something"}, cut it down to "something"
|
205
|
+
c.reject{ |k,v| k == :name }.empty? ? c[:name] : c
|
206
|
+
end
|
207
|
+
|
208
|
+
columns_for_create
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
include Plugins::ConfigurationTool if config[:config_tool_available] # it will load ConfigurationPanel into a modal window
|
103
213
|
end
|
104
214
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Netzke
|
2
|
+
module FormPanelApi
|
3
|
+
# API handling form submission
|
4
|
+
def submit(params)
|
5
|
+
data_hsh = ActiveSupport::JSON.decode(params[:data])
|
6
|
+
create_or_update_record(data_hsh)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Creates/updates a record from hash
|
10
|
+
def create_or_update_record(hsh)
|
11
|
+
klass = config[:data_class_name].constantize
|
12
|
+
@record = klass.find_by_id(hsh.delete("id"))
|
13
|
+
success = true
|
14
|
+
|
15
|
+
@record = klass.new if @record.nil?
|
16
|
+
|
17
|
+
hsh.each_pair do |k,v|
|
18
|
+
begin
|
19
|
+
@record.send("#{k}=",v)
|
20
|
+
rescue StandardError => exc
|
21
|
+
logger.debug "!!! FormPanelApi#create_or_update_record exception: #{exc.inspect}\n"
|
22
|
+
flash :error => exc.message
|
23
|
+
success = false
|
24
|
+
break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
if success && @record.save
|
29
|
+
{:set_form_values => array_of_values}
|
30
|
+
else
|
31
|
+
# flash eventual errors
|
32
|
+
@record.errors.each_full do |msg|
|
33
|
+
flash :error => msg
|
34
|
+
end
|
35
|
+
{:feedback => @flash}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# API handling form load
|
40
|
+
# def load(params)
|
41
|
+
# klass = config[:data_class_name].constantize
|
42
|
+
# case params[:neighbour]
|
43
|
+
# when "previous" then @record = klass.previous(params[:id])
|
44
|
+
# when "next" then @record = klass.next(params[:id])
|
45
|
+
# else @record = klass.find(params[:id])
|
46
|
+
# end
|
47
|
+
# {:data => [array_of_values]}
|
48
|
+
# end
|
49
|
+
|
50
|
+
def load(params)
|
51
|
+
@record = data_class && data_class.find_by_id(params[:id])
|
52
|
+
{:set_form_values => array_of_values}
|
53
|
+
end
|
54
|
+
|
55
|
+
# API that returns options for a combobox
|
56
|
+
def get_combobox_options(params)
|
57
|
+
column = params[:column]
|
58
|
+
query = params[:query]
|
59
|
+
|
60
|
+
{:data => config[:data_class_name].constantize.options_for(column, query).map{|s| [s]}}
|
61
|
+
end
|
62
|
+
|
63
|
+
def configuration_panel__fields__get_combobox_options(params)
|
64
|
+
query = params[:query]
|
65
|
+
{:data => (predefined_columns.map{ |c| c[:name].to_s }).grep(/^#{query}/).map{ |n| [n] }}.to_nifty_json
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns array of form values according to the configured columns
|
69
|
+
def array_of_values
|
70
|
+
@record && @record.to_array(columns, self)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Netzke
|
2
|
+
module FormPanelJs
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def js_base_class
|
9
|
+
"Ext.FormPanel"
|
10
|
+
end
|
11
|
+
|
12
|
+
def js_extend_properties
|
13
|
+
{
|
14
|
+
:body_style => 'padding:5px 5px 0',
|
15
|
+
:auto_scroll => true,
|
16
|
+
:label_width => 150,
|
17
|
+
:default_type => 'textfield',
|
18
|
+
# :label_align => 'top',
|
19
|
+
|
20
|
+
:init_component => <<-END_OF_JAVASCRIPT.l,
|
21
|
+
function(){
|
22
|
+
var recordFields = []; // Record
|
23
|
+
this.items = [];
|
24
|
+
var index = 0;
|
25
|
+
|
26
|
+
// Process columns
|
27
|
+
Ext.each(this.clmns, function(field){
|
28
|
+
if (typeof field == 'string') field = {name:field}; // normalize field
|
29
|
+
if (!field.hidden || field.name == 'id') {
|
30
|
+
recordFields.push({name:field.name, mapping:index++});
|
31
|
+
|
32
|
+
var defaultColumnConfig = Ext.apply({}, this.defaultColumnConfig);
|
33
|
+
var columnConfig = Ext.apply(defaultColumnConfig, field);
|
34
|
+
|
35
|
+
// apply dynamically defined properties
|
36
|
+
Ext.apply(columnConfig, {
|
37
|
+
fieldLabel: columnConfig.fieldLabel || columnConfig.name.humanize(),
|
38
|
+
hideLabel: columnConfig.hidden, // completely hide fields marked "hidden"
|
39
|
+
parentId: this.id,
|
40
|
+
name: columnConfig.name,
|
41
|
+
checked: columnConfig.xtype == "xcheckbox" ? columnConfig.value : null // checkbox state
|
42
|
+
});
|
43
|
+
|
44
|
+
this.items.push(columnConfig);
|
45
|
+
}
|
46
|
+
}, this);
|
47
|
+
|
48
|
+
var Record = Ext.data.Record.create(recordFields);
|
49
|
+
this.reader = new Ext.data.RecordArrayReader({root:"data"}, Record);
|
50
|
+
|
51
|
+
delete this.clmns; // we don't need them anymore
|
52
|
+
|
53
|
+
// Now let Ext.form.FormPanel do the rest
|
54
|
+
Ext.netzke.cache.FormPanel.superclass.initComponent.call(this);
|
55
|
+
|
56
|
+
// Apply event
|
57
|
+
this.addEvents('apply');
|
58
|
+
}
|
59
|
+
END_OF_JAVASCRIPT
|
60
|
+
|
61
|
+
# Defaults for each field
|
62
|
+
:defaults => {
|
63
|
+
# :anchor => '-20', # to leave some space for the scrollbar
|
64
|
+
:width => 180,
|
65
|
+
:listeners => {
|
66
|
+
# On "return" key, submit the form
|
67
|
+
:specialkey => {
|
68
|
+
:fn => <<-END_OF_JAVASCRIPT.l
|
69
|
+
function(field, event){
|
70
|
+
if (event.getKey() == 13) this.ownerCt.apply();
|
71
|
+
}
|
72
|
+
END_OF_JAVASCRIPT
|
73
|
+
}
|
74
|
+
}
|
75
|
+
},
|
76
|
+
|
77
|
+
:default_column_config => config_columns.inject({}){ |r, c| r.merge!({
|
78
|
+
c[:name] => c[:default]
|
79
|
+
}) },
|
80
|
+
|
81
|
+
:set_form_values => <<-END_OF_JAVASCRIPT.l,
|
82
|
+
function(values){
|
83
|
+
this.form.loadRecord(this.reader.readRecords({data:[values]}).records[0]);
|
84
|
+
}
|
85
|
+
END_OF_JAVASCRIPT
|
86
|
+
|
87
|
+
:load_record => <<-END_OF_JAVASCRIPT.l,
|
88
|
+
function(id, neighbour){
|
89
|
+
this.load({id:id});
|
90
|
+
// var proxy = new Ext.data.HttpProxy({url:this.initialConfig.api.load});
|
91
|
+
// proxy.load({id:id, neighbour:neighbour}, this.reader, function(data){
|
92
|
+
// if (data){
|
93
|
+
// this.form.loadRecord(data.records[0])
|
94
|
+
// }
|
95
|
+
// }, this)
|
96
|
+
}
|
97
|
+
END_OF_JAVASCRIPT
|
98
|
+
|
99
|
+
# :previous => <<-END_OF_JAVASCRIPT.l,
|
100
|
+
# function() {
|
101
|
+
# var currentId = this.form.getValues().id;
|
102
|
+
# this.loadRecord(currentId, 'previous');
|
103
|
+
# }
|
104
|
+
# END_OF_JAVASCRIPT
|
105
|
+
#
|
106
|
+
# :next => <<-END_OF_JAVASCRIPT.l,
|
107
|
+
# function() {
|
108
|
+
# var currentId = this.form.getValues().id;
|
109
|
+
# this.loadRecord(currentId, 'next');
|
110
|
+
# }
|
111
|
+
# END_OF_JAVASCRIPT
|
112
|
+
|
113
|
+
:apply => <<-END_OF_JAVASCRIPT.l
|
114
|
+
function() {
|
115
|
+
if (this.fireEvent('apply', this)) {
|
116
|
+
var values = this.form.getValues();
|
117
|
+
for (var k in values) {
|
118
|
+
if (values[k] == "") {delete values[k]}
|
119
|
+
}
|
120
|
+
this.submit(Ext.apply((this.baseParams || {}), {data:Ext.encode(values)}));
|
121
|
+
}
|
122
|
+
}
|
123
|
+
END_OF_JAVASCRIPT
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|