Skip to content

Customizing Results Display

Jonathan Rochkind edited this page Sep 2, 2015 · 14 revisions

bento_search provides a standard display of search results using the bento_search helper, which can help you prototype very quickly. In some cases, it's standard display can be good enough for a production app, but in many cases you will want to customize your results.

The most drastic way to customize your results is simply not to use the bento_search helper for results display, you can just write whatever code you want to display the BentoSearch::Results object full of BentoSearch::Items. However, the bento_search helper includes logic for displaying errors and no-results conditions you may not want to duplicate; and the standard display methods contain a lot of useful things you may not want to duplicate. There are a variety of other ways to customize results without abandoning bento_search's display framework entirely:

CSS

The built-in bento_search templates are written to be more-or-less presentable without any CSS at all, but the right CSS can make them a lot more usable. The built-in templates take advantage of some bootstrap classes -- you certainly don't need to use bootstrap with bento_search, but you may want to manually add some CSS to duplicate some aspects of the bootstrap styles if not using it. You can look at the sample_megasearch app to see how results display with bootstrap, and decide if there is some CSS you want to copy to get closer to that.

With or without bootstrap, the built-in templates try to provide semantic CSS classes (usually prefixed bento_) in all the right places to allow flexible styling.

TODO: suggested_styles asset pipeline include.

i18n

Any string literals in built-in templates are provided via Rails i18n feature; you can use I18n to customize strings, not just for multi-lingualization but if you prefer different strings even in English.

See default strings and keys in ./config/locales/en.yml. You can over-ride these piece-by-piece in a ./config/locales/en.yml in your own app.

Item Decorators: Customizing links or output, even on an engine-by-engine basis

Lots of presentational logic is kept in a "Decorator" class, that wraps BentoSearch::ItemResult. A decorator, in this case, is an object that passes all methods down to the wrapped base class, but adds some new methods, and potentially overrides some methods from the original base object too.

The bento_search default decorator is at ./app/item_decorators/bento_search/standard_decorator.rb, and it's applied using the bento_decorate helper method, invoked in the standard view template at ./app/views/bento_search/_std_item.html.erb.

If you'd like to customize this presentational logic (or even add entirely new methods for use in your own code), you can write a custom decorator -- usually as a sub-class of the StandardDecorator -- and over-ride or re-define methods.

Another common use of a custom decorator is to add or change links displayed with an item.

Bento_search's decorators can call _h to get an object that implements current Rails helpers, and _base to access the base wrapped model directly (only occasionally needed, when you want to call a pre-decorated implementation).

You can re-use a decorator with parameterized behavior by querying a ResultItem's display_configuration object -- the original engines for_display configuration will be copied over into `display_configuration' in each ResultItem fetched.

