diff --git a/Gemfile b/Gemfile index 606127c2..eba2379d 100644 --- a/Gemfile +++ b/Gemfile @@ -82,3 +82,5 @@ gem 'sneakers' gem 'chosen-rails' # jquery multiselect plugin for advanced search gem 'rails-reverse-proxy' gem 'pul_uv_rails', git: 'https://github.com/pulibrary/pul_uv_rails', branch: 'master' +gem 'mail_form' +gem 'simple_form' diff --git a/Gemfile.lock b/Gemfile.lock index eec4ea4a..cb657d5a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,7 +107,7 @@ GEM railties (>= 3.0) sass-rails (>= 3.2) cliver (0.3.2) - coderay (1.1.1) + coderay (1.1.2) coffee-rails (4.2.2) coffee-script (>= 2.2.0) railties (>= 4.0.0) @@ -205,6 +205,9 @@ GEM nokogiri (>= 1.5.9) mail (2.6.6) mime-types (>= 1.16, < 4) + mail_form (1.7.0) + actionmailer (>= 3.2, < 5.2) + activemodel (>= 3.2, < 5.2) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) @@ -339,6 +342,9 @@ GEM serverengine (1.5.11) sigdump (~> 0.2.2) sigdump (0.2.4) + simple_form (3.5.0) + actionpack (> 4, < 5.2) + activemodel (> 4, < 5.2) simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) @@ -428,6 +434,7 @@ DEPENDENCIES geoblacklight (~> 1.6.0) jbuilder (~> 2.5) jquery-rails + mail_form modernizr-rails neat (~> 1.8) omniauth-cas @@ -443,6 +450,7 @@ DEPENDENCIES rubocop (~> 0.42.0) rubocop-rspec (~> 1.6.0) sass-rails (~> 5.0) + simple_form sneakers solr_wrapper spring diff --git a/app/assets/stylesheets/components/forms.scss b/app/assets/stylesheets/components/forms.scss new file mode 100644 index 00000000..cd6b483a --- /dev/null +++ b/app/assets/stylesheets/components/forms.scss @@ -0,0 +1,41 @@ +@import "utils/variables"; + +// reset form styling cross-browser +.form-control, +.form-control:focus { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0); + -moz-box-shadow: 0 5px 15px rgba(0, 0, 0, 0); + -o-box-shadow: 0 5px 15px rgba(0, 0, 0, 0); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0); +} + +form table th:first-child { + width: 30px; +} + +.blacklight-feedback h1 { + margin-bottom: 0.25em; + display: block; +} +.blacklight-feedback small { + margin-bottom: 2em; + display: block; +} + +// overwrite bootstrap glyphicon for errors in input fields +.form-control-feedback { + right: 15px; +} + +.feedback-button { + margin-bottom: 15px; +} + +.has-error .error { + font-weight: bold; + color: $brand-danger; +} + +.blacklight-feedback-hidden { + display: none; +} diff --git a/app/assets/stylesheets/pulmap.scss b/app/assets/stylesheets/pulmap.scss index a9c032fd..3a7f4d9a 100644 --- a/app/assets/stylesheets/pulmap.scss +++ b/app/assets/stylesheets/pulmap.scss @@ -24,6 +24,7 @@ @import 'components/universal-viewer'; @import 'components/advanced'; @import 'components/buttons'; +@import 'components/forms'; @import 'shame-geo'; @import 'bootstrap-toggle'; diff --git a/app/assets/stylesheets/utils/variables.scss b/app/assets/stylesheets/utils/variables.scss index 9eebf0cf..afa602ae 100644 --- a/app/assets/stylesheets/utils/variables.scss +++ b/app/assets/stylesheets/utils/variables.scss @@ -12,10 +12,11 @@ $light-gray: #f5f4f1; $dark-orange: #89440a; $orange: #e87511; $blue: #337ab7; +$brand-danger: #a94442; //typography $font-serif: 'Droid Serif', Georgia, Times, serif !default; $font-sans: 'DejaVu Sans', 'Arial Unicode MS', Helvetica, sans-serif !default; $font-mono: 'Lucida Console', Monaco, monospace !default; $text-color: #000 !default; -$placeholder-text-color: rgb(204, 204, 204); \ No newline at end of file +$placeholder-text-color: rgb(204, 204, 204); diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb new file mode 100644 index 00000000..7c3c8556 --- /dev/null +++ b/app/controllers/feedback_controller.rb @@ -0,0 +1,39 @@ +class FeedbackController < ApplicationController + before_action :current_user_email + before_action :build_feedback_form, only: [:create] + + def new + @feedback_form = FeedbackForm.new if @feedback_form.nil? + @feedback_form.current_url = request.referer || root_url + end + + def create + respond_to do |format| + if @feedback_form.valid? + @feedback_form.deliver + format.js { flash.now[:notice] = I18n.t('blacklight.feedback.success') } + else + format.js { flash.now[:error] = @feedback_form.error_message } + end + end + end + + protected + + def build_feedback_form + @feedback_form = FeedbackForm.new(feedback_form_params) + @feedback_form.request = request + @feedback_form + end + + def feedback_form_params + params.require(:feedback_form).permit(:name, :email, :message, :current_url, :feedback_desc) + end + + def current_user_email + return if current_user.nil? + return if current_user.provider != 'cas' + @user_email = "#{current_user.uid}@princeton.edu" + @user_email + end +end diff --git a/app/models/feedback_form.rb b/app/models/feedback_form.rb new file mode 100644 index 00000000..79c2f269 --- /dev/null +++ b/app/models/feedback_form.rb @@ -0,0 +1,23 @@ +require 'mail_form' + +class FeedbackForm < MailForm::Base + attribute :name, validate: true + attribute :email, validate: /\A([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i + attribute :message, validate: true + attribute :current_url + attribute :feedback_desc, captcha: true + append :remote_ip, :user_agent + + def headers + { + subject: "#{I18n.t(:'blacklight.application_name')} Feedback Form", + to: ENV['MAP_FEEDBACK_TO'] || 'lsupport@princeton.edu', + from: %("#{name}" <#{email}>), + cc: ENV['MAP_FEEDBACK_CC'] + } + end + + def error_message + I18n.t(:'blacklight.feedback.error').to_s + end +end diff --git a/app/views/feedback/_form.html.erb b/app/views/feedback/_form.html.erb new file mode 100644 index 00000000..fb8c5451 --- /dev/null +++ b/app/views/feedback/_form.html.erb @@ -0,0 +1,54 @@ +<%= simple_form_for(@feedback_form, url: '/contact-us', id: 'feedback_form', remote: true, class: "form-horizontal" ) do |f| %> +
+ +
+ <%= f.text_field :name, class: 'form-control', error: 'Please provide a name' %> + <% unless @feedback_form.errors[:name].empty? %> + + (error) + This field <%= @feedback_form.errors[:name].first %> + <% end %> +
+
+
+ +
+ <%= f.email_field :email, class: 'form-control', error: 'Please provide a valid email address', value: @user_email %> + <% unless @feedback_form.errors[:email].empty? %> + + (error) + Email <%= @feedback_form.errors[:email].first %> + <% end %> +
+
+
+ +
+ <%= f.text_area :message, class: 'form-control', rows: '5', error: 'Please describe the problem you encountered or ask a question.' %> + <% unless @feedback_form.errors[:message].empty? %> + This field <%= @feedback_form.errors[:message].first %> + <% end %> +
+
+
+ +
+ <%= f.text_field :feedback_desc, class: 'form-control', error: 'Please describe' %> +
+
+ + <%= f.hidden_field :current_url %> +
+
+ <%= f.submit t('blacklight.sms.form.submit'), class: 'btn btn-primary pull-right' %> +
+
+<% end %> diff --git a/app/views/feedback/_return.html.erb b/app/views/feedback/_return.html.erb new file mode 100644 index 00000000..caaa5568 --- /dev/null +++ b/app/views/feedback/_return.html.erb @@ -0,0 +1,2 @@ +

