# = The Section Architecture # # Umlaut has what could be considered a 'domain specific language' for # describing the display individual sections of content on the resolve menu # page. These sections often correspond to a ServiceTypeValue, like "fulltext". # But sometimes may include multiple ServiceTypeValues (eg related_items section # includes cited_by and similar_items), or no ServiceTypeValue at all (eg # section to display a COinS). # # A description of a section is simply a hash with certain conventional # keys describing various aspects of the contents and display of that section. # These hashes are listed in the resolve_sections application configuration # variable, initialized in the resolve_views.rb initializer, and customized # or over-ridden in the local resolve_views.rb initializer. # # One benefit of describing a section through configuration is that section # display can often by changed at configure time without requiring a code # time. Another is that the description of the section can be used not # only to generate the initial HTML page; but also by the javascript that # update the sections with new background content as available; and by the # partial_html_sections api that delivers HTML fragments for sections in an # XML or JSON container. # # A description of a section is simply a hash, suitable for passing to # SectionRenderer.new, detailed below. Plus some additional variables # specifying _where_ to display the section, documented in the resolve_views.rb # initializer. # # = The SectionRenderer # A SectionRenderer object provides logic for displaying a specific section # on the Umlaut resolve menu page. It is initialized with a hash describing # the details -- or significantly, with simply a pointer to such a hash # already existing in the resolve_sections config variable. # # A SectionRenderer is typically created by the ResolveHelper#render_section # method, which then passes the SectionRender object to the # _section_display.erb.html that does the actual rendering, using # the SectionRenderer for logic and hashes to pass to render calls in # the partial. # # # == Section Options # # Section options are typically configured in hashes in the application # config variable resolve_sections, which is expected to be a list of hashes. # That hash is suitable to be passed to a SectionRenderer.new() as configuration # options for the section. The various ways these options can be used # is documented below. # # === Simplest Case, Defaults # # As is common in ruby, SectionRenderer will make a lot of conventional # assumptions, allowing you to be very concise for the basic simple case: # # { :div_id => "fulltext", :html_area => :main } # # This means that: # * this section is assumed to be contained within a
. The # div won't be automatically rendered, it's the containing pages # responsibility to put in a div with this id. # # * this section is assumed to contain responses of type # ServiceTypeValue["fulltext"] # # * The section will be displayed with stock heading block including a title # constructed from the display_name of ServiceTypeValue["fulltext"], or # in general the display_name of the first ServiceTypeValue included # in this section. # # * The section will include a stock 'spinner' if there are potential background # results being gathered for the ServiceTypeValue(s) contained. # # * The actual ServiceResponses collected for the ServiceTypeValue included # will be rendered with a _standard_response_item # partial, using render :collection. # # * The section will be displayed whether or not there are any actual # responses included. If there are no responses, a message will be displayed # to that effect. # # The display of a section can be customized via configuration parameters to # a large degree, including supplying your own partial to take over almost # all display of the section. # # === Customizing ServiceTypeValues # # You can specifically supply the ServiceTypeValues contained in this # section, to a different type than would be guessed from the div_id: # # {:div_id => "my_area", :service_type_values => ["fulltext"]} # # Or specify multiple types included in one section: # # {:div_id => "related_items", :service_type_values => ['cited_by', 'similar]} # # Or a section that isn't used for displaying service responses at all, # and has no service type: # # {:div_id => "coins", :partial => "coins", :service_type_values => []} # # Note that a custom partial needs to be supplied if there are no service_type_values supplied. # # === Customizing heading display # # You can supply a title for the section that's different than what would # be guessed from it's ServiceTypeValues. You can also supply a prompt. # # {:div_id =>"excerpts", :section_title=>"Lots of good stuff", :section_prompt => "Limited previes and excerpts."} # # You can also suppress display of the stock section heading at all: # {:show_heading => false, ...} # # This may be becuase you don't want a heading, or because you are supplying # a custom partial that will take care of the heading in a custom way. # # === Customizing spinner display # # You can also suppress display of the stock spinner, because you don't # want a spinner, or because your custom partial will be taking care of it. # {:show_spinner => false, ...} # # By default, the spinner displays what type of thing it's waiting on, guessing # from the ServiceTypeValue configured. If you want to specify this item name: # {:item_name_plural => "Related Items", ...} # # === Customizing visibility of section # # By default, a section will simply be displayed regardless of whether # there are any actual responses to display. However, the 'visibility' # argument can be used to customize this in many ways. # visibilty: # [*true*] # Default, always show section. # [*false*] # Never show section. (Not sure why you'd want this). # [:any_services] # Show section if and only if there are any configured # services that generate the ServiceTypeValues included # in this section, regardless of whether in this case # they have or not. # [:in_progress] # Show the section if responses exist, OR if any services # are currently in progress that are capable of generating # responses of the right type for this section. # [:responses_exist] # Show the section if and only if some responses # have actually been generated of the types contained # in this section. # [:complete_with_responses] # Show the section only if there are responses # generated, AND all services supplying # responses of the type contained in section # have completed, no more responses are possible. # [(lambda object)] # Most flexibly of all, you can supply your own lambda # supplying custom logic to determine whether to show # the section, based on current context. The lambda # will be passed the SectionRenderer object as an argument, # providing access to the Umlaut Request with context. # eg: # :visibility => lambda do |renderer| # renderer.request.something == something # end # # === List with limit # # You can have the section automatically use the ResolveHelper#list_with_limit # helper to limit the number of items initially displayed, with the rest behind # a 'more' expand/contract widget. # # { :div_id => "highlighted_link", # :list_visible_limit => 1, # :visibility => :in_progress, ... } # # === Custom partial display # # By default, the SectionRenderer assumes that all the ServiceResposnes included # are capable of being displayed by the standard_item_response, and displays # them simply by render standard_item_response with a \:colection. Sometimes # this assumption isn't true, or you want custom display for other reasons. # You can supply your own partial that the renderer will use to display # the content. # # { :div_id => "my_div", :partial => "my_partial", ... } # # The partial so supplied should live in resolve/_my_partial.html.erb # # When this partial is called, it will have local variables set # to give it the data it needs in order to create a display: # # [*responses_by_type*] # a hash keyed by ServiceTypeValue name, with the # the value being an array of the respective ServiceType # objects. # [*responses*] a flattened list of all ServiceTypes included in # this section, of varying ServiceTypeValues. Most # useful when the section only includes one # ServiceTypeValue # [*renderer*] The SectionRenderer object itself, from which # the current umlaut request can be obtained, # among other things. # # You can supply additional static local arguments to the partial # in the SectionRenderer setup: # # {:div_id=> "foo", :partial=>"my_partial", :partial_locals => {:mode => "big"}, ... } # # the :partial_locals argument can be used with the standard_response_item # too: # {:div_id => "highlighted_link", :partial_locals => {:show_source => true}} # # Note that your custom partial will still be displayed with stock # header and possibly spinner surrounding it. You can suppress these elements: # # {:div_id => "cover_image", :partial => "cover_image", :show_heading => false, :show_spinner => false} # # But even so, some 'wrapping' html is rendered surrounding your partial. # If you want to disable even this, becuase your partial will take care of it # itself, you can do so with \:show_partial_only => true # {:div_id => "search_inside", :partial => "search_inside", :show_partial_only => true} class SectionRenderer include ActionView::Helpers::TagHelper @@bg_update_sections = @@partial_update_sections = nil # First argument is the current umlaut Request object. # Second argument is a session description hash. See class overview # for an overview. Recognized keys of session description hash: # * [id] SessionRenderer will look up session description hash in # resolve_views finding one with :div_id == id # * [div_id] The id of the
the section lives in. Also used # generally as unique ID for the section. # * [service_type_values] ServiceTypeValue's that this section contains. # defaults to [ServiceTypeValue[div_id]] # * [section_title] Title for the section. Defaults to # service_type_values.first.display_name # * [section_prompt] Prompt. Default nil. # * [show_heading] Show the heading section at all. Default true. # * [show_spinner] Show a stock spinner for bg action for service_type_values. # default true. # * [item_name_plural] Pluralized name of the objects included, used in # spinner message. Default # service_type_values.first.display_name_pluralize # * [visibilty] What logic to use to decide whether to show the section at # all. true|false|:any_services|:in_progress|:responses_exist|:complete_with_responses|(lambda object) # * [list_visible_limit] Use list_with_limit to limit initially displayed # items to value. Default nil, meaning don't use # list_with_limit. # * [partial] Use a custom partial to display this section, instead of # using render("standard_response_item", :collection => [all responses]) as default. # * [show_partial_only] Display custom partial without any of the usual # standardized wrapping HTML. Custom partial will # take care of it itself. def initialize(a_umlaut_request, section_def = {}) @umlaut_request = a_umlaut_request @section_id = section_def[:id] || section_def[:div_id] raise Exception.new("SectionRenderer needs an :id passed in arguments hash") unless @section_id # Merge in default arguments for this section from config. construct_options(section_def) end # Returns all ServiceTypeValue objects contained in this section, as # configured. Lazy caches result for perfomance. def service_type_values @service_type_values ||= @options[:service_type_values].collect do |s| s.kind_of?(ServiceTypeValue)? s : ServiceTypeValue[s] end end # Whether any services that generate #service_type_values are # currently in progress. Lazy caches result for efficiency. def services_in_progress? # cache for efficiency @services_in_progress ||= @umlaut_request.service_types_in_progress?(service_type_values) end # Hash of ServiceType objects (join obj # representing individual reponse data) included in this # section. Keyed by string ServiceTypeValue id, value is array # of ServiceTypes def responses unless (@responses) @responses = {} service_type_values.each do |st| @responses[st.name] = @umlaut_request.get_service_type(st) end end @responses end # All the values from #responses, flattened into a simple Array. def responses_list responses.values.flatten end def responses_empty? responses_list.empty? end def request return @umlaut_request end def div_id return @section_id end def show_heading? (! show_partial_only?) && @options[:show_heading] end def render_heading content_tag(:div, :class=>"section_heading") output = '' output <<= '
' (output <<= '