Here's an example custom decorator. You could put the source code for the custom decorator in your local ./app/decorators, or any other app sub-class, or, for simple cases, even inline in your bento_search configuration initializer.

    class ArticleDecorator < BentoSearch::StandardDecorator

      # Redefine where a title link will go. Could return nil if you want no
      # title link. 
      #
      # we check if default link is fulltext -- if it is, we leave it alone,
      # if not, we replace with an OpenURL link to our local link resolver. 
      #
      # Our local link resolver is Umlaut, which respects the query param
      # &umlaut.skip_resolve_menu_for_type=fulltext to tell it to redirect
      # directly to fulltext if availalble. Note we also supply an OpenURL
      # rfr_id
      def link
        if self.link_is_fulltext?
          original = _base.link
          original ? "http://proxy.library.jhu.edu/login?qurl=#{CGI.escape original}" : nil
        else
          # No fulltext at ebsco
          "http://findit.some.edu/resolve?#{to_openurl_kev}&rfr_id=#{CGI.escape 'http://some.edu/search/articles'}&&umlaut.skip_resolve_menu_for_type=fulltext"
        end
      end
      
      # other_links is an array of BentoSearch::Link objects. We can
      # over-ride to add some more other_links, that are generally shown
      # as buttons underneath an item display. 
      #
      # In this case, we add an OpenURL link. We're also going to add
      # a link to the original main link, labelled "Original", just for
      # demonstration purposes. Note how we use `_base.link` to get the
      # original pre-decorated main link. 
      def other_links
        openurl_link = BentoSearch::Link.new(
          :label => "Find It @ JH",
          :url => "http://findit.library.jhu.edu/resolve?#{self.to_openurl_kev}&rfr_id=#{CGI.escape 'http://catalyst.library.jhu.edu/search/articles'}"
        )

        main_link = BentoSearch::Link.new(:label => "Original", :url => _base.link)
        
        return super + [openurl_link, main_link]
      end
        
      # Standard view uses #display_format to show the user a format
      # label. We over-ride to return nil, no format label displayed,
      # if item is believed to be of 'Article' type. Note #format returns
      # value from an internal controlled vocabulary. 
      def display_format
        # for some reason self.format rather than just `format` 
        # is required to avoid conflicting with something weird 
        # in ruby (Kernel#format somehow?)
        (self.format != "Article") ? super : nil
      end     

      # Over-ridden to make author names links to author fielded
      # searches. Not neccesarily ideal to copy-paste all this logic,
      # but better than the whole template I think.
      #
      # Another option would be moving this whole thing to a view
      # partial, and over-riding display_authors_list to _h.render it. 
      #
      # bento_single_search_path is a helper defined by our local app
      # config/routes.rb routing. 
      def render_authors_list
        parts = []
        
        first_three = self.authors.slice(0,3) 
            
        first_three.each_with_index do |author, index|
          author_display = self.author_display(author)
          
          parts << _h.content_tag("span", :class => "author") do
            _h.content_tag("a", 
              author_display, 
              :href => _h.bento_single_search_path("articles", 
                          :q => '"' + author_display + '"',
                          :search_field => "author")
              )
          end
          if (index + 1) < first_three.length
            parts << "; "
          end      
        end
        
        if self.authors.length > 3
          parts << I18n.t("bento_search.authors_et_al")
        end
        
        return _h.safe_join(parts, "")
      end
    
      # Custom method added locally. 
      # Only if there's an ISSN, returns an <a> tag with source_title label,
      # linking to Find It OpenURL resolver by issn. Otherwise, return nil.  
      def source_title_openurl_link
        return nil unless self.issn
        
        url = "http://findit.library.jhu.edu/resolve?rft.issn=#{CGI.escape self.issn}&rft.jtitle=#{CGI.escape self.source_title}&rfr_id=#{CGI.escape 'http://catalyst.library.jhu.edu/search/articles'}" 
        
        return _h.link_to(self.source_title, url)
      end
      
      # Over-ridden from standard decorator so we can make journal titles
      # links to our link resolver. Again not the greatest to copy-paste
      # all this code, but better than copy-pasting even more code!
      def render_source_info
        parts = []
        
        if self.source_title.present?
          parts << _h.content_tag("span", I18n.t("bento_search.published_in"), :class=> "source_label")
          if link = self.source_title_openurl_link
            parts << _h.content_tag("span", link, :class => "source_title")
          else
            parts << _h.content_tag("span", self.source_title, :class => "source_title")
          end
          parts << ". "
        elsif self.publisher.present?
          parts << _h.content_tag("span", self.publisher, :class => "publisher")
          parts << ". "
        end
        
        if text = self.render_citation_details
          parts << text << "."
        end
            
        return _h.safe_join(parts, "")
      end  
       
    end

And then you'd configure one or more bento_search engines to use your custom decorator, with a for_display.decorator configuration key, set to the String name of your decorator class.

   BentoSearch.register_engine("something") do |config|
      #....
      config.for_display do |display|
        #....
        display.decorator = "ArticleDecorator"
      end
   end

Custom templates

The bento_search helper method by default will use built-in partial templates:

  • bento_search/search_error when error occurs.
  • bento_search/no_results for no results message.
  • bento_search/std_item for displaying an individual item with results

Each template has the BentoSearch::Results item passed in in local results. The item template also has the BentoSearch::ResultItem in local item.

You can supply custom local templates for each of these rules in engine configuration, supplying the name of a local template:

BentoSearch.register_engine('some_engine') do |config|
   config.for_display do |display|
      display.error_partial      = 'bento/my_error'
      display.no_results_partial = 'bento/my_no_results'
      display.item_partial       = 'my_bento_item'
   end
end

You might want to start from built-in partials, copy them to a local name and customize.

(Maybe to do: Decorator helper method to determine item_partial, so it can be dynamically determined based on individual item? Would this be useful?)

Partial wrappers