<%= I18n.t('blacklight.feedback.confirmation') %>

+<%= link_to t('blacklight.feedback.return').html_safe, @feedback_form.current_url, class: 'btn btn-default feedback-button' %> diff --git a/app/views/feedback/create.js.erb b/app/views/feedback/create.js.erb new file mode 100644 index 00000000..e91f5024 --- /dev/null +++ b/app/views/feedback/create.js.erb @@ -0,0 +1,16 @@ +$(".flash_messages").html("<%= escape_javascript(render :partial=>'shared/flash_msg', layout: 'shared/flash_messages') %>"); + +setTimeout(function() { + $(".flash_messages .alert-info, .flash_messages .alert-success").fadeOut('slow', function(){ + $(".flash_messages .alert-info, .flash_messages .alert-success").remove(); + }); +}, 3000); + +<% if !@feedback_form.errors.full_messages.empty? %> + $("#new_feedback_form").replaceWith("<%= escape_javascript(render 'form') %>"); + $(".error").each(function( index ) { + $(this).parent().addClass('has-error'); + }); +<% else %> + $("#new_feedback_form").replaceWith("<%= escape_javascript(render 'return') %>"); +<% end %> diff --git a/app/views/feedback/new.html.erb b/app/views/feedback/new.html.erb new file mode 100644 index 00000000..9fbdc558 --- /dev/null +++ b/app/views/feedback/new.html.erb @@ -0,0 +1,9 @@ +<%= render 'shared/start_over_row' %> +
+

