congress_forms 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# CongressForms
|
2
|
+
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
Add this line to your application's Gemfile:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'congress_forms'
|
10
|
+
```
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install congress_forms
|
19
|
+
|
20
|
+
|
21
|
+
### Program Dependencies
|
22
|
+
|
23
|
+
* google-chrome, git
|
24
|
+
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
To send a message to Congress, begin by creating a form object. Senators should be identified by their [BioGuide ID](https://www.congress.gov/help/field-values/member-bioguide-ids), while Representatives are identified by an office code H*XXYY*, where XX is their state and YY is their district.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
# Form for Senator Kamala Harris (BioGuide H001075)
|
32
|
+
CongressForms::Form.find("H001075")
|
33
|
+
|
34
|
+
# Form for CA-13 Represenative Barbara Lee (office code HCA13)
|
35
|
+
CongressForms::Form.find("HCA13")
|
36
|
+
```
|
37
|
+
|
38
|
+
Each Senator's office may require different fields, provide different options for select menus, etc. You can query the fields required by a particular form by calling `CongressForms::Form#required_params`.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
# List required message parameters
|
42
|
+
irb(main)> CongressForms::Form.find("H001075").required_params
|
43
|
+
[
|
44
|
+
# Required text fields
|
45
|
+
{ :value => "$NAME_FIRST", :max_length => nil },
|
46
|
+
{ :value => "$NAME_LAST", :max_length => nil },
|
47
|
+
|
48
|
+
# Required multiple choice field
|
49
|
+
{ :value => "$NAME_PREFIX", :options => ["Mr.", "Ms.", "Mrs.", ...] },
|
50
|
+
|
51
|
+
# Required multiple choice field with distinct labels and values
|
52
|
+
{
|
53
|
+
:value => "$TOPIC", :options => {
|
54
|
+
"Abortion" => "943AD4D7-5056-A066-60A5-D652A671D70E",
|
55
|
+
"Agriculture" => "943AD58A-5056-A066-60BD-A9DBEE1187A1",
|
56
|
+
"Animal Welfare" => "943AD622-5056-A066-6065-1B45E2F6F45D",
|
57
|
+
}
|
58
|
+
},
|
59
|
+
...
|
60
|
+
]
|
61
|
+
...
|
62
|
+
```
|
63
|
+
|
64
|
+
Pass the required values, in a hash, to `CongressForms::Form#fill` to send the message.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
form = CongressForms::Form.find("H001075")
|
68
|
+
|
69
|
+
form.fill(
|
70
|
+
"$NAME_FIRST" => "...",
|
71
|
+
"$NAME_LAST" => "...",
|
72
|
+
"$MESSAGE" => "...",
|
73
|
+
...
|
74
|
+
)
|
75
|
+
```
|
76
|
+
|
77
|
+
For Senate offices, this will fill out the representative's contact form with a headless instance of Google Chrome. For House offices, messages are submitted through the Communicating with Congress (CWC) API.
|
78
|
+
|
79
|
+
|
80
|
+
### CLI Usage
|
81
|
+
|
82
|
+
You can also send messages from the command line:
|
83
|
+
|
84
|
+
```
|
85
|
+
$ bin/congress_forms --help
|
86
|
+
Usage: congress_forms [options]
|
87
|
+
-i, --rep_id REP_ID ID of the representative to message
|
88
|
+
-r, --repo DIR Location for unitedstates/contact_congress repository
|
89
|
+
-p, --param KEY=VALUE e.g. -p NAME_FIRST=Badger
|
90
|
+
```
|
91
|
+
|
92
|
+
|
93
|
+
## Operation and Configuration
|
94
|
+
|
95
|
+
Senate messages rely on contact form details tracked by the [unitedstates/contact-congress](https://github.com/unitedstates/contact-congress) project. This repo is cloned into a temporary directory by default. You can configure CongressForms to use an existing/persistent direcory with
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
CongressForms.contact_congress_repository = "data/contact_congress"
|
99
|
+
```
|
100
|
+
|
101
|
+
A `git pull` is performed every now and then in this direcory, to keep the form details up to date. You can disable this behavior with
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
CongressForms.auto_update_contact_congress = false
|
105
|
+
```
|
106
|
+
|
107
|
+
House messages are submitted through the [Communicating with Congress](https://www.house.gov/doing-business-with-the-house/communicating-with-congress-cwc) API. You will need to complete the vendor application process, then configure the API client with
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
Cwc::Client.configure(
|
111
|
+
api_key: ENV["CWC_API_KEY"],
|
112
|
+
host: ENV["CWC_HOST"],
|
113
|
+
delivery_agent: ENV["CWC_DELIVERY_AGENT"],
|
114
|
+
delivery_agent_ack_email: ENV["CWC_DELIVERY_AGENT_ACK_EMAIL"],
|
115
|
+
delivery_agent_contact_name: ENV["CWC_DELIVERY_AGENT_CONTACT_NAME"],
|
116
|
+
delivery_agent_contact_email: ENV["CWC_DELIVERY_AGENT_CONTACT_EMAIL"],
|
117
|
+
delivery_agent_contact_phone: ENV["CWC_DELIVERY_AGENT_CONTACT_PHONE"]
|
118
|
+
)
|
119
|
+
```
|
120
|
+
|
121
|
+
### CWC Concerns
|
122
|
+
|
123
|
+
The CWC API requires that you connect from a whitelisted IP address. This is true even for the test endpoint, which makes development and testing of the API client tricky.
|
124
|
+
|
125
|
+
If you have a whitelisted IP, you can use SSH port forwarding to tunnel requests to CWC through the approved server. Keep this command running in a console:
|
126
|
+
|
127
|
+
```
|
128
|
+
$ ssh -L [port]:test-cwc.house.gov:443 [server]
|
129
|
+
```
|
130
|
+
|
131
|
+
Use `https://localhost:[port]/` as your CWC host, and define these environment variables:
|
132
|
+
|
133
|
+
```
|
134
|
+
CWC_VERIFY_SSL=false
|
135
|
+
CWC_HOST_HEADER=test-cwc.house.gov
|
136
|
+
```
|
137
|
+
|
138
|
+
(substitute `[server]` and `[port]` with your own values)
|
139
|
+
|
140
|
+
### Disabling headless mode
|
141
|
+
|
142
|
+
Chrome can be run in windowed mode by setting the environment variable `HEADLESS=0`.
|
143
|
+
|
144
|
+
## Contributing
|
145
|
+
|
146
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/efforg/congress_forms.
|
147
|
+
|
148
|
+
|
149
|
+
## License
|
150
|
+
|
151
|
+
The gem is available as open source under the terms of the [GPLv3 License](https://github.com/EFForg/congress_forms/blob/master/LICENSE.txt).
|
152
|
+
|
data/Rakefile
ADDED
data/bin/congress_forms
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
require "congress_forms"
|
7
|
+
|
8
|
+
require "dotenv/load"
|
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
|
+
options, params = { submit: true }, {}
|
23
|
+
|
24
|
+
ENV.each do |k, v|
|
25
|
+
next unless m = k.match(/CONGRESS_FORMS_(.+)/)
|
26
|
+
params["$#{m[1]}"] = v
|
27
|
+
end
|
28
|
+
|
29
|
+
opts = OptionParser.new do |opts|
|
30
|
+
opts.on("--rep_id REP_ID", "-i", "ID of the representative to message") do |id|
|
31
|
+
options[:rep_id] = id
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("--repo DIR", "-r", "Location for unitedstates/contact_congress repository") do |dir|
|
35
|
+
CongressForms.contact_congress_repository = dir
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("--param KEY=VALUE", "-p", "e.g. -p NAME_FIRST=Badger") do |pair|
|
39
|
+
key, value = pair.split("=", 2)
|
40
|
+
params["$#{key}"] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("--no-submit", "Fill out the form without submitting") do
|
44
|
+
options[:submit] = false
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("--debug", "Debug mode (HEADLESS=0, binding.pry if an exception is raised)") do
|
48
|
+
ENV["HEADLESS"] = "0"
|
49
|
+
options[:debug] = true
|
50
|
+
end
|
51
|
+
end.tap(&:parse!)
|
52
|
+
|
53
|
+
unless options[:rep_id]
|
54
|
+
warn opts.help
|
55
|
+
exit(1)
|
56
|
+
end
|
57
|
+
|
58
|
+
form = CongressForms::Form.find(options[:rep_id])
|
59
|
+
|
60
|
+
form.required_params.each do |param|
|
61
|
+
next if params[param[:value]]
|
62
|
+
|
63
|
+
print("#{param[:value]}: ")
|
64
|
+
|
65
|
+
if param[:options]
|
66
|
+
choices =
|
67
|
+
if param[:options].is_a?(Array)
|
68
|
+
param[:options].zip(param[:options]).to_h
|
69
|
+
else
|
70
|
+
param[:options]
|
71
|
+
end
|
72
|
+
|
73
|
+
puts
|
74
|
+
choices.each_with_index do |choice, i|
|
75
|
+
puts("#{i+1}. #{choice[0]}")
|
76
|
+
end
|
77
|
+
|
78
|
+
puts
|
79
|
+
print("Choice: ")
|
80
|
+
i = $stdin.gets or exit(1)
|
81
|
+
|
82
|
+
choice = choices.to_a[i.to_i-1][0]
|
83
|
+
params[param[:value]] = choices[choice]
|
84
|
+
puts
|
85
|
+
else
|
86
|
+
params[param[:value]] = $stdin.gets or exit(1)
|
87
|
+
params[param[:value]].chomp!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
form.fill(params, submit: options[:submit])
|
93
|
+
rescue Exception => e
|
94
|
+
if options[:debug]
|
95
|
+
require "pry"
|
96
|
+
binding.pry
|
97
|
+
else
|
98
|
+
raise e
|
99
|
+
end
|
100
|
+
end
|
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'congress_forms/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "congress_forms"
|
7
|
+
spec.version = CongressForms::VERSION
|
8
|
+
spec.authors = ["Peter Woo"]
|
9
|
+
spec.email = ["peterw@eff.org"]
|
10
|
+
|
11
|
+
spec.summary = %q{...}
|
12
|
+
spec.homepage = "https://github.com/efforg/congress_forms"
|
13
|
+
spec.license = "GPL-3.0"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "bin"
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.require_paths = ["lib", "cwc/lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "pry", "~> 0.11"
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
24
|
+
spec.add_development_dependency "dotenv", "~> 2.5"
|
25
|
+
|
26
|
+
spec.add_dependency "capybara-selenium", "~> 0.0.6"
|
27
|
+
spec.add_dependency "chromedriver-helper", "~> 1.2"
|
28
|
+
spec.add_dependency "nokogiri", ">= 1.8.2"
|
29
|
+
spec.add_dependency "rest-client", "~> 2.0"
|
30
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module CongressForms
|
2
|
+
module Actions
|
3
|
+
DEFAULT_FIND_WAIT_TIME = 5
|
4
|
+
|
5
|
+
def self.build(step)
|
6
|
+
key = step.keys.first
|
7
|
+
|
8
|
+
const_name = key.capitalize.gsub(/_(\w)/){ |m| m[1].upcase }
|
9
|
+
klass = const_get(const_name, false)
|
10
|
+
|
11
|
+
if Visit == klass
|
12
|
+
Array(klass.new("value" => step[key]))
|
13
|
+
else
|
14
|
+
step[key].map do |params|
|
15
|
+
klass.new(params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Base
|
21
|
+
attr_accessor :selector, :value, :options, :required
|
22
|
+
alias :required? :required
|
23
|
+
|
24
|
+
def initialize(params = {})
|
25
|
+
self.selector = params["selector"]
|
26
|
+
self.value = params["value"]
|
27
|
+
self.options = params["options"] || {}
|
28
|
+
self.required = !!params["required"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def max_length
|
32
|
+
options.is_a?(Hash) ? options["max_length"] : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def select_options
|
36
|
+
[Choose, Select].include?(self.class) ? options : nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def placeholder_value?
|
40
|
+
value[0, 1] == "$"
|
41
|
+
end
|
42
|
+
|
43
|
+
def escape_css_attribute(v)
|
44
|
+
v.gsub('"', '\"')
|
45
|
+
end
|
46
|
+
|
47
|
+
def submit?
|
48
|
+
"#{value} #{selector}".match(/submit/i)
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
s = "#{self.class.name.sub(/^CongressForms::Actions::/, '')}("
|
53
|
+
s << "#{selector.inspect}, " unless selector.nil?
|
54
|
+
s << value.inspect << ")"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Visit < Base
|
59
|
+
def perform(browser, params={})
|
60
|
+
browser.visit(value)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Wait < Base
|
65
|
+
def perform(browser, params={})
|
66
|
+
sleep(value.to_i)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class FillIn < Base
|
71
|
+
def perform(browser, params={})
|
72
|
+
if placeholder_value?
|
73
|
+
value = params.fetch(self.value).gsub("\t", " ")
|
74
|
+
|
75
|
+
maxl = options["max_length"]
|
76
|
+
value = value[0, (0.95 * maxl).floor] if maxl
|
77
|
+
else
|
78
|
+
value = self.value
|
79
|
+
end
|
80
|
+
|
81
|
+
browser.find(selector).set(value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Select < Base
|
86
|
+
def perform(browser, params={})
|
87
|
+
user_value = params[value]
|
88
|
+
|
89
|
+
browser.within(selector) do
|
90
|
+
if placeholder_value?
|
91
|
+
option_value = user_value
|
92
|
+
else
|
93
|
+
option_value = value
|
94
|
+
end
|
95
|
+
|
96
|
+
begin
|
97
|
+
elem = browser.first('option[value="' + escape_css_attribute(option_value) + '"]')
|
98
|
+
rescue Capybara::ElementNotFound
|
99
|
+
elem = browser.first('option', text: Regexp.compile("^" + Regexp.escape(option_value) + "(\\W|$)"))
|
100
|
+
end
|
101
|
+
|
102
|
+
elem.select_option
|
103
|
+
end
|
104
|
+
rescue Capybara::ElementNotFound => e
|
105
|
+
raise e, e.message unless options == "DEPENDENT"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class ClickOn < Base
|
110
|
+
def perform(browser, params={})
|
111
|
+
browser.find(selector).click
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Find < Base
|
116
|
+
def perform(browser, params={})
|
117
|
+
wait_val = options["wait"] || DEFAULT_FIND_WAIT_TIME
|
118
|
+
|
119
|
+
if value.nil?
|
120
|
+
browser.find(selector, wait: wait_val)
|
121
|
+
else
|
122
|
+
browser.find(selector, text: Regexp.compile(value),
|
123
|
+
wait: wait_val)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Check < Base
|
129
|
+
def perform(browser, params={})
|
130
|
+
browser.find(selector).set(true)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Uncheck < Base
|
135
|
+
def perform(browser, params={})
|
136
|
+
browser.find(selector).set(false)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Choose < Base
|
141
|
+
def perform(browser, params={})
|
142
|
+
if options.any?
|
143
|
+
user_value = params[value]
|
144
|
+
|
145
|
+
browser.
|
146
|
+
find(selector + '[value="' + escape_css_attribute(user_value) + '"]').
|
147
|
+
set(true)
|
148
|
+
else
|
149
|
+
browser.find(selector).set(true)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class Javascript < Base
|
155
|
+
def perform(browser, params={})
|
156
|
+
browser.driver.evaluate_script(value)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CongressForms
|
2
|
+
class CwcForm < Form
|
3
|
+
attr_accessor :office_code
|
4
|
+
|
5
|
+
def initialize(office_code)
|
6
|
+
self.office_code = office_code
|
7
|
+
end
|
8
|
+
|
9
|
+
def updated_at
|
10
|
+
Time.at(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
def required_params
|
14
|
+
Cwc::RequiredJson.fetch("required_actions").map(&:dup)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fill(values, campaign_tag: nil, organization: nil, browser: nil, validate_only: false)
|
18
|
+
params = {
|
19
|
+
campaign_id: campaign_tag || SecureRandom.hex(16),
|
20
|
+
|
21
|
+
recipient: { member_office: office_code },
|
22
|
+
|
23
|
+
constituent: {
|
24
|
+
prefix: values["$NAME_PREFIX"],
|
25
|
+
first_name: values["$NAME_FIRST"],
|
26
|
+
last_name: values["$NAME_LAST"],
|
27
|
+
address: Array(values["$ADDRESS_STREET"]),
|
28
|
+
city: values["$ADDRESS_CITY"],
|
29
|
+
state_abbreviation: values["$ADDRESS_STATE_POSTAL_ABBREV"],
|
30
|
+
zip: values["$ADDRESS_ZIP5"],
|
31
|
+
email: values["$EMAIL"]
|
32
|
+
},
|
33
|
+
|
34
|
+
message: {
|
35
|
+
subject: values["$SUBJECT"],
|
36
|
+
library_of_congress_topics: Array(values["$TOPIC"])
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
if organization
|
41
|
+
params[:organization] = organization
|
42
|
+
end
|
43
|
+
|
44
|
+
if values["$STATEMENT"]
|
45
|
+
params[:message][:organization_statement] = values["$STATEMENT"]
|
46
|
+
end
|
47
|
+
|
48
|
+
if values["$MESSAGE"] && values["$MESSAGE"] != values["$STATEMENT"]
|
49
|
+
params[:message][:constituent_message] = values["$MESSAGE"]
|
50
|
+
end
|
51
|
+
|
52
|
+
cwc_client = Cwc::Client.new
|
53
|
+
message = cwc_client.create_message(params)
|
54
|
+
|
55
|
+
if validate_only
|
56
|
+
cwc_client.validate(message)
|
57
|
+
else
|
58
|
+
cwc_client.deliver(message)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module CongressForms
|
2
|
+
class Form
|
3
|
+
@@repo = nil
|
4
|
+
|
5
|
+
def self.repo
|
6
|
+
@@repo ||=
|
7
|
+
Repo.new(CongressForms.contact_congress_remote).tap do |repo|
|
8
|
+
repo.location = CongressForms.contact_congress_repository
|
9
|
+
repo.auto_update = CongressForms.auto_update_contact_congress?
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find(form_id)
|
14
|
+
if Cwc::Client.new.office_supported?(form_id)
|
15
|
+
CwcForm.new(form_id)
|
16
|
+
else
|
17
|
+
content, timestamp = repo.find("members/#{form_id}.yaml")
|
18
|
+
WebForm.parse(content, updated_at: timestamp)
|
19
|
+
end
|
20
|
+
rescue Errno::ENOENT => e
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def missing_required_params(params)
|
25
|
+
missing_parameters = []
|
26
|
+
|
27
|
+
required_params.each do |field|
|
28
|
+
unless params.include?(field[:value])
|
29
|
+
missing_parameters << field[:value]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
missing_parameters.empty? ? nil : missing_parameters.any
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "tmpdir"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
module CongressForms
|
5
|
+
class Repo
|
6
|
+
attr_reader :remote
|
7
|
+
attr_accessor :auto_update
|
8
|
+
|
9
|
+
alias :auto_update? :auto_update
|
10
|
+
|
11
|
+
def initialize(remote)
|
12
|
+
@remote = remote
|
13
|
+
@semaphore = Mutex.new
|
14
|
+
self.auto_update = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def location
|
18
|
+
@location ||= Pathname.new(Dir.mktmpdir).tap do |tmpdir|
|
19
|
+
Kernel.at_exit{ FileUtils.rm_r(tmpdir) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def location=(loc)
|
24
|
+
@location = loc ? Pathname.new(loc) : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def clone
|
28
|
+
system(
|
29
|
+
"git",
|
30
|
+
"clone",
|
31
|
+
"--quiet",
|
32
|
+
"--depth", "1",
|
33
|
+
remote,
|
34
|
+
location.to_s
|
35
|
+
) or raise Error, "Error cloning repo at #{remote}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialized?
|
39
|
+
File.exists?(git_dir)
|
40
|
+
end
|
41
|
+
|
42
|
+
def update!
|
43
|
+
system(
|
44
|
+
"git",
|
45
|
+
"--git-dir", git_dir.to_s,
|
46
|
+
"pull",
|
47
|
+
"--quiet",
|
48
|
+
"--ff-only"
|
49
|
+
) or raise Error, "Error updating git repo at #{location}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def update
|
53
|
+
begin
|
54
|
+
update!
|
55
|
+
rescue
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def age
|
60
|
+
repo_touched_at = File.mtime(git_dir.join("HEAD"))
|
61
|
+
Time.now - repo_touched_at
|
62
|
+
end
|
63
|
+
|
64
|
+
def find(file)
|
65
|
+
lock do
|
66
|
+
clone unless initialized?
|
67
|
+
|
68
|
+
update if auto_update? && age > 5*60 # update every 5m
|
69
|
+
|
70
|
+
repo_file = system(
|
71
|
+
"git",
|
72
|
+
"--git-dir", git_dir.to_s,
|
73
|
+
"ls-files", "--error-unmatch",
|
74
|
+
"--", file
|
75
|
+
)
|
76
|
+
|
77
|
+
raise Errno::ENOENT, file unless repo_file
|
78
|
+
|
79
|
+
path = location.join(file).to_s
|
80
|
+
|
81
|
+
[File.read(path), File.mtime(path)]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def git_dir
|
86
|
+
location.join(".git")
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def lock(&block)
|
92
|
+
@semaphore.synchronize(&block)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|