Working with Blacklight Part 1 – email search history

This is the first of a series of posts about configuring and modifying Blacklight at UCSF. It’s less about emailing search history and more about getting familiar with Blacklight by picking something to modify and seeing how it goes…

We are developing a front end for a Solr repository of tobacco industry documents. Blacklight, out of the box, provides a lot of what we’d need. We decided to come up with a business requirement that isn’t currently in Blacklight and see what it’s like working with the code.

We decided to try emailing a list of saved searches. This blog post is a write up of my notes. I’m hoping it will be useful as a tutorial/exercise for developers looking to get up to speed with with working with Blacklight code.

You should be able to start with a clean installation of Blacklight and add the functionality to email search histories from the notes here. A completed version is available on github at https://github.com/ucsf-ckm/blacklight-email-tutorial.

 Step 1: Get a clean installation of blacklight app going

Use the quickstart guide at
https://github.com/projectblacklight/blacklight/wiki/Quickstart

(do all of it, including the Jetty Solr part).

Step 2: Configure an SMTP mailer (optional)

This is optional, but I prefer not to use a sytem mailer on my dev machine.

in config/environments/development.rb


# Expands the lines which load the assets
config.assets.debug = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: 'myhost.com' }
config.action_mailer.perform_deliveries = true

config.action_mailer.smtp_settings = {
    :address => "smtp.gmail.com",
    :port => 587,
    :domain => "localhost:3000",
    :user_name => "username",
    :password => "password",
    :authentication => "plain",
    :enable_starttls_auto => true
}

Test this to be sure it works by creating and emailing a Blacklight bookmark to yourself (the next steps won’t work if this doesn’t work).

Step 3: Add a feature to send an email history through the saved searches page

1) Create and save a few searches

Do a few searches (anything you like), then go to Saved Searches and save a few of them.
You’ll notice that unlike the Bookmarks page, there’s no functionality to email your saved searches yet.

2) Add a button to email saved searches.

First, we need to add an email button to the saved searches page. We’ll piggyback on the email button used for bookmarks.

If you look in your views directory, you won’t see any code in your local app. It is currently stored in the Blacklight gem. Because our customizations are local we (of course) won’t hack the gem directly; we’ll add or override things in our local app.

You can follow this tutorial without looking at the Blacklight gem source directly, but I’d recommend unpacking the gem so that you can look at the code. Do not change the gem code.

We’ll need to add an email button to the Saved Searches page. To do this, we’ll need to both create a new view and override an existing view in the Blacklight gem.

The view code for the main display page for saved searches is in /app/views/saved_searches/index.html

We’ll override this page locally to add the email button. To do this, create a new directory called saved_searches in the views directory and create a file called index.html.erb with this content (modified from the same file in the gem itself):


<div id="content" class="span9">
<h1><%= t('blacklight.saved_searches.title') %></h1>

<%- if current_or_guest_user.blank? -%>
<h2><%= t('blacklight.saved_searches.need_login') %></h2>
<%- elsif @searches.blank? -%>
<h2><%= t('blacklight.saved_searches.no_searches') %></h2>
<%- else -%>
<p>
<%= link_to t('blacklight.saved_searches.clear.action_title'), clear_saved_searches_path, :method => :delete, :data => { :confirm => t('blacklight.saved_searches.clear.action_confirm') } %>
</p>

<h2><%= t('blacklight.saved_searches.list_title') %></h2>
<%= render 'search_tools' %>

<table class="table table-striped">
<%- @searches.each do |search| -%>
<tr>
<td><%= link_to_previous_search(search.query_params) %></td>
<td><%= button_to t('blacklight.saved_searches.delete'), forget_search_path(search.id) %></td>
</tr>
<%- end -%>
</table>

<%- end -%>

</div>

This will add a search tools (through <%= render ‘search_tools’ %>) to the index page.

The _search_tools.html.erb partial doesn’t exist in the gem. To create one, we’ll copy and modify the _tools.html.erb partial from the gem (used to render the various tools for bookmarks) to create a partial _search_tools.html.erb (also in the saved_searches view folder).


<ul class="bookmarkTools">

<li class="email">
<%= link_to t('blacklight.tools.email'), email_search_path(:id => @searches), {:id => 'emailLink', :class => 'lightboxLink'} %>
</li>
</ul>

3) Create routes for for the email_search path

This email button links to a new path (email_search_path) that will need routes. Your first instinct as a Rails programmer might be to look into config/routes.rb.  But the Blacklight gem uses a separate class in /lib/blacklight/routes.rb to generate most of the routes.

Instead of manually creating a new route in the config folder, we’ll modify Blacklight’s routes class. There are a few ways to do this. You could override the entire class by creating a routes.rb file under the same directory path in your rails app. For this exercise, we’ll limit our modifications to the method we need to override and put the code in the initializer folder under lib/blacklight.routes.rb. Although we’re only overriding one method, I would recommend taking a look at the full source in the gem to get a better sense of what this class does.


require "#{Blacklight.root}/lib/blacklight/routes.rb"

