diff --git a/.env.example b/.env.example
index 7c05f6e69..a25b320af 100644
--- a/.env.example
+++ b/.env.example
@@ -63,6 +63,8 @@ amazon_authorize_key=
# Third party integration (required)
congress_forms_url=http://phantomdc.example.com
+google_civic_api_url=https://www.googleapis.com/civicinfo/v2/representatives/
+google_civic_api_key=
smarty_streets_id=
smarty_streets_token=
diff --git a/README.md b/README.md
index 526985ac8..94891733d 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,10 @@ Follow these instructions to run the Action Center using Docker (recommended). T
* Allows users to submit e-messages to congress
* [Call Congress](https://github.com/EFForg/call-congress) url and API key
* Connects calls between citizens and their congress person using the Twilio API
+* [Google Civic Information API](https://developers.google.com/civic-information) url and API key
+ * Representative information powered by the Civic Information API
+ * We use this when we need to give a user the ability to find their representatives to complete a state-level email action
+ * Some key limitations: https://developers.google.com/civic-information/docs/data_guidelines?hl=en e.g. "Developer’s using the API should make every effort to ensure all users are met with the same experience. We do not allow holdbacks, A/B testing, or similar experiments."
## Using the Action Center
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index a3cf8d238..118f8643c 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -8,6 +8,7 @@
//= require admin/gallery
//= require admin/action_pages
//= require admin/action_pages/petition-targets
+//= require admin/action_pages/email
//= require admin/analytics
//= require_tree ./admin/components
diff --git a/app/assets/javascripts/admin/action_pages/email.js b/app/assets/javascripts/admin/action_pages/email.js
new file mode 100644
index 000000000..fdb5167cc
--- /dev/null
+++ b/app/assets/javascripts/admin/action_pages/email.js
@@ -0,0 +1,15 @@
+$(document).ready(function() {
+ var stateLevelTarget = $("#action_page_email_campaign_attributes_state");
+ var stateLevelTargetSelection = $("#state-level-target-selection");
+
+ if (stateLevelTarget.val() === "")
+ stateLevelTargetSelection.hide();
+
+ stateLevelTarget.on("change", function() {
+ if (stateLevelTarget.val() !== "")
+ stateLevelTargetSelection.show();
+ else
+ stateLevelTargetSelection.hide();
+ });
+});
+
diff --git a/app/assets/javascripts/application/tools/congress_message.js b/app/assets/javascripts/application/tools/congress_message.js
index c1c155291..7539e0b33 100644
--- a/app/assets/javascripts/application/tools/congress_message.js
+++ b/app/assets/javascripts/application/tools/congress_message.js
@@ -105,23 +105,4 @@ $(document).on("ready", function() {
e.preventDefault();
$('#customize-message .notice').addClass('down');
});
-
- function show_progress_bars() {
- $(".progress-striped").show();
- $("#tools :submit").hide();
- $("#tools input,textarea,button,select", $(this)).attr("disabled", "disabled");
- }
-
- function show_error(error, form) {
- $(".progress-striped").hide();
- form.find(":submit").show();
- form.find(".alert-danger").remove();
- $("#errors").append($('
').text(error));
- $("#tools input,textarea,button,select", form).removeAttr("disabled");
- }
-
- function update_tabs(from, to) {
- $(".page-indicator div.page" + from).removeClass('active');
- $(".page-indicator div.page" + to).addClass('active');
- }
});
diff --git a/app/assets/javascripts/application/tools/email.js.erb b/app/assets/javascripts/application/tools/email.js.erb
index 7159ed624..d5dcdec9c 100644
--- a/app/assets/javascripts/application/tools/email.js.erb
+++ b/app/assets/javascripts/application/tools/email.js.erb
@@ -12,4 +12,24 @@ $(document).on('ready', function() {
$('.thank-you').show();
$('#email-tool').hide();
});
+
+ $(".state-rep-email").hide();
+
+ $("form.state-rep-lookup").on("ajax:complete", function(e, xhr, status) {
+ var $form = $(this);
+ var data = xhr.responseJSON;
+ $('.state-rep-lookup').hide();
+ $('.state-reps').replaceWith(data.content);
+ if (status == "success") {
+ $form.remove();
+ $(".state-reps").html(data);
+
+ if ($("#action-content").length) {
+ $(window).scrollTop( $("#action-content").offset().top ); // go to top of page if on action center site
+ }
+ update_tabs(1, 2);
+ } else {
+ show_error("Something went wrong. Please try again later.", $form);
+ }
+ });
});
diff --git a/app/assets/javascripts/application/tools/shared.js b/app/assets/javascripts/application/tools/shared.js
new file mode 100644
index 000000000..97d07e676
--- /dev/null
+++ b/app/assets/javascripts/application/tools/shared.js
@@ -0,0 +1,18 @@
+function show_progress_bars() {
+ $(".progress-striped").show();
+ $("#tools :submit").hide();
+ $("#tools input,textarea,button,select", $(this)).attr("disabled", "disabled");
+}
+
+function show_error(error, form) {
+ $(".progress-striped").hide();
+ form.find(":submit").show();
+ form.find(".alert-danger").remove();
+ $("#errors").append($('
').text(error));
+ $("#tools input,textarea,button,select", form).removeAttr("disabled");
+}
+
+function update_tabs(from, to) {
+ $(".page-indicator div.page" + from).removeClass('active');
+ $(".page-indicator div.page" + to).addClass('active');
+}
\ No newline at end of file
diff --git a/app/controllers/admin/action_pages_controller.rb b/app/controllers/admin/action_pages_controller.rb
index 5045b8f7a..ae9a906bd 100644
--- a/app/controllers/admin/action_pages_controller.rb
+++ b/app/controllers/admin/action_pages_controller.rb
@@ -198,10 +198,11 @@ def action_page_params
tweet_targets_attributes: [:id, :_destroy, :twitter_id, :image]
],
email_campaign_attributes: [
- :id, :message, :subject, :target_house, :target_senate, :target_email,
- :email_addresses, :target_bioguide_id, :bioguide_id, :alt_text_email_your_rep,
- :alt_text_look_up_your_rep, :alt_text_extra_fields_explain, :topic_category_id,
- :alt_text_look_up_helper, :alt_text_customize_message_helper, :campaign_tag
+ :id, :message, :subject, :state, :target_state_lower_chamber, :target_state_upper_chamber,
+ :target_governor, :target_email, :email_addresses, :target_bioguide_id,
+ :bioguide_id, :alt_text_email_your_rep, :alt_text_look_up_your_rep,
+ :alt_text_extra_fields_explain, :topic_category_id, :alt_text_look_up_helper,
+ :alt_text_customize_message_helper, :campaign_tag
],
congress_message_campaign_attributes: [
:id, :message, :subject, :target_house, :target_senate, { target_bioguide_list: [] },
diff --git a/app/controllers/tools_controller.rb b/app/controllers/tools_controller.rb
index d5fe9158d..1cb4f2a1a 100644
--- a/app/controllers/tools_controller.rb
+++ b/app/controllers/tools_controller.rb
@@ -123,7 +123,34 @@ def email
@actionPage = @action_page
render "email_target"
else
- redirect_to @action_page.email_campaign.service_uri(params[:service])
+ if params[:state_rep_email]
+ redirect_to @action_page.email_campaign.service_uri(params[:service], { email: params[:state_rep_email] })
+ else
+ redirect_to @action_page.email_campaign.service_uri(params[:service])
+ end
+ end
+ end
+
+ # GET /tools/state_reps
+ #
+ # This endpoint is hit by the js for state legislator lookup-by-address actions.
+ # It renders json containing html markup for presentation on the view
+ def state_reps
+ @email_campaign = EmailCampaign.find(params[:email_campaign_id])
+ @actionPage = @email_campaign.action_page
+ address = "#{params[:street_address]} #{params[:zipcode]}"
+ civic_api_response = CivicApi.state_rep_search(address, @email_campaign.leg_level)
+ @state_reps = JSON.parse(civic_api_response.body)["officials"]
+ state_rep_emails = []
+ @state_reps.each do |sr|
+ state_rep_emails << sr["emails"] if !sr["emails"].nil?
+ end
+ # single-rep lookup only
+ @state_rep_email = state_rep_emails.flatten.first
+ if @state_reps.present?
+ render json: { content: render_to_string(partial: "action_page/state_reps") }, status: 200
+ else
+ render json: { error: "No representatives found" }, status: 200
end
end
diff --git a/app/helpers/email_campaign_helper.rb b/app/helpers/email_campaign_helper.rb
new file mode 100644
index 000000000..54c9f28b1
--- /dev/null
+++ b/app/helpers/email_campaign_helper.rb
@@ -0,0 +1,15 @@
+module EmailCampaignHelper
+ def legislative_level_from_state_representative_info(legislator_info)
+ role = case legislator_info
+ when "legislatorLowerBody"
+ "state representative of the lower chamber"
+ when "legislatorUpperBody"
+ "state representative of the upper chamber"
+ when "headOfGovernment"
+ "Governor of the State"
+ else
+ "Invalid info"
+ end
+ role
+ end
+end
diff --git a/app/models/email_campaign.rb b/app/models/email_campaign.rb
index 604935b9b..e94989cb5 100644
--- a/app/models/email_campaign.rb
+++ b/app/models/email_campaign.rb
@@ -2,6 +2,9 @@ class EmailCampaign < ActiveRecord::Base
belongs_to :topic_category
has_one :action_page
+ # No DC
+ STATES = %w(AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY).freeze
+
def email_your_rep_text(default)
target_bioguide_text_or_default alt_text_email_your_rep, default
end
@@ -22,19 +25,39 @@ def extra_fields_explain_text(default)
target_bioguide_text_or_default alt_text_extra_fields_explain, default
end
+ def leg_level
+ return "legislatorLowerBody" if self.target_state_lower_chamber
+ return "legislatorUpperBody" if self.target_state_upper_chamber
+ return "headOfGovernment" if self.target_governor
+ ""
+ end
+
include ERB::Util
- def service_uri(service)
- mailto_addresses = email_addresses.split(/\s*,\s*/).map do |email|
- u(email.gsub(" ", "")).gsub("%40", "@")
+ def service_uri(service, opts = {})
+ mailto_addresses = opts[:email]
+ mailto_addresses ||= email_addresses
+ # look for custom email addresses set on the back end if there is no email param from the front-end,
+ # as is the case when we send state-level emails -- we cannot store these email address in our db,
+ # reason below:
+
+ # https://developers.google.com/terms#e_prohibitions_on_content
+ # Section 5.e.1., as of December 2022
+ # e. Prohibitions on Content
+ # Unless expressly permitted by the content owner or by applicable law, you will not, and will not permit your end users or others acting on your behalf to, do the following with content returned from the APIs:
+ # Scrape, build databases, or otherwise create permanent copies of such content, or keep cached copies longer than permitted by the cache header;
+
+ # results in comma-separated string of email addresses
+ default_mailto_addresses ||= mailto_addresses.split(/\s*,\s*/).map do |email|
+ u(email.gsub(" ", "")).gsub("%40", "@").gsub("%2B", "+")
end.join(",")
{
- default: "mailto:#{mailto_addresses}?#{query(body: message, subject: subject)}",
+ default: "mailto:#{default_mailto_addresses}?#{query(body: message, subject: subject)}",
- gmail: "https://mail.google.com/mail/?view=cm&fs=1{{ to: email_addresses, body: message, su: subject }.to_query}",
+ gmail: "https://mail.google.com/mail/?view=cm&fs=1{{ to: mailto_addresses, body: message, su: subject }.to_query}",
- hotmail: "https://outlook.live.com/default.aspx?rru=compose{{ to: email_addresses, body: message, subject: subject }.to_query}#page=Compose"
+ hotmail: "https://outlook.live.com/default.aspx?rru=compose{{ to: mailto_addresses, body: message, subject: subject }.to_query}#page=Compose"
}.fetch(service.to_sym)
end
diff --git a/app/views/action_page/_state_reps.html.erb b/app/views/action_page/_state_reps.html.erb
new file mode 100644
index 000000000..58740e8d3
--- /dev/null
+++ b/app/views/action_page/_state_reps.html.erb
@@ -0,0 +1,17 @@
+
+ <%= "This action is for the #{legislative_level_from_state_representative_info(@email_campaign.leg_level)}." %>
+ <% @state_reps.each do |sr| %>
+
+ <%= "Your representative is #{sr['name']}" %>.
+ <% if sr["emails"].present? %>
+ <%= "They can be reached at: #{sr['emails'].join(', ')}" %>
+ <% else %>
+ <%= "We could not find their email address." %>
+ <% end %>
+