congress_forms 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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