' << CGI::escapeHTML(section_title) << '

') if section_title (output <<= '

' << CGI::escapeHTML(section_prompt) << '

') if section_prompt output <<= '
' output.html_safe end def show_spinner? (! show_partial_only?) && @options[:show_spinner] && @umlaut_request.service_types_in_progress?(service_type_values) end # A hash suitable to be passed to Rails render(), to render # a spinner for this section. Called by section_display partial, # nobody else should need to call it. def spinner_render_hash { :partial => "background_progress", :locals =>{ :svc_types => service_type_values, :div_id => "progress_#{@section_id}", :current_set_empty => responses_empty?, :item_name => @options[:item_name_plural]} } end def show_partial_only? @options[:show_partial_only] end def custom_partial? ! @options[:partial].nil? end # A hash suitable to be passed to Rails render() to render the # inner content portion of the section. Called by the section_display # partial, nobody else should need to call this. You may be looking # for ResolveHelper#render_section instead. def content_render_hash if custom_partial? {:partial => @options[:partial].to_s, :object => responses_list, :locals => @options[:partial_locals].merge( {:responses_by_type => responses, :responses => responses_list, :umlaut_request => request, :renderer => self})} else {:partial => @options[:item_partial].to_s, :collection => responses_list, :locals => @options[:partial_locals].clone} end end # used only with with list_with_limit functionality in section_display # partial. def item_render_hash(item) # need to clone @options[:partial_locals], because # Rails will modify it to add the 'object' to it. Bah! {:partial => @options[:item_partial], :object => item, :locals => @options[:partial_locals].clone} end # Is the section visible according to it's settings calculated in current # context? def visible? case @options[:visibility] when true, false @options[:visibility] when :any_services any_services? when :in_progress # Do we have any of our types generated, or any services in progress # that might generate them? (! responses_empty?) || services_in_progress? when :responses_exist # Have any responses of our type actually been generated? ! responses_empty? when :complete_with_responses (! responses.empty?) && ! (services_in_progress?) when Proc # It's a lambda, which takes @umlaut_request as an arg @options[:visibility].call(self) else true end end # do any services exist which even potentially generate our types, even # if they've completed without doing so?. def any_services? nil != @umlaut_request.dispatched_services.to_a.find do |ds| ! (service_type_values & ds.service.service_types_generated ).empty? end end def list_visible_limit @options[:list_visible_limit] end def section_title @options[:section_title] end def section_prompt @options[:section_prompt] end protected def construct_options(arguments) # Fill in static defaults @options = {:show_spinner => true, :show_heading => true, :visibility => true, :show_partial_only => false, :partial_locals => {}}.merge!(arguments) # service type value default to same name as section_id @options[:service_type_values] ||= [@section_id] # Fill in calculatable-defaults if (service_type_values.length > 0) @options = {:section_title => service_type_values.first.display_name }.merge(@options) end # Partials to display. Default to _standard_response_item item partial. if ( @options[:partial] == true) @options[:partial] = @section_id end if (@options[:partial].blank?) @options[:item_partial] = case @options[:item_partial] when true then @section_id + "_item" when String then options[:item_partial] else "standard_response_item" end end # sanity check if ( @options[:show_partial_only] && ! @options[:partial]) raise Exception.new("SectionRenderer: You must supply a :partial argument if :show_partial_only is set true") end return @options end end