Contact Us

+ *All fields are required +
+ +
+ <%= render 'form' %> +
diff --git a/app/views/shared/_pul_branding.html.erb b/app/views/shared/_pul_branding.html.erb index f5317c70..03a1c967 100644 --- a/app/views/shared/_pul_branding.html.erb +++ b/app/views/shared/_pul_branding.html.erb @@ -22,7 +22,7 @@ diff --git a/app/views/shared/_this_is_beta.html.erb b/app/views/shared/_this_is_beta.html.erb index a4ed68e3..8588ca3b 100644 --- a/app/views/shared/_this_is_beta.html.erb +++ b/app/views/shared/_this_is_beta.html.erb @@ -1,6 +1,6 @@

This is Beta Software
- There may be features missing. Please send us a note + There may be features missing. Please send us a note if you have feedback or think you have found an error.

diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb new file mode 100644 index 00000000..f306c195 --- /dev/null +++ b/config/initializers/simple_form.rb @@ -0,0 +1,169 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, class: :input, + hint_class: :field_with_hint, error_class: :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + # and/or database column lengths + b.optional :maxlength + + # Calculate minlength from length validations for string inputs + b.optional :minlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } + + ## full_messages_for + # If you want to display the full error message for the attribute, you can + # use the component :full_error, like: + # + # b.use :full_error, wrap_with: { tag: :span, class: :error } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :nested + + # Default class for buttons + config.button_class = 'btn' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'error_notification' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the default class to be used on forms. Can be overriden + # with `html: { :class }`. Defaulting to none. + # config.default_form_class = nil + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'checkbox' + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' +end diff --git a/config/locales/blacklight.en.yml b/config/locales/blacklight.en.yml index 2e7d9080..6b840d01 100644 --- a/config/locales/blacklight.en.yml +++ b/config/locales/blacklight.en.yml @@ -13,3 +13,8 @@ en: header_links: account_page: 'Your Account' back_to_search: ' ' + feedback: + success: 'Your comments have been submitted' + error: 'Please fill in the required form values.' + return: ' Back to the previous page.' + confirmation: 'Thank you for your comments. They are helpful as we work to improve this new Library service.' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml new file mode 100644 index 00000000..23743833 --- /dev/null +++ b/config/locales/simple_form.en.yml @@ -0,0 +1,31 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/config/routes.rb b/config/routes.rb index 855cf0be..da955999 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,9 @@ end end + get 'contact-us', to: 'feedback#new' + post 'contact-us', to: 'feedback#create' + mount Geoblacklight::Engine => 'geoblacklight' concern :gbl_exportable, Geoblacklight::Routes::Exportable.new diff --git a/lib/templates/erb/scaffold/_form.html.erb b/lib/templates/erb/scaffold/_form.html.erb new file mode 100644 index 00000000..201a069e --- /dev/null +++ b/lib/templates/erb/scaffold/_form.html.erb @@ -0,0 +1,13 @@ +<%%= simple_form_for(@<%= singular_table_name %>) do |f| %> + <%%= f.error_notification %> + +
+ <%- attributes.each do |attribute| -%> + <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> + <%- end -%> +
+ +
+ <%%= f.button :submit %> +
+<%% end %> diff --git a/spec/controllers/feedback_controller_spec.rb b/spec/controllers/feedback_controller_spec.rb new file mode 100644 index 00000000..4bad9d60 --- /dev/null +++ b/spec/controllers/feedback_controller_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +describe FeedbackController, type: :routing do + describe 'routing' do + it '/feedback routes to a new form' do + expect(get: '/contact-us').to route_to('feedback#new') + end + + it '/contact-us posts to create' do + expect(post: '/contact-us').to route_to('feedback#create') + end + end +end diff --git a/spec/models/feedback_form_spec.rb b/spec/models/feedback_form_spec.rb new file mode 100644 index 00000000..0cf6b69c --- /dev/null +++ b/spec/models/feedback_form_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe FeedbackForm do + let(:feedback) { described_class.new(params) } + let(:params) do + { name: 'Bob Smith', + email: 'bsmith@university.edu', + message: 'Awesome Site!!!!' } + end + describe 'A vaild Feedback Email' do + it 'is valid' do + expect(feedback.valid?).to be_truthy + end + + it 'Can deliver a message' do + expect(feedback).to respond_to(:deliver) + end + + context 'It has invalid data' do + let(:params) do + { name: 'Bar', + email: 'foo', + message: nil } + end + it 'is invalid' do + expect(feedback.valid?).to be_falsey + end + end + end + + describe '#headers' do + it 'returns mail headers' do + expect(feedback.headers).to be_truthy + end + + it "Contains the submitter's email address" do + expect(feedback.headers[:from]).to eq('"Bob Smith" ') + end + end + + describe 'error_message' do + it 'returns the configured error string' do + expect(feedback.error_message).to eq(I18n.t('blacklight.feedback.error')) + end + end +end diff --git a/spec/views/feedback/feedback_view_spec.rb b/spec/views/feedback/feedback_view_spec.rb new file mode 100644 index 00000000..4a5ac5cf --- /dev/null +++ b/spec/views/feedback/feedback_view_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe 'Feedback Form', type: :feature do + context 'User has not signed in' do + before(:each) do + visit('/catalog/princeton-8c97ks94m') + click_link('Contact Us') + end + + it 'Displays an empty form' do + expect(page).to have_content 'Contact Us' + expect(page).to have_field 'feedback_form_name' + expect(page).to have_field 'feedback_form_email' + expect(page).to have_field 'feedback_form_message' + end + + it 'Fill ins and submits a valid form Form', js: true do + fill_in 'feedback_form_name', with: 'Joe Smith' + fill_in 'feedback_form_email', with: 'jsmith@university.edu' + fill_in 'feedback_form_message', with: 'awesome site' + click_button 'Send' + expect(page).to have_content(I18n.t('blacklight.feedback.confirmation')) + end + + describe 'It provides error messages', js: true do + it 'When the name field is not filled in' do + fill_in 'feedback_form_email', with: 'foo@university.edu' + fill_in 'feedback_form_message', with: 'awesome site' + click_button 'Send' + expect(page).to have_content(I18n.t('blacklight.feedback.error')) + expect(page).to have_selector('.has-error') + end + end + end + + context 'Princeton Community User has signed in' do + let(:user) { FactoryGirl.create(:user) } + before { OmniAuth.config.test_mode = true } + it 'Populates Email Field' do + sign_in user + visit('/catalog/princeton-8c97ks94m') + click_link('Contact Us') + expect(page).to have_field('feedback_form_email', with: "#{user.uid}@princeton.edu") + end + end +end