ams_layout 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.pryrc +8 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +134 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +42 -0
- data/Rakefile +116 -0
- data/ams_layout.gemspec +32 -0
- data/bin/ams_layout +10 -0
- data/lib/ams_layout/browser_loader.rb +99 -0
- data/lib/ams_layout/cli/config.rb +250 -0
- data/lib/ams_layout/cli/generate.rb +145 -0
- data/lib/ams_layout/cli.rb +29 -0
- data/lib/ams_layout/client.rb +190 -0
- data/lib/ams_layout/core_ext/string.rb +30 -0
- data/lib/ams_layout/core_ext.rb +11 -0
- data/lib/ams_layout/delegate_writer.rb +348 -0
- data/lib/ams_layout/pages/login_page.rb +69 -0
- data/lib/ams_layout/pages/prequal_detail.rb +26 -0
- data/lib/ams_layout/pages.rb +36 -0
- data/lib/ams_layout/parser.rb +137 -0
- data/lib/ams_layout/version.rb +3 -0
- data/lib/ams_layout/writer.rb +124 -0
- data/lib/ams_layout.rb +198 -0
- data/spec/data/layout-small.yml +25 -0
- data/spec/data/layout-small.yml.aliases +22 -0
- data/spec/data/layout.yml +652 -0
- data/spec/data/layout.yml.aliases +613 -0
- data/spec/lib/ams_layout/ams_layout_spec.rb +7 -0
- data/spec/lib/ams_layout/cli/config_spec.rb +471 -0
- data/spec/lib/ams_layout/cli/generate_spec.rb +188 -0
- data/spec/lib/ams_layout/cli_spec.rb +35 -0
- data/spec/lib/ams_layout/client_spec.rb +93 -0
- data/spec/lib/ams_layout/delegate_writer_spec.rb +80 -0
- data/spec/lib/ams_layout/writer_spec.rb +64 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/spec_helper_spec.rb +27 -0
- data/spec/support/asserts.rb +13 -0
- data/spec/support/dirs.rb +45 -0
- data/spec/support/helpers.rb +46 -0
- metadata +231 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# File:: delegate_writer.rb
|
|
3
|
+
# Purpose:: Generate a ruby source file that is a delegate class containing
|
|
4
|
+
# the fields on the Loan Entry screen.
|
|
5
|
+
#
|
|
6
|
+
# Author:: Jeff McAffee 06/23/2014
|
|
7
|
+
# Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
|
|
8
|
+
# Website:: http://ktechsystems.com
|
|
9
|
+
##############################################################################
|
|
10
|
+
|
|
11
|
+
require 'ams_layout/core_ext'
|
|
12
|
+
|
|
13
|
+
module AmsLayout
|
|
14
|
+
class DelegateWriter
|
|
15
|
+
|
|
16
|
+
attr_writer :source_file_name
|
|
17
|
+
attr_writer :class_name
|
|
18
|
+
attr_writer :delegated_class_name
|
|
19
|
+
#attr_writer :aliases
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# Name of this class' source file
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
def source_file_name
|
|
26
|
+
class_name.ams_layout_snakecase + '.rb'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Name of this class
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
def class_name
|
|
34
|
+
@class_name ||= AmsLayout.configuration.delegate_class_name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Name of class we will delegate to
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
def delegated_class_name
|
|
42
|
+
@delegated_class_name ||= AmsLayout.configuration.layout_class_name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# List of aliases for specified fields
|
|
47
|
+
#
|
|
48
|
+
|
|
49
|
+
def aliases
|
|
50
|
+
@aliases ||= {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def aliases=(data)
|
|
54
|
+
@aliases = Hash(data)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Write a class file, based on the the layout.yml, to the provided stream
|
|
59
|
+
#
|
|
60
|
+
|
|
61
|
+
def write stream, layout
|
|
62
|
+
stream << header
|
|
63
|
+
|
|
64
|
+
layout.each do |section_label, fields|
|
|
65
|
+
stream << section(section_label)
|
|
66
|
+
|
|
67
|
+
fields.each do |fld|
|
|
68
|
+
stream << field(fld[:label], fld[:id], fld[:type])
|
|
69
|
+
write_aliases stream, fld[:label], fld[:id], fld[:type]
|
|
70
|
+
end # fields
|
|
71
|
+
end # layout
|
|
72
|
+
|
|
73
|
+
stream << footer
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Write a field's aliases to the stream
|
|
80
|
+
#
|
|
81
|
+
|
|
82
|
+
def write_aliases stream, label, id, type
|
|
83
|
+
label_aliases = aliases.key?(label) ? aliases[label] : []
|
|
84
|
+
label_aliases.each do |al|
|
|
85
|
+
stream << field_alias(al, id, type)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Emit a file header
|
|
91
|
+
#
|
|
92
|
+
|
|
93
|
+
def header
|
|
94
|
+
text =<<TEXT
|
|
95
|
+
#############################################################################
|
|
96
|
+
# #{source_file_name}
|
|
97
|
+
#
|
|
98
|
+
# This file has been generated by AmsLayout.
|
|
99
|
+
# Do not modify this file manually.
|
|
100
|
+
#
|
|
101
|
+
#############################################################################
|
|
102
|
+
|
|
103
|
+
require_relative 'loan_entry_fields'
|
|
104
|
+
require 'nokogiri'
|
|
105
|
+
|
|
106
|
+
class #{class_name} < DelegateClass(#{delegated_class_name})
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Capture and parse the page's raw HTML with Nokogiri
|
|
110
|
+
# Returns a Nokogiri::HTML document
|
|
111
|
+
#
|
|
112
|
+
|
|
113
|
+
def html_doc
|
|
114
|
+
@html_doc ||= Nokogiri::HTML(__getobj__.raw_html)
|
|
115
|
+
#Nokogiri::HTML(html).css("table#ctl00_ContentPlaceHolder1_tblMain").each do |tbl|
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# Return the first element matching the given ID
|
|
120
|
+
# Returns a Nokogiri Element
|
|
121
|
+
#
|
|
122
|
+
|
|
123
|
+
def noko_element(id)
|
|
124
|
+
html_doc.css(id).first
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Returns true if element is checked
|
|
129
|
+
#
|
|
130
|
+
|
|
131
|
+
def checked?(id)
|
|
132
|
+
# el['type'] = 'checkbox'
|
|
133
|
+
is_checked = noko_element(id)['checked']
|
|
134
|
+
! is_checked.nil?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Returns a text element's value
|
|
139
|
+
#
|
|
140
|
+
|
|
141
|
+
def get_text_element_value(id)
|
|
142
|
+
# el['type'] = 'text'
|
|
143
|
+
value = noko_element(id)['value']
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Returns a textarea element's value
|
|
148
|
+
#
|
|
149
|
+
|
|
150
|
+
def get_textarea_element_value(id)
|
|
151
|
+
noko_element(id).text
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Returns the selected option, '-- Please Select --' if no option is selected
|
|
156
|
+
#
|
|
157
|
+
|
|
158
|
+
def get_selected_option id
|
|
159
|
+
elem = noko_element(id)
|
|
160
|
+
elem.children.each do |c|
|
|
161
|
+
if c.attributes.key? 'selected'
|
|
162
|
+
return c.text
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Return the default value if nothing is selected
|
|
167
|
+
'-- Please Select --'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
#
|
|
171
|
+
# Fields (ordered by section as seen on screen)
|
|
172
|
+
#
|
|
173
|
+
TEXT
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
##
|
|
177
|
+
# Emit a section header
|
|
178
|
+
#
|
|
179
|
+
|
|
180
|
+
def section label
|
|
181
|
+
text =<<TEXT
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Section: #{label}
|
|
185
|
+
TEXT
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# Emit field methods
|
|
190
|
+
#
|
|
191
|
+
|
|
192
|
+
def field label, id, type
|
|
193
|
+
case type
|
|
194
|
+
when 'text'
|
|
195
|
+
return text_field_methods(label, id, type)
|
|
196
|
+
when 'textarea'
|
|
197
|
+
return textarea_field_methods(label, id, type)
|
|
198
|
+
when 'select'
|
|
199
|
+
return select_field_methods(label, id, type)
|
|
200
|
+
when 'checkbox'
|
|
201
|
+
return checkbox_field_methods(label, id, type)
|
|
202
|
+
else
|
|
203
|
+
return "\n# unknown_field_type: #{label}, #{id}, #{type}\n"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# Emit field methods for aliased fields
|
|
209
|
+
#
|
|
210
|
+
|
|
211
|
+
def field_alias label, id, type
|
|
212
|
+
field label, id, type
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# Emit text field methods
|
|
217
|
+
#
|
|
218
|
+
|
|
219
|
+
def text_field_methods label, id, type
|
|
220
|
+
field_label = label.ams_layout_snakecase
|
|
221
|
+
field_id = '#' + id
|
|
222
|
+
|
|
223
|
+
if field_id.include? 'PlaceHolder1_cn'
|
|
224
|
+
return float_field_methods label, id, type
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
text =<<TEXT
|
|
228
|
+
|
|
229
|
+
def #{field_label}=(value)
|
|
230
|
+
if get_text_element_value('#{field_id}') != value
|
|
231
|
+
super(value)
|
|
232
|
+
# else skip sending the value.
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
TEXT
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
##
|
|
239
|
+
# Emit float field methods
|
|
240
|
+
#
|
|
241
|
+
|
|
242
|
+
def float_field_methods label, id, type
|
|
243
|
+
field_label = label.ams_layout_snakecase
|
|
244
|
+
field_id = '#' + id
|
|
245
|
+
|
|
246
|
+
text =<<TEXT
|
|
247
|
+
|
|
248
|
+
def #{field_label}=(value)
|
|
249
|
+
current_value = get_text_element_value('#{field_id}')
|
|
250
|
+
|
|
251
|
+
current_value = '0' if current_value.empty?
|
|
252
|
+
value = '0' if value.empty?
|
|
253
|
+
|
|
254
|
+
if ((Float(current_value) * 1000) != (Float(value) * 1000))
|
|
255
|
+
super(value)
|
|
256
|
+
# else skip sending the value.
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
TEXT
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
##
|
|
263
|
+
# Emit textarea field methods
|
|
264
|
+
#
|
|
265
|
+
|
|
266
|
+
def textarea_field_methods label, id, type
|
|
267
|
+
field_label = label.ams_layout_snakecase
|
|
268
|
+
field_id = '#' + id
|
|
269
|
+
|
|
270
|
+
text =<<TEXT
|
|
271
|
+
|
|
272
|
+
def #{field_label}=(value)
|
|
273
|
+
if get_textarea_element_value('#{field_id}') != value
|
|
274
|
+
super(value)
|
|
275
|
+
# else skip sending the value.
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
TEXT
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
##
|
|
282
|
+
# Emit select field methods
|
|
283
|
+
#
|
|
284
|
+
|
|
285
|
+
def select_field_methods label, id, type
|
|
286
|
+
field_label = label.ams_layout_snakecase
|
|
287
|
+
field_id = '#' + id
|
|
288
|
+
|
|
289
|
+
text =<<TEXT
|
|
290
|
+
|
|
291
|
+
def #{field_label}=(value)
|
|
292
|
+
if get_selected_option('#{field_id}') != value
|
|
293
|
+
super(value)
|
|
294
|
+
# else skip sending the value.
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
TEXT
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
##
|
|
301
|
+
# Emit checkbox field methods
|
|
302
|
+
#
|
|
303
|
+
|
|
304
|
+
def checkbox_field_methods label, id, type
|
|
305
|
+
field_label = label.ams_layout_snakecase
|
|
306
|
+
field_id = '#' + id
|
|
307
|
+
|
|
308
|
+
text =<<TEXT
|
|
309
|
+
|
|
310
|
+
def check_#{field_label}
|
|
311
|
+
if ! checked?('#{field_id}')
|
|
312
|
+
super()
|
|
313
|
+
# else skip sending the value.
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def uncheck_#{field_label}
|
|
318
|
+
if checked?('#{field_id}')
|
|
319
|
+
super()
|
|
320
|
+
# else skip sending the value.
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
TEXT
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
##
|
|
327
|
+
# Emit file footer
|
|
328
|
+
#
|
|
329
|
+
|
|
330
|
+
def footer
|
|
331
|
+
text =<<TEXT
|
|
332
|
+
|
|
333
|
+
end # #{class_name}
|
|
334
|
+
|
|
335
|
+
TEXT
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
##
|
|
339
|
+
# Convert a field name to snake_case
|
|
340
|
+
#
|
|
341
|
+
|
|
342
|
+
def delme_snakecase str
|
|
343
|
+
snake = str.gsub /[^a-zA-Z0-9]/, '_'
|
|
344
|
+
snake = snake.gsub /_+/, '_'
|
|
345
|
+
snake.downcase
|
|
346
|
+
end
|
|
347
|
+
end # DelegateWriter
|
|
348
|
+
end # AmsLayout
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# File:: login_page.rb
|
|
3
|
+
# Purpose:: AMS Login page
|
|
4
|
+
#
|
|
5
|
+
# Author:: Jeff McAffee 06/22/2014
|
|
6
|
+
# Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
|
|
7
|
+
# Website:: http://ktechsystems.com
|
|
8
|
+
##############################################################################
|
|
9
|
+
|
|
10
|
+
require 'page-object'
|
|
11
|
+
|
|
12
|
+
module AmsLayout
|
|
13
|
+
module Pages
|
|
14
|
+
|
|
15
|
+
class LoginPage
|
|
16
|
+
include PageObject
|
|
17
|
+
|
|
18
|
+
page_url( AmsLayout.configuration.base_url )
|
|
19
|
+
|
|
20
|
+
text_field(:username, :id => "ctl00_ContentPlaceHolder1_Login1_txtUserName" )
|
|
21
|
+
text_field(:password_mask, :id => "ctl00_ContentPlaceHolder1_Login1_txtPasswordMask" )
|
|
22
|
+
text_field(:password, :id => "ctl00_ContentPlaceHolder1_Login1_txtPassword" )
|
|
23
|
+
button(:login, :id => "ctl00_ContentPlaceHolder1_Login1_btnLogin" )
|
|
24
|
+
|
|
25
|
+
# Part of the main menu visible after login
|
|
26
|
+
link(:pipelines, :text => "Pipelines")
|
|
27
|
+
|
|
28
|
+
def login_as(username, password)
|
|
29
|
+
self.username = username
|
|
30
|
+
allow_password_entry
|
|
31
|
+
self.password = password
|
|
32
|
+
login
|
|
33
|
+
|
|
34
|
+
trys = 0
|
|
35
|
+
# Make sure we wait until the (sometimes) slow login is finished.
|
|
36
|
+
while trys < 10 && !self.text.include?('LOAN PIPELINE')
|
|
37
|
+
trys += 1
|
|
38
|
+
sleep 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def logout
|
|
43
|
+
navigate_to page_url_value + '/User/AppLogout.aspx'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def logged_in?
|
|
47
|
+
!self.username?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def allow_password_entry
|
|
51
|
+
# We used to have to click on the password mask before the page would let us enter the password itself:
|
|
52
|
+
#
|
|
53
|
+
# # For some unknown reason, we must click on a password mask input before
|
|
54
|
+
# # we can access the password field itself.
|
|
55
|
+
# password_mask_element.click
|
|
56
|
+
#
|
|
57
|
+
# Now, we have to use javascript to hide the mask and display the password field.
|
|
58
|
+
hide_mask_script = <<EOS
|
|
59
|
+
pwdmasks = document.getElementsByClassName('passwordmask');
|
|
60
|
+
pwdmasks[0].style.display = 'none';
|
|
61
|
+
pwds = document.getElementsByClassName('password');
|
|
62
|
+
pwds[0].style.display = 'block';
|
|
63
|
+
EOS
|
|
64
|
+
|
|
65
|
+
@browser.execute_script(hide_mask_script)
|
|
66
|
+
end
|
|
67
|
+
end # class LoginPage
|
|
68
|
+
end # Pages
|
|
69
|
+
end # AmsLayout
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# File:: prequal_detail.rb
|
|
3
|
+
# Purpose:: Loan Entry (flow) for AMS Portal
|
|
4
|
+
#
|
|
5
|
+
# Author:: Jeff McAffee 2014-06-21
|
|
6
|
+
# Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
|
|
7
|
+
# Website:: http://ktechsystems.com
|
|
8
|
+
##############################################################################
|
|
9
|
+
|
|
10
|
+
require 'page-object'
|
|
11
|
+
|
|
12
|
+
module AmsLayout
|
|
13
|
+
module Pages
|
|
14
|
+
|
|
15
|
+
class PrequalDetail
|
|
16
|
+
include PageObject
|
|
17
|
+
|
|
18
|
+
page_url(::AmsLayout.configuration.base_url + '/SubmitLoan/PrequalDetail.aspx')
|
|
19
|
+
|
|
20
|
+
def html
|
|
21
|
+
@browser.html
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end # Pages
|
|
25
|
+
end # AmsLayout
|
|
26
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# File:: pages.rb
|
|
3
|
+
# Purpose:: Require all Page classes
|
|
4
|
+
#
|
|
5
|
+
# Author:: Jeff McAffee 2014-06-21
|
|
6
|
+
# Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
|
|
7
|
+
# Website:: http://ktechsystems.com
|
|
8
|
+
##############################################################################
|
|
9
|
+
|
|
10
|
+
require 'ams_layout/browser_loader'
|
|
11
|
+
require 'ams_layout/pages/login_page'
|
|
12
|
+
require 'ams_layout/pages/prequal_detail'
|
|
13
|
+
|
|
14
|
+
module AmsLayout
|
|
15
|
+
module Pages
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Return a configured browser object. If a browser has already been created,
|
|
19
|
+
# this returns the existing browser.
|
|
20
|
+
#
|
|
21
|
+
# An +at_exit+ proc is created to close the browser when the program exits.
|
|
22
|
+
|
|
23
|
+
def browser
|
|
24
|
+
if @browser.nil?
|
|
25
|
+
BrowserLoader.user_data_path = AmsLayout.configuration.user_data_path
|
|
26
|
+
@browser = BrowserLoader.init_browser AmsLayout.configuration.browser_timeout
|
|
27
|
+
|
|
28
|
+
at_exit do
|
|
29
|
+
@browser.close unless @browser.nil?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@browser
|
|
34
|
+
end
|
|
35
|
+
end # Pages
|
|
36
|
+
end # AmsLayout
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# File:: parser.rb
|
|
3
|
+
# Purpose:: Parse a Loan Entry screen and generate the layout data for the
|
|
4
|
+
# controls on the screen.
|
|
5
|
+
#
|
|
6
|
+
# Author:: Jeff McAffee 06/21/2014
|
|
7
|
+
# Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
|
|
8
|
+
# Website:: http://ktechsystems.com
|
|
9
|
+
##############################################################################
|
|
10
|
+
|
|
11
|
+
require 'nokogiri'
|
|
12
|
+
|
|
13
|
+
module AmsLayout
|
|
14
|
+
class Parser
|
|
15
|
+
|
|
16
|
+
def layout
|
|
17
|
+
@layout ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse html
|
|
21
|
+
# Table: ctl00_ContentPlaceHolder1_tblMain
|
|
22
|
+
Nokogiri::HTML(html).css("table#ctl00_ContentPlaceHolder1_tblMain").each do |tbl|
|
|
23
|
+
tbl.css('tr').each do |row|
|
|
24
|
+
parse_row row
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
layout
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
def parse_row row
|
|
33
|
+
cells = row.css('td')
|
|
34
|
+
|
|
35
|
+
if cells[0]['class'] == 'sectionheader'
|
|
36
|
+
add_section cells[0].text
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if cells.size >= 2
|
|
41
|
+
add_control build_control(cells[0], cells[1])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if cells.size >= 4
|
|
45
|
+
add_control build_control(cells[2], cells[3])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add_section section_name
|
|
50
|
+
@current_section = section_name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def current_section
|
|
54
|
+
fail "@current_section not set" if @current_section.nil?
|
|
55
|
+
|
|
56
|
+
if layout[@current_section].nil?
|
|
57
|
+
layout[@current_section] = Array.new
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
layout[@current_section]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_control control
|
|
64
|
+
current_section << control.to_hash unless control.is_a?(NullControl)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_control label_cell, control_cell
|
|
68
|
+
assert_label_td label_cell
|
|
69
|
+
assert_control_td control_cell
|
|
70
|
+
|
|
71
|
+
label = label_cell.text.gsub("\u00A0", ' ').strip
|
|
72
|
+
# Return if this is a blank cell
|
|
73
|
+
return NullControl.new if label.empty?
|
|
74
|
+
|
|
75
|
+
# Actual control is nested INPUT field
|
|
76
|
+
ctrl = find_input_element(control_cell)
|
|
77
|
+
if ctrl.nil?
|
|
78
|
+
fail "Unable to determine input element.\n#{control_cell.inspect}"
|
|
79
|
+
end
|
|
80
|
+
id = ctrl['id']
|
|
81
|
+
type = ctrl['type']
|
|
82
|
+
type = ctrl.name if type.nil?
|
|
83
|
+
assert_control_type type
|
|
84
|
+
Control.new label, id, type
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find_input_element cell
|
|
88
|
+
ctrl = cell.css('input').first
|
|
89
|
+
ctrl = cell.css('select').first if ctrl.nil?
|
|
90
|
+
ctrl = cell.css('textarea').first if ctrl.nil?
|
|
91
|
+
ctrl
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assert_label_td cell
|
|
95
|
+
label_classes = %w[LabelTD Mandatory]
|
|
96
|
+
fail "TD does not contain class attribute of 'LabelTD'" unless label_classes.include?(cell['class'])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def assert_control_td cell
|
|
100
|
+
fail "TD does not contain class attribute of 'ControlTD'" unless cell['class'] == 'ControlTD'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def assert_control_type type
|
|
104
|
+
fail 'nil control type' if type.nil?
|
|
105
|
+
fail "unexpected control type: #{type}" unless %w[text textarea select checkbox].include?(type)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Control
|
|
109
|
+
attr_reader :label, :id, :type
|
|
110
|
+
|
|
111
|
+
def initialize label, id, type
|
|
112
|
+
@label = label
|
|
113
|
+
@id = id
|
|
114
|
+
@type = type
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_hash
|
|
118
|
+
{label: @label, id: @id, type: @type}
|
|
119
|
+
end
|
|
120
|
+
end # Control
|
|
121
|
+
|
|
122
|
+
class NullControl
|
|
123
|
+
attr_reader :label, :id, :type
|
|
124
|
+
|
|
125
|
+
def initialize
|
|
126
|
+
@label = ''
|
|
127
|
+
@id = ''
|
|
128
|
+
@type = ''
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def to_hash
|
|
132
|
+
{label: @label, id: @id, type: @type}
|
|
133
|
+
end
|
|
134
|
+
end # Control
|
|
135
|
+
end # Parser
|
|
136
|
+
end # AmsLayout
|
|
137
|
+
|