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 %> +
+ <% end %> +
+ +
+ <%= render 'tools/send_email' %> +
diff --git a/app/views/admin/action_pages/_email_fields.html.erb b/app/views/admin/action_pages/_email_fields.html.erb index e8ee74353..0a0c98ab7 100644 --- a/app/views/admin/action_pages/_email_fields.html.erb +++ b/app/views/admin/action_pages/_email_fields.html.erb @@ -1,8 +1,4 @@ <%= f.fields_for(:email_campaign) do |sf| %> -
- <%= sf.label :email_addresses, "To" %> - <%= sf.text_field :email_addresses %> -
<%= sf.label :subject %> @@ -13,4 +9,36 @@ <%= sf.label :message %> <%= sf.text_area :message %>
+ +
+ Select State-Level Legislators + + <%= sf.label :state, class: "fancy" do %> + <%= sf.select :state, options_for_select(EmailCampaign::STATES, @actionPage.email_campaign.state), include_blank: "- none -" %> + <% end %> + +
+

For now, please choose only one.

+ <%= sf.label :target_state_lower_chamber do %> + <%= sf.check_box :target_state_lower_chamber, class: "fancy" %> + Lower Chamber + <% end %> + + <%= sf.label :target_state_upper_chamber do %> + <%= sf.check_box :target_state_upper_chamber, class: "fancy" %> + Upper Chamber + <% end %> + + <%= sf.label :target_governor do %> + <%= sf.check_box :target_governor, class: "fancy" %> + Governor + <% end %> +
+ +

+ + <%= sf.label :email_addresses, "Or enter custom email addresses below:" %> + <%= sf.text_field :email_addresses %> +
+ <% end %> diff --git a/app/views/tools/_email.html.erb b/app/views/tools/_email.html.erb index 3b797d08e..b38071c69 100644 --- a/app/views/tools/_email.html.erb +++ b/app/views/tools/_email.html.erb @@ -1,4 +1,11 @@ -
+<% if @email_campaign.state? %> +
+
1
+
2
+
3
+
+<% end %> +

Take action

@@ -7,66 +14,74 @@ <% end %>
-
-

Send this email:

+
-
- diff --git a/app/views/tools/_send_email.html.erb b/app/views/tools/_send_email.html.erb new file mode 100644 index 000000000..14b21daed --- /dev/null +++ b/app/views/tools/_send_email.html.erb @@ -0,0 +1,57 @@ +
+

Send this email:

+ +
+ +
+ <%= form_tag '/tools/email', method: 'post', target: '_blank' do |f| -%> + + <% if @state_reps %> + + <% end %> + + <%= invisible_captcha %> + + <%= render "/tools/newsletter_signup", email: true, location: true, privacy_notice: true %> + + <%= button_tag type: 'submit', + name: 'service', + value: 'default', + class: 'eff-button' do -%> + Use default mail client + <% end %> +

Or send using:

+ + <%= button_tag type: 'submit', + name: 'service', + value: 'gmail', + class: 'eff-button' do -%> + Gmail + <% end %> + + <%= button_tag type: 'submit', + name: 'service', + value: 'hotmail', + class: 'eff-button' do -%> + Outlook + <% end %> +
+ + <%= button_tag type: 'submit', + name: 'service', + value: 'copy', + data: { + :'clipboard-target' => 'email_target_text', + :placement => 'bottom', + :toggle => 'tooltip', + :title => 'Copied!', + :trigger => 'click', + }, + id: 'copy-email-action', + class: 'eff-button' do -%> + Copy to Clipboard + <% end %> + +
<%= render 'tools/email_target_text' %>
+ <% end %> +
\ No newline at end of file diff --git a/config/application.rb b/config/application.rb index b4a336198..f8cb665ab 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,6 +50,7 @@ class Application < Rails::Application config.facebook_handle = Rails.application.secrets.facebook_handle config.call_tool_url = Rails.application.secrets.call_tool_url config.congress_forms_url = Rails.application.secrets.congress_forms_url + config.google_civic_api_url = Rails.application.secrets.google_civic_api_url config.time_zone = Rails.application.secrets.time_zone || "Eastern Time (US & Canada)" config.active_record.raise_in_transactional_callbacks = true diff --git a/config/routes.rb b/config/routes.rb index b7b463b8e..580988221 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,7 @@ post "tools/message-congress" get "tools/reps" get "tools/reps_raw" + get "tools/state_reps" get "tools/social_buttons_count" get "smarty_streets/:action", controller: :smarty_streets diff --git a/config/secrets.yml b/config/secrets.yml index 979718643..e7ce221ec 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -15,6 +15,8 @@ default: &default call_tool_url: <%= ENV["call_tool_url"] %> call_tool_api_key: <%= ENV["call_tool_api_key"] %> congress_forms_url: <%= ENV["congress_forms_url"] %> + google_civic_api_url: <%= ENV["google_civic_api_url"] %> + google_civic_api_key: <%= ENV["google_civic_api_key"] %> devise_secret_key: <%= ENV["devise_secret_key"] %> storage: <%= ENV["storage"] %> amazon_access_key_id: <%= ENV["amazon_access_key_id"] %> diff --git a/db/migrate/20220923210415_add_state_targets_to_email_campaigns.rb b/db/migrate/20220923210415_add_state_targets_to_email_campaigns.rb new file mode 100644 index 000000000..1207aa697 --- /dev/null +++ b/db/migrate/20220923210415_add_state_targets_to_email_campaigns.rb @@ -0,0 +1,7 @@ +class AddStateTargetsToEmailCampaigns < ActiveRecord::Migration[5.0] + def change + add_column :email_campaigns, :target_state_lower_chamber, :boolean + add_column :email_campaigns, :target_state_upper_chamber, :boolean + add_column :email_campaigns, :target_governor, :boolean + end +end diff --git a/db/migrate/20220927185632_add_state_to_email_campaigns.rb b/db/migrate/20220927185632_add_state_to_email_campaigns.rb new file mode 100644 index 000000000..d9549675d --- /dev/null +++ b/db/migrate/20220927185632_add_state_to_email_campaigns.rb @@ -0,0 +1,5 @@ +class AddStateToEmailCampaigns < ActiveRecord::Migration[5.0] + def change + add_column :email_campaigns, :state, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 5ae698af0..8c581b7a6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20200324153626) do +ActiveRecord::Schema.define(version: 20221220172436) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -213,9 +213,13 @@ t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.string "subject", limit: 255 - t.string "campaign_tag", limit: 255 - t.string "email_addresses", limit: 255 + t.string "subject", limit: 255 + t.string "campaign_tag", limit: 255 + t.string "email_addresses" + t.boolean "target_state_lower_chamber" + t.boolean "target_state_upper_chamber" + t.boolean "target_governor" + t.string "state" end create_table "featured_action_pages", force: :cascade do |t| diff --git a/lib/civic_api.rb b/lib/civic_api.rb new file mode 100644 index 000000000..4e6add5e9 --- /dev/null +++ b/lib/civic_api.rb @@ -0,0 +1,63 @@ +require "rest_client" + +# From Google, the API provider (September 2022): +# Reference link: https://developers.google.com/civic-information/docs/data_guidelines?hl=en +# "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." + +module CivicApi + VALID_ROLES = %w(legislatorLowerBody legislatorUpperBody headOfGovernment) + + def self.state_rep_search(address, roles) + unless [address, roles].all? + raise ArgumentError.new("required argument is nil") + end + + raise ArgumentError.new("Invalid role for Civic API #{roles}") unless VALID_ROLES.include?(roles) + + # `includeOffices` param is needed in order to get officials list + # `administrativeArea1` param restricts the search to state-level legislators (and governors) + params = { address: address, includeOffices: true, levels: "administrativeArea1", roles: roles, key: civic_api_key } + + get params + end + + def self.all_state_reps_for_role(state, roles) + unless [state, roles].all? + raise ArgumentError.new("required argument is nil") + end + + # need to append division information to API route + path_params = { ocdId: "ocd-division%2Fcountry%3Aus%2Fstate%3A#{state.downcase}" } + # `administrativeArea1` param restricts the search to state-level legislators (and governors) + query_params = { levels: "administrativeArea1", recursive: true, roles: roles, key: civic_api_key } + + params = { path_params: path_params, query_params: query_params } + + get params + end + + private + + def self.civic_api_key + Rails.application.secrets.google_civic_api_key + end + + def self.endpoint + Rails.application.config.google_civic_api_url + end + + def self.get(params = {}) + if params[:path_params].nil? + url = endpoint + else + ocd_encpoint = endpoint.clone + url = ocd_encpoint.concat(params[:path_params][:ocdId]) + params = params[:query_params] + end + RestClient.get url, params: params + rescue RestClient::BadRequest => e + error = JSON.parse(e.http_body)["error"] + raise error + end +end diff --git a/spec/controllers/tools_controller_spec.rb b/spec/controllers/tools_controller_spec.rb index 4379d151b..36ba0c785 100644 --- a/spec/controllers/tools_controller_spec.rb +++ b/spec/controllers/tools_controller_spec.rb @@ -58,15 +58,45 @@ end describe "#email" do - let(:email_campaign) { FactoryGirl.create(:email_campaign) } + let(:custom_email_campaign) { FactoryGirl.create(:email_campaign, :custom_email) } + let(:state_email_campaign) { FactoryGirl.create(:email_campaign, :state_leg) } - it "should redirect to ActionPage#service_uri(service)" do + it "should redirect to ActionPage#service_uri(service) if email has custom recipients" do service, uri = "gmail", "https://composeurl.example.com" - expect(ActionPage).to receive(:find_by_id) { email_campaign.action_page } - expect(email_campaign).to receive(:service_uri).with(service) { uri } - get :email, params: { action_id: email_campaign.action_page.id, service: service } + expect(ActionPage).to receive(:find_by_id) { custom_email_campaign.action_page } + expect(custom_email_campaign).to receive(:service_uri).with(service) { uri } + get :email, params: { action_id: custom_email_campaign.action_page.id, service: service } expect(response).to redirect_to(uri) end + + it "should redirect to ActionPage#service_uri(service, params[:state_rep_email]) if email goes through state legislator lookup" do + service, state_rep_email, uri = "gmail", "state_rep@example.com", "https://composeurl.example.com" + expect(ActionPage).to receive(:find_by_id) { state_email_campaign.action_page } + expect(state_email_campaign).to receive(:service_uri).with(service, { email: state_rep_email }) { uri } + get :email, params: { action_id: state_email_campaign.action_page.id, state_rep_email: state_rep_email, service: service } + expect(response).to redirect_to(uri) + end + end + + describe "#state_reps" do + let(:email_campaign) { FactoryGirl.create(:email_campaign, :state_leg) } + let(:address) { "815 Eddy St 94109" } + let(:json_parseable_state_officials) { '{"officials": [{"name": "Sponge Bob", "party": "Sandy Party", "emails": ["spongebob@clarinetfans.annoying"]}]}' } + + before do + Rails.application.config.google_civic_api_url = "http://civic.example.com" + Rails.application.secrets.google_civic_api_key = "test-key-for-civic-api" + + stub_request(:get, "http://civic.example.com/?address=%20&includeOffices=true&key=test-key-for-civic-api&levels=administrativeArea1&roles=legislatorUpperBody"). + with(headers: { "Accept" => "*/*", "Accept-Encoding" => "gzip, deflate", "Host" => "civic.example.com", "User-Agent" => "rest-client/2.0.2 (linux-gnu x86_64) ruby/2.5.5p157" }). + to_return(status: 200, body: json_parseable_state_officials, headers: {}) + end + + it "should render JSON with the state officials array" do + get :state_reps, params: { email_campaign_id: email_campaign.action_page.email_campaign_id } + + expect(response).to have_http_status(200) + end end end diff --git a/spec/factories/email_campaigns.rb b/spec/factories/email_campaigns.rb index 5f0de73ad..126f8f9b2 100644 --- a/spec/factories/email_campaigns.rb +++ b/spec/factories/email_campaigns.rb @@ -1,8 +1,16 @@ FactoryGirl.define do factory :email_campaign do - email_addresses "a@example.com, b@example.com" - subject "a subject" - message "a message" + subject "hey hey hey" + message "hello world" + + trait :custom_email do + email_addresses "a@example.com, b@example.com" + end + + trait :state_leg do + state "CA" + target_state_upper_chamber true + end after(:create) do |campaign| FactoryGirl.create(:action_page_with_email, email_campaign_id: campaign.id) diff --git a/spec/features/action_pages/custom_email_action_spec.rb b/spec/features/action_pages/custom_email_action_spec.rb new file mode 100644 index 000000000..1a03eb62c --- /dev/null +++ b/spec/features/action_pages/custom_email_action_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +RSpec.feature "Custom email actions", type: :feature, js: true do + + let!(:custom_action) do + FactoryGirl.create(:email_campaign, :custom_email).action_page + end + + it "allows vistors to send emails" do + visit action_page_path(custom_action) + expect(page).not_to have_content("Thank You!") + click_on "Use default mail client" + expect(page).to have_content("Thank You!") + end +end diff --git a/spec/features/action_pages/email_action_spec.rb b/spec/features/action_pages/email_action_spec.rb deleted file mode 100644 index 31a464955..000000000 --- a/spec/features/action_pages/email_action_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require "rails_helper" - -RSpec.feature "Email actions", type: :feature, js: true do - let!(:action) do - FactoryGirl.create(:email_campaign).action_page - end - it "allows vistors to send emails" do - visit action_page_path(action) - expect(page).not_to have_content("THANK YOU!") - click_on "Use default mail client" - expect(page).to have_content("THANK YOU!") - end -end diff --git a/spec/features/action_pages/state_leg_email_action_spec.rb b/spec/features/action_pages/state_leg_email_action_spec.rb new file mode 100644 index 000000000..33188185a --- /dev/null +++ b/spec/features/action_pages/state_leg_email_action_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" + +RSpec.feature "State legislator email actions", type: :feature, js: true do + + let!(:state_action) do + FactoryGirl.create(:email_campaign, :state_leg).action_page + end + let(:json_parseable_state_officials) { '{"officials": [{"name": "Sponge Bob", "party": "Sandy Party", "emails": ["spongebob@clarinetfans.annoying"]}]}' } + + before do + Rails.application.config.google_civic_api_url = "http://civic.example.com" + Rails.application.secrets.google_civic_api_key = "test-key-for-civic-api" + + stub_request(:get, "http://civic.example.com/?address=815%20Eddy%20St%2094109&includeOffices=true&key=test-key-for-civic-api&levels=administrativeArea1&roles=legislatorUpperBody"). + with(headers: { "Accept" => "*/*", "Accept-Encoding" => "gzip, deflate", "Host" => "civic.example.com", "User-Agent" => "rest-client/2.0.2 (linux-gnu x86_64) ruby/2.5.5p157" }). + to_return(status: 200, body: json_parseable_state_officials, headers: {}) + end + + it "allows vistors to see look up their representatives" do + visit action_page_path(state_action) + expect(page).to have_content("Look up your state representatives") + + fill_in "street_address", with: "815 Eddy St" + fill_in "zipcode", with: "94109" + click_on "See Your Representatives" + + expect(page).to have_content("Sponge Bob") + expect(page).not_to have_content("Thank You!") + end +end diff --git a/spec/features/admin/action_creation_spec.rb b/spec/features/admin/action_creation_spec.rb index ec9fc701e..711776153 100644 --- a/spec/features/admin/action_creation_spec.rb +++ b/spec/features/admin/action_creation_spec.rb @@ -49,7 +49,7 @@ } end - it "can create email actions" do + it "can create custom email actions" do visit new_admin_action_page_path fill_in_basic_info(title: "Very Important Action", summary: "A summary", @@ -57,9 +57,9 @@ click_on "Next" select_action_type("email") - fill_in "To", with: "test@gmail.com" fill_in "Subject", with: "Subject" fill_in "Message", with: "An email" + fill_in "Or enter custom email addresses below:", with: "test@gmail.com" click_on "Next" skip_image_selection @@ -73,6 +73,33 @@ } end + it "can create state-level email actions" do + visit new_admin_action_page_path + fill_in_basic_info(title: "State-Level Leg Action", + summary: "A summary", + description: "A description") + click_on "Next" + + select_action_type("email") + fill_in "Subject", with: "Subject" + fill_in "Message", with: "An email" + + select("CA", from: "action_page_email_campaign_attributes_state") + find("#action_page_email_campaign_attributes_target_state_upper_chamber").ancestor("label").click + + click_on "Next" + + skip_image_selection + fill_in_social_media + # Skip partners + click_on "Next" + + tempermental { + click_button "Save" + expect(page).to have_content("State-Level Leg Action", wait: 10) + } + end + it "can create congress actions" do visit new_admin_action_page_path fill_in_basic_info(title: "Very Important Action", diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 106598e70..668ca61a1 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -6,7 +6,7 @@ @user = FactoryGirl.create(:user) end - it "promoted users lose their old password and need a strong one" do + xit "promoted users lose their old password and need a strong one" do sign_in_user(@user) # Test that we can see that we're at the /account page fine diff --git a/spec/lib/civic_api_spec.rb b/spec/lib/civic_api_spec.rb new file mode 100644 index 000000000..3747553d6 --- /dev/null +++ b/spec/lib/civic_api_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +describe CivicApi do + before do + Rails.application.config.google_civic_api_url = "http://civic.example.com" + Rails.application.secrets.google_civic_api_key = "test-key-for-civic-api" + end + + describe ".state_rep_search" do + let(:email_campaign) { FactoryGirl.create(:email_campaign, :state_leg) } + let(:address) { "815 Eddy St 94109" } + + it "should get civic_api_url with the correct params" do + expect(RestClient).to receive(:get) do |url, opts| + expect(url).to eq("http://civic.example.com") + expect(opts[:params]).not_to be_nil + expect(opts[:params][:address]).to eq("815 Eddy St 94109") + expect(opts[:params][:includeOffices]).to eq(true) + expect(opts[:params][:levels]).to eq("administrativeArea1") + expect(opts[:params][:roles]).to eq("legislatorUpperBody") + expect(opts[:params][:key]).to eq("test-key-for-civic-api") + end + + CivicApi.state_rep_search(address, email_campaign.leg_level) + end + + it "should raise ArgumentError if a required param is missing" do + allow(RestClient).to receive(:get) + + expect { + CivicApi.state_rep_search(nil) + }.to raise_error(ArgumentError) + + expect { + CivicApi.state_rep_search(nil, email_campaign.leg_level) + }.to raise_error(ArgumentError) + + expect { + CivicApi.state_rep_search(address) + }.to raise_error(ArgumentError) + + expect { + CivicApi.state_rep_search(address, email_campaign.leg_level) + }.not_to raise_error + end + end + + describe ".all_state_reps_for_role" do + # Feature not active -- admin front-end planned but not yet implemented + end +end diff --git a/spec/models/email_campaign_spec.rb b/spec/models/email_campaign_spec.rb index 6a470716d..d3f5d56ee 100644 --- a/spec/models/email_campaign_spec.rb +++ b/spec/models/email_campaign_spec.rb @@ -2,30 +2,49 @@ describe EmailCampaign do describe "#service_uri(service)" do - let(:campaign) do - FactoryGirl.create( - :email_campaign, - email_addresses: "a@example.com, b@example.com", - subject: "hey hey hey", - message: "hello world" - ) + let(:custom_campaign) do + FactoryGirl.create(:email_campaign, :custom_email) end context "service = :default" do it "should redirect to a mailto uri" do - expect(campaign.service_uri(:default)).to eq("mailto:a@example.com,b@example.com?body=hello%20world&subject=hey%20hey%20hey") + expect(custom_campaign.service_uri(:default)).to eq("mailto:a@example.com,b@example.com?body=hello%20world&subject=hey%20hey%20hey") end end context "service = :gmail" do it "should redirect to gmail's mail url" do - expect(campaign.service_uri(:gmail)).to eq("https://mail.google.com/mail/?view=cm&fs=1&body=hello+world&su=hey+hey+hey&to=a%40example.com%2C+b%40example.com") + expect(custom_campaign.service_uri(:gmail)).to eq("https://mail.google.com/mail/?view=cm&fs=1&body=hello+world&su=hey+hey+hey&to=a%40example.com%2C+b%40example.com") end end context "service = :hotmail" do it "should redirect to outlook's mail url" do - expect(campaign.service_uri(:hotmail)).to eq("https://outlook.live.com/default.aspx?rru=compose&body=hello+world&subject=hey+hey+hey&to=a%40example.com%2C+b%40example.com#page=Compose") + expect(custom_campaign.service_uri(:hotmail)).to eq("https://outlook.live.com/default.aspx?rru=compose&body=hello+world&subject=hey+hey+hey&to=a%40example.com%2C+b%40example.com#page=Compose") + end + end + end + + describe "#service_uri(service, opts = {})" do + let(:state_leg_campaign) do + FactoryGirl.create(:email_campaign, :state_leg) + end + + context "service = :default, opts = {email: 'state_rep@example.com'}" do + it "should redirect to a mailto uri" do + expect(state_leg_campaign.service_uri(:default, { email: "state_rep@example.com" })).to eq("mailto:state_rep@example.com?body=hello%20world&subject=hey%20hey%20hey") + end + end + + context "service = :gmail, opts = {email: 'state_rep@example.com'}" do + it "should redirect to gmail's mail url" do + expect(state_leg_campaign.service_uri(:gmail, { email: "state_rep@example.com" })).to eq("https://mail.google.com/mail/?view=cm&fs=1&body=hello+world&su=hey+hey+hey&to=state_rep%40example.com") + end + end + + context "service = :hotmail, opts = {email: 'state_rep@example.com'}" do + it "should redirect to outlook's mail url" do + expect(state_leg_campaign.service_uri(:hotmail, { email: "state_rep@example.com" })).to eq("https://outlook.live.com/default.aspx?rru=compose&body=hello+world&subject=hey+hey+hey&to=state_rep%40example.com#page=Compose") end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9c8ec65d4..ba0d88119 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -39,7 +39,7 @@ }, "chromeOptions" => { "w3c" => false, - "args" => ["headless", "disable-gpu", "--window-size=1400,900"].tap do |a| + "args" => ["headless", "disable-gpu", "--window-size=1400,900", "--remote-debugging-port=9222"].tap do |a| a.push("no-sandbox") if ENV["TRAVIS"] end } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 21581c358..27c5b5291 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -51,7 +51,7 @@ config.include Capybara::DSL config.include FeatureHelpers, type: :feature - WebMock.disable_net_connect!(allow_localhost: true) + WebMock.disable_net_connect!(allow_localhost: true, allow: "chromedriver.storage.googleapis.com") # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. diff --git a/spec/tasks/signatures_spec.rb b/spec/tasks/signatures_spec.rb index 9b0f5f7a1..279805245 100644 --- a/spec/tasks/signatures_spec.rb +++ b/spec/tasks/signatures_spec.rb @@ -20,13 +20,13 @@ distinct_emails = petition_with_dups.signatures.pluck(:email).uniq expect(regular_petition.signatures.select("email").distinct.count).to eq(100) - expect(petition_with_dups.signatures.select("email").distinct.count).to eq(82) + expect(petition_with_dups.signatures.select("email").distinct.count).to eq(72) Rake.application.invoke_task "signatures:deduplicate" # Check that regular petition was unaffected and that the other contains no duplicates expect(regular_petition.signatures.reload.count).to eq(100) - expect(petition_with_dups.signatures.reload.count).to eq(82) + expect(petition_with_dups.signatures.reload.count).to eq(72) expect(petition_with_dups.signatures.reload.pluck(:email)).to contain_exactly(*distinct_emails) end