congress_forms 0.1.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.
- checksums.yaml +7 -0
- data/.env.example +14 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +684 -0
- data/README.md +152 -0
- data/Rakefile +6 -0
- data/bin/congress_forms +100 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/congress_forms.gemspec +30 -0
- data/lib/congress_forms/actions.rb +160 -0
- data/lib/congress_forms/cwc_form.rb +62 -0
- data/lib/congress_forms/form.rb +36 -0
- data/lib/congress_forms/repo.rb +95 -0
- data/lib/congress_forms/version.rb +3 -0
- data/lib/congress_forms/web_form.rb +102 -0
- data/lib/congress_forms.rb +85 -0
- data/lib/cwc/bad_request.rb +13 -0
- data/lib/cwc/client.rb +171 -0
- data/lib/cwc/extensions/hash.rb +20 -0
- data/lib/cwc/fixtures.rb +102 -0
- data/lib/cwc/message.rb +184 -0
- data/lib/cwc/office.rb +29 -0
- data/lib/cwc/topic_codes.rb +37 -0
- data/lib/cwc.rb +2 -0
- metadata +201 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
module CongressForms
|
2
|
+
class WebForm < Form
|
3
|
+
attr_accessor :bioguide, :actions
|
4
|
+
attr_accessor :success_status, :success_content
|
5
|
+
attr_accessor :updated_at
|
6
|
+
|
7
|
+
def self.parse(spec, attrs={})
|
8
|
+
yaml = YAML.load(spec)
|
9
|
+
|
10
|
+
actions = yaml.dig("contact_form", "steps").map do |step|
|
11
|
+
Actions.build(step)
|
12
|
+
end.flatten
|
13
|
+
|
14
|
+
new(
|
15
|
+
actions,
|
16
|
+
attrs.merge(
|
17
|
+
bioguide: yaml["bioguide"],
|
18
|
+
success_status:
|
19
|
+
yaml.dig("contact_form", "success", "headers", "status"),
|
20
|
+
success_content:
|
21
|
+
yaml.dig("contact_form", "success", "body", "contains"),
|
22
|
+
)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create_browser
|
27
|
+
if ENV["HEADLESS"] == "0"
|
28
|
+
Capybara::Session.new(:chrome)
|
29
|
+
else
|
30
|
+
Capybara::Session.new(:headless_chrome)
|
31
|
+
end.tap do |browser|
|
32
|
+
browser.current_window.resize_to(1920, 1080)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(actions = [], bioguide: nil,
|
37
|
+
success_status: nil,
|
38
|
+
success_content: nil,
|
39
|
+
updated_at: nil)
|
40
|
+
self.bioguide = bioguide
|
41
|
+
self.actions = actions
|
42
|
+
self.success_status = success_status
|
43
|
+
self.success_content = success_content
|
44
|
+
self.updated_at = updated_at
|
45
|
+
end
|
46
|
+
|
47
|
+
def required_params
|
48
|
+
required_actions = actions.dup
|
49
|
+
|
50
|
+
required_actions.select!(&:required?)
|
51
|
+
required_actions.select!(&:placeholder_value?)
|
52
|
+
|
53
|
+
required_actions.map do |action|
|
54
|
+
{
|
55
|
+
value: action.value,
|
56
|
+
max_length: action.max_length,
|
57
|
+
options: action.select_options
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def fill(values, browser: self.class.create_browser, validate_only: false)
|
63
|
+
log("#{bioguide} fill")
|
64
|
+
|
65
|
+
actions.each do |action|
|
66
|
+
break if action.submit? && validate_only
|
67
|
+
|
68
|
+
action.perform(browser, values)
|
69
|
+
end
|
70
|
+
|
71
|
+
log("done: success")
|
72
|
+
rescue Capybara::CapybaraError, Selenium::WebDriver::Error::WebDriverError => e
|
73
|
+
log("done: error")
|
74
|
+
|
75
|
+
error = Error.new(e.message)
|
76
|
+
error.set_backtrace(e.backtrace)
|
77
|
+
|
78
|
+
if screenshot = ENV["CONGRESS_FORMS_SCREENSHOT_LOCATION"]
|
79
|
+
error.screenshot = "#{screenshot}/#{SecureRandom.hex(16)}.png"
|
80
|
+
browser.save_screenshot(error.screenshot, full: true)
|
81
|
+
end
|
82
|
+
|
83
|
+
raise error
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def log(message)
|
89
|
+
if defined?(Rails)
|
90
|
+
Rails.logger.debug(message)
|
91
|
+
end
|
92
|
+
|
93
|
+
if defined?(Raven)
|
94
|
+
unless Raven.context.extra.key?(:fill_log)
|
95
|
+
Raven.extra_context(fill_log: "")
|
96
|
+
end
|
97
|
+
|
98
|
+
Raven.context.extra[:fill_log] << message << "\n"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
require "capybara"
|
4
|
+
require "selenium/webdriver"
|
5
|
+
|
6
|
+
require "congress_forms/version"
|
7
|
+
|
8
|
+
require "cwc"
|
9
|
+
|
10
|
+
unless ENV["CWC_API_KEY"].nil?
|
11
|
+
Cwc::Client.configure(
|
12
|
+
api_key: ENV["CWC_API_KEY"],
|
13
|
+
host: ENV["CWC_HOST"],
|
14
|
+
delivery_agent: ENV["CWC_DELIVERY_AGENT"],
|
15
|
+
delivery_agent_ack_email: ENV["CWC_DELIVERY_AGENT_ACK_EMAIL"],
|
16
|
+
delivery_agent_contact_name: ENV["CWC_DELIVERY_AGENT_CONTACT_NAME"],
|
17
|
+
delivery_agent_contact_email: ENV["CWC_DELIVERY_AGENT_CONTACT_EMAIL"],
|
18
|
+
delivery_agent_contact_phone: ENV["CWC_DELIVERY_AGENT_CONTACT_PHONE"]
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
Capybara.register_driver :chrome do
|
24
|
+
Capybara::Selenium::Driver.new(nil, browser: :chrome)
|
25
|
+
end
|
26
|
+
|
27
|
+
Capybara.register_driver :headless_chrome do
|
28
|
+
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
29
|
+
chromeOptions: {
|
30
|
+
args: %w(no-sandbox disable-gpu window-size=1200,1400).tap do |o|
|
31
|
+
o.push("--headless") unless ENV["HEADLESS"] == "0"
|
32
|
+
end
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
Capybara::Selenium::Driver.new(
|
37
|
+
nil,
|
38
|
+
browser: :chrome,
|
39
|
+
desired_capabilities: capabilities
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
module CongressForms
|
44
|
+
Error = Class.new(Exception) do
|
45
|
+
attr_accessor :screenshot
|
46
|
+
end
|
47
|
+
|
48
|
+
autoload :Form, "congress_forms/form"
|
49
|
+
autoload :WebForm, "congress_forms/web_form"
|
50
|
+
autoload :CwcForm, "congress_forms/cwc_form"
|
51
|
+
autoload :Actions, "congress_forms/actions"
|
52
|
+
|
53
|
+
autoload :Repo, "congress_forms/repo"
|
54
|
+
|
55
|
+
@@contact_congress_remote = "https://github.com/unitedstates/contact-congress.git"
|
56
|
+
|
57
|
+
|
58
|
+
def self.contact_congress_remote=(location)
|
59
|
+
@@contact_congress_remote = location
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.contact_congress_remote
|
63
|
+
@@contact_congress_remote
|
64
|
+
end
|
65
|
+
|
66
|
+
@@contact_congress_repository = nil
|
67
|
+
|
68
|
+
def self.contact_congress_repository=(location)
|
69
|
+
@@contact_congress_repository = location
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.contact_congress_repository
|
73
|
+
@@contact_congress_repository
|
74
|
+
end
|
75
|
+
|
76
|
+
@@auto_update_contact_congress = true
|
77
|
+
|
78
|
+
def self.auto_update_contact_congress=(auto_update)
|
79
|
+
@@auto_update_contact_congress = auto_update
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.auto_update_contact_congress?
|
83
|
+
@@auto_update_contact_congress
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Cwc
|
2
|
+
class BadRequest < Exception
|
3
|
+
attr_reader :original_exception, :errors
|
4
|
+
|
5
|
+
def initialize(e)
|
6
|
+
@original_exception = e
|
7
|
+
@errors = Nokogiri::XML(e.response.body).xpath("//Error").map(&:content)
|
8
|
+
if @errors.empty?
|
9
|
+
@errors << e.response.body
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/cwc/client.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
require "rest-client"
|
5
|
+
|
6
|
+
require "cwc/office"
|
7
|
+
require "cwc/message"
|
8
|
+
require "cwc/topic_codes"
|
9
|
+
require "cwc/bad_request"
|
10
|
+
require "cwc/fixtures"
|
11
|
+
|
12
|
+
module Cwc
|
13
|
+
class Client
|
14
|
+
attr_accessor :options
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def default_client_configuration=(x)
|
18
|
+
@default_client_configuration = x
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_client_configuration
|
22
|
+
@default_client_configuration ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure(options)
|
26
|
+
self.default_client_configuration = options
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Required options keys
|
31
|
+
# api_key String
|
32
|
+
# delivery_agent String, must match the api key owner
|
33
|
+
# delivery_agent_ack_email String
|
34
|
+
# delivery_agent_contact_name String
|
35
|
+
# delivery_agent_contact_email String
|
36
|
+
# delivery_agent_contact_phone String, format xxx-xxx-xxxx
|
37
|
+
def initialize(options={})
|
38
|
+
options = self.class.default_client_configuration.merge(options)
|
39
|
+
self.options = {
|
40
|
+
api_key: options.fetch(:api_key),
|
41
|
+
host: options.fetch(:host),
|
42
|
+
|
43
|
+
delivery_agent: {
|
44
|
+
name: options.fetch(:delivery_agent),
|
45
|
+
ack_email: options.fetch(:delivery_agent_ack_email),
|
46
|
+
contact_name: options.fetch(:delivery_agent_contact_name),
|
47
|
+
contact_email: options.fetch(:delivery_agent_contact_email),
|
48
|
+
contact_phone: options.fetch(:delivery_agent_contact_phone)
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Params format
|
54
|
+
# {
|
55
|
+
# campaign_id: String
|
56
|
+
# recipient: {
|
57
|
+
# member_office: String
|
58
|
+
# is_response_requested: Boolean ?
|
59
|
+
# newsletter_opt_in: Boolean ?
|
60
|
+
# },
|
61
|
+
# organization: {
|
62
|
+
# name: String ?
|
63
|
+
# contact: {
|
64
|
+
# name: String ?
|
65
|
+
# email: String ?
|
66
|
+
# phone: String ?
|
67
|
+
# about: String ?
|
68
|
+
# }
|
69
|
+
# },
|
70
|
+
# constituent: {
|
71
|
+
# prefix: String
|
72
|
+
# first_name: String
|
73
|
+
# middle_name: String ?
|
74
|
+
# last_name: String
|
75
|
+
# suffix: String ?
|
76
|
+
# title: String ?
|
77
|
+
# organization: String ?
|
78
|
+
# address: Array[String]
|
79
|
+
# city: String
|
80
|
+
# state_abbreviation: String
|
81
|
+
# zip: String
|
82
|
+
# phone: String ?
|
83
|
+
# address_validation: Boolean ?
|
84
|
+
# email: String
|
85
|
+
# email_validation: Boolean ?
|
86
|
+
# },
|
87
|
+
# message: {
|
88
|
+
# subject: String
|
89
|
+
# library_of_congress_topics: Array[String], drawn from Cwc::TopicCodes. Must give at least 1.
|
90
|
+
# bills: { Array[Hash]
|
91
|
+
# congress: Integer ?
|
92
|
+
# type_abbreviation: String
|
93
|
+
# number: Integer
|
94
|
+
# },
|
95
|
+
# pro_or_con: "pro" or "con" ?
|
96
|
+
# organization_statement: String ?
|
97
|
+
# constituent_message: String ?
|
98
|
+
# more_info: String (URL) ?
|
99
|
+
# }
|
100
|
+
#
|
101
|
+
# Use message[:constituent_message] for personal message,
|
102
|
+
# or message[:organization_statement] for campaign message
|
103
|
+
# At least one of these must be given
|
104
|
+
def create_message(params)
|
105
|
+
Cwc::Message.new.tap do |message|
|
106
|
+
message.delivery[:agent] = options.fetch(:delivery_agent)
|
107
|
+
message.delivery[:organization] = params.fetch(:organization, {})
|
108
|
+
message.delivery[:campaign_id] = params.fetch(:campaign_id)
|
109
|
+
|
110
|
+
message.recipient.merge!(params.fetch(:recipient))
|
111
|
+
message.constituent.merge!(params.fetch(:constituent))
|
112
|
+
message.message.merge!(params.fetch(:message))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def deliver(message)
|
117
|
+
post action("/v2/message"), message.to_xml
|
118
|
+
true
|
119
|
+
rescue RestClient::BadRequest => e
|
120
|
+
raise BadRequest.new(e)
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate(message)
|
124
|
+
post action("/v2/validate"), message.to_xml
|
125
|
+
true
|
126
|
+
rescue RestClient::BadRequest => e
|
127
|
+
raise BadRequest.new(e)
|
128
|
+
end
|
129
|
+
|
130
|
+
def office_supported?(office_code)
|
131
|
+
!offices.find{ |office| office.code == office_code }.nil?
|
132
|
+
end
|
133
|
+
|
134
|
+
def required_json(o={})
|
135
|
+
Cwc::RequiredJson.merge(o)
|
136
|
+
end
|
137
|
+
|
138
|
+
protected
|
139
|
+
|
140
|
+
def action(action)
|
141
|
+
host = options[:host].sub(/\/+$/, '')
|
142
|
+
action = action.sub(/^\/+/, '')
|
143
|
+
"#{host}/#{action}?apikey=#{options[:api_key]}"
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def get(url)
|
149
|
+
verify = !["false", "0"].include?(ENV["CWC_VERIFY_SSL"])
|
150
|
+
headers = { host: ENV["CWC_HOST_HEADER"] }.reject{ |_, v| v.nil? }
|
151
|
+
RestClient::Resource.new(url, verify_ssl: verify).get(headers)
|
152
|
+
end
|
153
|
+
|
154
|
+
def post(url, message)
|
155
|
+
verify = !["false", "0"].include?(ENV["CWC_VERIFY_SSL"])
|
156
|
+
headers = { content_type: :xml, host: ENV["CWC_HOST_HEADER"] }.
|
157
|
+
reject{ |_, v| v.nil? }
|
158
|
+
RestClient::Resource.new(url, verify_ssl: verify).
|
159
|
+
post(message, headers)
|
160
|
+
end
|
161
|
+
|
162
|
+
def offices
|
163
|
+
if options[:host] =~ %r{^https://cwc.house.gov}
|
164
|
+
Cwc::OfficeCodes.map{ |code| Office.new(code) }
|
165
|
+
else
|
166
|
+
response = get action("/offices")
|
167
|
+
JSON.parse(response.body).map{ |code| Office.new(code) }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Hash
|
2
|
+
unless instance_methods.include?(:dig)
|
3
|
+
def dig(key, *args)
|
4
|
+
obj = self[key]
|
5
|
+
if args.empty?
|
6
|
+
obj
|
7
|
+
elsif !obj.nil?
|
8
|
+
obj.dig(*args)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def dig!(key, *keys)
|
14
|
+
if keys.empty?
|
15
|
+
fetch(key)
|
16
|
+
else
|
17
|
+
fetch(key).dig!(*keys)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/cwc/fixtures.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
module Cwc
|
2
|
+
Prefixes = [
|
3
|
+
"Mr.", "Mrs.", "Ms.", "Mr. and Mrs.", "Miss", "Dr.", "Dr. and Mrs.",
|
4
|
+
"Dr. and Mr.", "Admiral", "Captain", "Chief Master Sergeant", "Colonel",
|
5
|
+
"Commander", "Corporal", "Father", "Lieutenant", "Lieutenant Colonel",
|
6
|
+
"Master Sergeant", "Reverend", "Sergeant", "Second Lieutenant", "Sergeant Major",
|
7
|
+
"Sister", "Technical Sergeant"
|
8
|
+
]
|
9
|
+
|
10
|
+
RequiredJson = {
|
11
|
+
"required_actions" => [
|
12
|
+
{ value: "$NAME_PREFIX", max_length: nil, options: Cwc::Prefixes },
|
13
|
+
{ value: "$NAME_FIRST", max_length: nil, options: nil },
|
14
|
+
{ value: "$NAME_LAST", max_length: nil, options: nil },
|
15
|
+
{ value: "$ADDRESS_STREET", max_length: nil, options: nil },
|
16
|
+
{ value: "$ADDRESS_CITY", max_length: nil, options: nil },
|
17
|
+
{ value: "$ADDRESS_ZIP5", max_length: nil, options: nil },
|
18
|
+
{ value: "$EMAIL", max_length: nil, options: nil },
|
19
|
+
{ value: "$SUBJECT", max_length: nil, options: nil },
|
20
|
+
{ value: "$MESSAGE", max_length: nil, options: nil },
|
21
|
+
{
|
22
|
+
value: "$ADDRESS_STATE_POSTAL_ABBREV", max_length: nil, options: [
|
23
|
+
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "DC", "FL", "GA",
|
24
|
+
"HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA",
|
25
|
+
"MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY",
|
26
|
+
"NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX",
|
27
|
+
"UT", "VT", "VA", "WA", "WV", "WI", "WY"
|
28
|
+
]
|
29
|
+
},
|
30
|
+
{ value: "$TOPIC", max_length: nil, options: Cwc::TopicCodes}
|
31
|
+
]
|
32
|
+
}
|
33
|
+
|
34
|
+
# this method was doing a request to a static JSON file on the CWC server.
|
35
|
+
# When their server temporarily went down, it brought down our form requests with it.
|
36
|
+
# We should probably update this at some point.
|
37
|
+
OfficeCodes = [
|
38
|
+
"HAK00","HAL01","HAL02","HAL03","HAL04","HAL05","HAL06",
|
39
|
+
"HAL07","HAR01","HAR02","HAR03","HAR04","HAZ01","HAZ02",
|
40
|
+
"HAZ03","HAZ04","HAZ05","HAZ06","HAZ07","HAZ08","HAZ09",
|
41
|
+
"HCA01","HCA02","HCA03","HCA04","HCA05","HCA06","HCA07",
|
42
|
+
"HCA08","HCA09","HCA10","HCA11","HCA12","HCA13","HCA14",
|
43
|
+
"HCA15","HCA16","HCA17","HCA18","HCA19","HCA20","HCA21",
|
44
|
+
"HCA22","HCA23","HCA24","HCA25","HCA26","HCA27","HCA28",
|
45
|
+
"HCA29","HCA30","HCA31","HCA32","HCA33","HCA34","HCA35",
|
46
|
+
"HCA36","HCA37","HCA38","HCA39","HCA40","HCA41","HCA42",
|
47
|
+
"HCA43","HCA44","HCA45","HCA46","HCA47","HCA48","HCA49",
|
48
|
+
"HCA50","HCA51","HCA52","HCA53","HCO01","HCO02","HCO03",
|
49
|
+
"HCO04","HCO05","HCO06","HCO07","HCT01","HCT02","HCT03",
|
50
|
+
"HCT04","HCT05","HDC00","HDE00","HFL01","HFL02","HFL03",
|
51
|
+
"HFL04","HFL05","HFL06","HFL07","HFL08","HFL09","HFL10",
|
52
|
+
"HFL11","HFL12","HFL13","HFL14","HFL15","HFL16","HFL17",
|
53
|
+
"HFL18","HFL19","HFL20","HFL21","HFL22","HFL23","HFL24",
|
54
|
+
"HFL25","HFL26","HFL27","HGA01","HGA02","HGA03","HGA04",
|
55
|
+
"HGA05","HGA06","HGA07","HGA08","HGA09","HGA10","HGA11",
|
56
|
+
"HGA12","HGA13","HGA14","HGU00","HHI01","HHI02","HIA01",
|
57
|
+
"HIA02","HIA03","HIA04","HID01","HID02","HIL01","HIL02",
|
58
|
+
"HIL03","HIL04","HIL05","HIL06","HIL07","HIL08","HIL09",
|
59
|
+
"HIL10","HIL11","HIL12","HIL13","HIL14","HIL15","HIL16",
|
60
|
+
"HIL17","HIL18","HIN01","HIN02","HIN03","HIN04","HIN05",
|
61
|
+
"HIN06","HIN07","HIN08","HIN09","HKS01","HKS02","HKS03",
|
62
|
+
"HKS04","HKY01","HKY02","HKY03","HKY04","HKY05","HKY06",
|
63
|
+
"HLA01","HLA02","HLA03","HLA04","HLA05","HLA06","HMA01",
|
64
|
+
"HMA02","HMA03","HMA04","HMA05","HMA06","HMA07","HMA08",
|
65
|
+
"HMA09","HMD01","HMD02","HMD03","HMD04","HMD05","HMD06",
|
66
|
+
"HMD07","HMD08","HME01","HME02","HMI01","HMI02","HMI03",
|
67
|
+
"HMI04","HMI05","HMI06","HMI07","HMI08","HMI09","HMI10",
|
68
|
+
"HMI11","HMI12","HMI13","HMI14","HMN01","HMN02","HMN03",
|
69
|
+
"HMN04","HMN05","HMN06","HMN07","HMN08","HMO01","HMO02",
|
70
|
+
"HMO03","HMO04","HMO05","HMO06","HMO07","HMO08","HMS01",
|
71
|
+
"HMS02","HMS03","HMS04","HMT00","HNC01","HNC02","HNC03",
|
72
|
+
"HNC04","HNC05","HNC06","HNC07","HNC08","HNC09","HNC10",
|
73
|
+
"HNC11","HNC12","HNC13","HND00","HNE01","HNE02","HNE03",
|
74
|
+
"HNH01","HNH02","HNJ01","HNJ02","HNJ03","HNJ04","HNJ05",
|
75
|
+
"HNJ06","HNJ07","HNJ08","HNJ09","HNJ10","HNJ11","HNJ12",
|
76
|
+
"HNM01","HNM02","HNM03","HNV01","HNV02","HNV03","HNV04",
|
77
|
+
"HNY01","HNY02","HNY03","HNY04","HNY05","HNY06","HNY07",
|
78
|
+
"HNY08","HNY09","HNY10","HNY11","HNY12","HNY13","HNY14",
|
79
|
+
"HNY15","HNY16","HNY17","HNY18","HNY19","HNY20","HNY21",
|
80
|
+
"HNY22","HNY23","HNY24","HNY25","HNY26","HNY27","HOH01",
|
81
|
+
"HOH02","HOH03","HOH04","HOH05","HOH06","HOH07","HOH08",
|
82
|
+
"HOH09","HOH10","HOH11","HOH12","HOH13","HOH14","HOH15",
|
83
|
+
"HOH16","HOK01","HOK02","HOK03","HOK04","HOK05","HOR01",
|
84
|
+
"HOR02","HOR03","HOR04","HOR05","HPA01","HPA02","HPA03",
|
85
|
+
"HPA04","HPA05","HPA06","HPA07","HPA08","HPA09","HPA10",
|
86
|
+
"HPA11","HPA12","HPA13","HPA14","HPA15","HPA16","HPA17",
|
87
|
+
"HPA18","HPR00","HRI01","HRI02","HSC01","HSC02","HSC03",
|
88
|
+
"HSC04","HSC05","HSC06","HSC07","HSD00","HTN01","HTN02",
|
89
|
+
"HTN03","HTN04","HTN05","HTN06","HTN07","HTN08","HTN09",
|
90
|
+
"HTX01","HTX02","HTX03","HTX04","HTX05","HTX06","HTX07",
|
91
|
+
"HTX08","HTX09","HTX10","HTX11","HTX12","HTX13","HTX14",
|
92
|
+
"HTX15","HTX16","HTX17","HTX18","HTX19","HTX20","HTX21",
|
93
|
+
"HTX22","HTX23","HTX24","HTX25","HTX26","HTX27","HTX28",
|
94
|
+
"HTX29","HTX30","HTX31","HTX32","HTX33","HTX34","HTX35",
|
95
|
+
"HTX36","HUT01","HUT02","HUT03","HUT04","HVA01","HVA02",
|
96
|
+
"HVA03","HVA04","HVA05","HVA06","HVA07","HVA08","HVA09",
|
97
|
+
"HVA10","HVA11","HVI00","HVT00","HWA01","HWA02","HWA03",
|
98
|
+
"HWA04","HWA05","HWA06","HWA07","HWA08","HWA09","HWA10",
|
99
|
+
"HWI01","HWI02","HWI03","HWI04","HWI05","HWI06","HWI07",
|
100
|
+
"HWI08","HWV01","HWV02","HWV03","HWY00","MP00"
|
101
|
+
]
|
102
|
+
end
|
data/lib/cwc/message.rb
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
|
2
|
+
require "securerandom"
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
require "cwc/extensions/hash"
|
6
|
+
|
7
|
+
module Cwc
|
8
|
+
class Message
|
9
|
+
attr_accessor :delivery
|
10
|
+
attr_accessor :recipient
|
11
|
+
attr_accessor :constituent
|
12
|
+
attr_accessor :message
|
13
|
+
|
14
|
+
attr_accessor :delivery_id
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.delivery = {}
|
18
|
+
self.recipient = {}
|
19
|
+
self.constituent = {}
|
20
|
+
self.message = {}
|
21
|
+
|
22
|
+
self.delivery_id = SecureRandom.random_number(36**33).to_s(36).rjust(12, "0")[0, 32]
|
23
|
+
end
|
24
|
+
|
25
|
+
def delivery_date
|
26
|
+
Time.now.strftime("%Y%m%d")
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_xml
|
30
|
+
Nokogiri::XML::Builder.new do |xml|
|
31
|
+
xml.CWC {
|
32
|
+
xml.CWCVersion "2.0"
|
33
|
+
|
34
|
+
delivery_section(xml)
|
35
|
+
recipient_section(xml)
|
36
|
+
constituent_section(xml)
|
37
|
+
message_section(xml)
|
38
|
+
}
|
39
|
+
end.to_xml
|
40
|
+
end
|
41
|
+
|
42
|
+
def delivery_section(xml)
|
43
|
+
xml.Delivery {
|
44
|
+
xml.DeliveryId delivery_id
|
45
|
+
xml.DeliveryDate delivery_date
|
46
|
+
|
47
|
+
xml.DeliveryAgent delivery.dig!(:agent, :name)
|
48
|
+
xml.DeliveryAgentAckEmailAddress delivery.dig!(:agent, :ack_email)
|
49
|
+
xml.DeliveryAgentContact {
|
50
|
+
xml.DeliveryAgentContactName delivery.dig!(:agent, :contact_name)
|
51
|
+
xml.DeliveryAgentContactEmail delivery.dig!(:agent, :contact_email)
|
52
|
+
xml.DeliveryAgentContactPhone delivery.dig!(:agent, :contact_phone)
|
53
|
+
}
|
54
|
+
|
55
|
+
if delivery.dig(:organization, :name)
|
56
|
+
xml.Organization delivery.dig(:organization, :name)
|
57
|
+
end
|
58
|
+
|
59
|
+
if (delivery.dig(:organization, :contact) || {}).keys.grep(/name|email|phone/).any?
|
60
|
+
xml.OrganizationContact {
|
61
|
+
if delivery.dig(:organization, :contact, :name)
|
62
|
+
xml.OrganizationContactName delivery.dig(:organization, :contact, :name)
|
63
|
+
end
|
64
|
+
|
65
|
+
if delivery.dig(:organization, :contact, :email)
|
66
|
+
xml.OrganizationContactEmail delivery.dig(:organization, :contact, :email)
|
67
|
+
end
|
68
|
+
|
69
|
+
if delivery.dig(:organization, :contact, :phone)
|
70
|
+
xml.OrganizationContactPhone delivery.dig(:organization, :contact, :phone)
|
71
|
+
end
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
if delivery.dig(:organization, :about)
|
76
|
+
xml.OrganizationAbout delivery.dig(:organization, :about)
|
77
|
+
end
|
78
|
+
|
79
|
+
xml.CampaignId delivery.fetch(:campaign_id)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def recipient_section(xml)
|
84
|
+
xml.Recipient {
|
85
|
+
xml.MemberOffice recipient.fetch(:member_office)
|
86
|
+
|
87
|
+
if recipient[:is_response_requested]
|
88
|
+
xml.IsResponseRequested "Y"
|
89
|
+
end
|
90
|
+
|
91
|
+
if recipient[:newsletter_opt_in]
|
92
|
+
xml.NewsletterOptIn "Y"
|
93
|
+
end
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def constituent_section(xml)
|
98
|
+
xml.Constituent {
|
99
|
+
xml.Prefix constituent.fetch(:prefix)
|
100
|
+
xml.FirstName constituent.fetch(:first_name)
|
101
|
+
|
102
|
+
if constituent[:middle_name]
|
103
|
+
xml.MiddleName constituent[:middle_name]
|
104
|
+
end
|
105
|
+
|
106
|
+
xml.LastName constituent.fetch(:last_name)
|
107
|
+
|
108
|
+
if constituent[:suffix]
|
109
|
+
xml.Suffix constituent[:suffix]
|
110
|
+
end
|
111
|
+
|
112
|
+
if constituent[:title]
|
113
|
+
xml.Title constituent[:title]
|
114
|
+
end
|
115
|
+
|
116
|
+
if constituent[:organization]
|
117
|
+
xml.Organization constituent[:organization]
|
118
|
+
end
|
119
|
+
|
120
|
+
xml.Address1 Array(constituent.fetch(:address))[0]
|
121
|
+
if Array(constituent[:address])[1]
|
122
|
+
xml.Address2 Array(constituent[:address])[1]
|
123
|
+
|
124
|
+
if Array(constituent[:address])[2]
|
125
|
+
xml.Address3 Array(constituent[:address])[2]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
xml.City constituent.fetch(:city)
|
129
|
+
xml.StateAbbreviation constituent.fetch(:state_abbreviation)
|
130
|
+
xml.Zip constituent.fetch(:zip)
|
131
|
+
|
132
|
+
if constituent[:phone]
|
133
|
+
xml.Phone constituent[:phone]
|
134
|
+
end
|
135
|
+
|
136
|
+
if constituent[:address_validation]
|
137
|
+
xml.AddressValidation "Y"
|
138
|
+
end
|
139
|
+
|
140
|
+
xml.Email constituent.fetch(:email)
|
141
|
+
|
142
|
+
if constituent[:email_validation]
|
143
|
+
xml.EmailValidation "Y"
|
144
|
+
end
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def message_section(xml)
|
149
|
+
xml.Message {
|
150
|
+
xml.Subject message.fetch(:subject)
|
151
|
+
|
152
|
+
xml.LibraryOfCongressTopics {
|
153
|
+
message.fetch(:library_of_congress_topics).each do |topic|
|
154
|
+
xml.LibraryOfCongressTopic topic
|
155
|
+
end
|
156
|
+
}
|
157
|
+
|
158
|
+
Array(message[:bills]).each do |bill|
|
159
|
+
xml.Bill {
|
160
|
+
xml.BillCongress bill[:congress]
|
161
|
+
xml.BillTypeAbbreviation bill[:type_abbreviation]
|
162
|
+
xml.BillNumber bill[:number]
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
if message[:pro_or_con]
|
167
|
+
xml.ProOrCon message[:pro_or_con]
|
168
|
+
end
|
169
|
+
|
170
|
+
if message[:organization_statement]
|
171
|
+
xml.OrganizationStatement message[:organization_statement]
|
172
|
+
end
|
173
|
+
|
174
|
+
if message[:constituent_message]
|
175
|
+
xml.ConstituentMessage message[:constituent_message]
|
176
|
+
end
|
177
|
+
|
178
|
+
if message[:more_info]
|
179
|
+
xml.MoreInfo message[:more_info]
|
180
|
+
end
|
181
|
+
}
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|