# -*- encoding : utf-8 -*-
require 'deprecation'
module Blacklight
  class Routes
    extend Deprecation
    protected
    module RouteSets
      def saved_searches(_)
        add_routes do |options|
          delete "saved_searches/clear",       :to => "saved_searches#clear",   :as => "clear_saved_searches"
          get "saved_searches",       :to => "saved_searches#index",   :as => "saved_searches"
          put "saved_searches/save/:id",    :to => "saved_searches#save",    :as => "save_search"
          delete "saved_searches/forget/:id",  :to => "saved_searches#forget",  :as => "forget_search"
          post "saved_searches/forget/:id",  :to => "saved_searches#forget"
          get "saved_searches/email",       :to => "saved_searches#email",   :as => "email_saved_searches"
          post "saved_searches/email"
        end
      end
    end
    include RouteSets
  end
end

4) Add a form to submit the email

Now that the routes are in place, we can create the form needed to submit an email.

In app/views/saved_searches create an email.html.erb view. This is based on the email.html.erb used to email bookmarks (under app/views/catalog in the blacklight gem).


<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h1><%= t('blacklight.email.form.title') %></h1>
</div>
<%= render :partial => 'email_search_form' %>

In the same directory, create a partial to provide the form fields.

_email_search_form.html.erb


<%= form_tag url_for(:controller => "saved_searches", :action => "email"), :id => 'email_search_form', :class => "form-horizontal ajax_form", :method => :post do %>

<div class="modal-body">
<%= render :partial=>'/flash_msg' %>
<div class="control-group">
<label class="control-label" for="to">
<%= t('blacklight.email.form.send_to') %>
</label>
<div class="controls">
<%= text_field_tag :to, params[:to] %><br/>
</div>
</div>
<div class="control-group">
<label class="control-label" for="message">
<%= t('blacklight.email.form.message') %>
</label>
<div class="controls">
<%= text_area_tag :message, params[:message] %>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary"> <%= t('blacklight.sms.form.submit') %></button>
</div>
<% end %>

5) Add an email_search action to the controller

The partial form invokes a controller action (email) that doesn’t exist yet. We’ll add this next.

The Blacklight gem has a class saved_searches_controller.rb that holds the controller methods for saved_searches. It’s worth taking a look at this controller class in the gem (in lib/blacklight/catalog.rb). We’ll be basing our new controller method on the email_record action that already exists in this catalog controller (also in the gem).

In app/controllers/saved_searches_controller.rb (in your local instance), put:


require "#{Blacklight.root}/app/controllers/saved_searches_controller.rb"

# -*- encoding : utf-8 -*-
class SavedSearchesController < ApplicationController
  include Blacklight::Configurable

  # Email Action (this will render the appropriate view on GET requests and process the form and send the email on POST requests)

  def email

     @searches = current_user.searches

     if request.post? and validate_email_params

        email = SearchMailer.email_search(@searches, {:to => params[:to], :message => params[:message]}, url_options)
        email.deliver

        flash[:success] = I18n.t("blacklight.email.success")

        respond_to do |format|
           format.html { redirect_to catalog_path(params['id']) }
           format.js { render 'email_sent' }
        end and return
     end

     respond_to do |format|
        format.html
        format.js { render :layout => false }
     end
  end

  def validate_email_params
    case
    when params[:to].blank?
      flash[:error] = I18n.t('blacklight.email.errors.to.blank')
    when !params[:to].match(defined?(Devise) ? Devise.email_regexp : /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
      flash[:error] = I18n.t('blacklight.email.errors.to.invalid', :to => params[:to])
    end

    flash[:error].blank?
  end

end

Here, the email action is grabbing the saved searches from the current_user object and storing them in an array.

@searches = current_user.searches

If the call to this method is POST, this means the form has been submitted, so the method will call a mailer method (email_search, which we still need to write) and pass the @searches array as a parameter.

6) Create the mailer method

Create a new file named search_mailer.rb in the app/mailers directory. This is similar to the record_mailer.rb file in the blacklight gem, adapted for a list of searches rather than bookmarks.


require "#{Blacklight.models_dir}/record_mailer.rb"

# -*- encoding : utf-8 -*-
# Only works for documents with a #to_marc right now.
class RecordMailer < ActionMailer::Base

   def email_search(searches, details, url_gen_params)
      subject = I18n.t('blacklight.email_search_subject', :title => ("search results") )

      @searches = searches
      @message = details[:message]
      @url_gen_params = url_gen_params

      mail(:from => "youremail@yourserver.edu", :to => details[:to], :subject => subject)
   end
end

The subject text (blacklight.email.search_subject) doesn’t exist yet. You can see a full list in the gem under config/locales. We’ll add the new text required for our local app under blacklight.en.yml.


en:
  blacklight:
    application_name: 'Blacklight'
    email_search_subject: 'Your saved search history'

7) Create the mailer view

You will also need a view for this mailer to create the body of the email that will be sent. The view for document emails in the Blacklight gem is in app/views/record_mailer/email_record.html.erb.

We’ll create a similar view for the search history email.

In your local app, create a search_mailer directory in app/views, and create a new view named email_search.text.erb.  (In other words, create  app/views/record_mailer/email_search.text.erb.)


Here are your saved searches

sent with the message: <%= @message %>

<% @searches.each do |s| %>

http://localhost:3000/?<%=(s.query_params).to_query%>

<%end%>

Give it a try! You should now be able to email your saved searches through Blacklight.

8) Next steps

As you can see, the email view for search is hacky. You don’t want to hardcode localhost and you should probably exclude the action and controller name in the URL. You might also want to consider moving some of the headers and text to a configuration file. (Check out config/locales/blacklight.en.yml and blacklight.fr.yml in the gem for a starting point.)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>