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
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
|