blacklight-hierarchy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Blacklight::Hierarchy
2
+
3
+ This plugin provides hierarchical facets for [Blacklight](https://github.com/projectblacklight/blacklight).
4
+
5
+ ## Usage
6
+
7
+ Add the plugin to your Blacklight app's Gemfile
8
+
9
+ gem 'blacklight-hierarchy', :git => 'git@github.com:sul-dlss/blacklight-hierarchy.git'
10
+
11
+ Index your hierarchies in colon-separated list. For example, items in a "processing" queue with a "copy" action, might be indexed as
12
+
13
+ <doc>
14
+ <field name="id">foo</field>
15
+ <field name="queue_status_facet">processing</field>
16
+ <field name="queue_status_facet">processing:copy</field>
17
+ <field name="queue_status_facet">processing:copy:waiting</field>
18
+ </doc>
19
+ <doc>
20
+ <field name="id">bar</field>
21
+ <field name="queue_status_facet">processing</field>
22
+ <field name="queue_status_facet">processing:copy</field>
23
+ <field name="queue_status_facet">processing:copy:completed</field>
24
+ </doc>
25
+
26
+ That would cause the facet count to appear at all three levels
27
+
28
+ - [processing](#) (2)
29
+ - [copy](#) (2)
30
+ - [completed](#) (1)
31
+ - [waiting](#) (1)
32
+
33
+ You can skip as many levels as you'd like, as long as the "leaf" values are indexed. For example, if you didn't index the "processing" part alone, it will simply be a container, not a clickable/countable facet:
34
+
35
+ - processing
36
+ - [copy](#) (2)
37
+ - [completed](#) (1)
38
+ - [waiting](#) (1)
39
+
40
+ (**Note**: If you use Solr's built-in [PathHierarchyTokenizerFactory](http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.PathHierarchyTokenizerFactory), you can index the entire depth by supplying only the leaf nodes.)
41
+
42
+ In your Blacklight controller configuration (usually `CatalogController`), tell Blacklight to render the facet using the hierarchy partial
43
+
44
+ config.add_facet_field 'queue_status_facet', :label => 'Queue Status',
45
+ :partial => 'blacklight/hierarchy/facet_hierarchy'
46
+
47
+ Add the hierarchy-specific options to the controller configuration
48
+
49
+ config.facet_display = {
50
+ :hierarchy => {
51
+ 'tag' => [nil]
52
+ }
53
+ }
54
+
55
+ (The `[nil]` value is present in support of rotatable facet hierarchies, which I don't have time to document right now.)
56
+
57
+ ## Caveats
58
+
59
+ This code was ripped out of another project, and is still quite immature as a standalone project. Every effort has been made to make it as plug-and-play as possible, but it may stomp on Blacklight in unintended ways (e.g., ways that made sense in context of its former host app, but which aren't compatible with generic Blacklight). Proceed with caution, and report issues.
60
+
61
+ ## TODO
62
+
63
+ - WRITE TESTS
64
+ - Switch internal facet management from hack-y Hash to `Blacklight::Hierarchy::FacetGroup` class (already implemented, but not plumbed up)
65
+ - Add configuration support for hierarchy delimiters other than `/\s*:\s*/` (baked into `Blacklight::Hierarchy::FacetGroup`, but again, requiring additional plumbing)
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ begin
11
+ require 'rdoc/task'
12
+ rescue LoadError
13
+ require 'rdoc/rdoc'
14
+ require 'rake/rdoctask'
15
+ RDoc::Task = Rake::RDocTask
16
+ end
17
+
18
+ RDoc::Task.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = 'Blacklight::Hierarchy'
21
+ rdoc.options << '--line-numbers'
22
+ rdoc.rdoc_files.include('README.rdoc')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+
27
+
28
+
29
+ Bundler::GemHelper.install_tasks
30
+
31
+ require 'rake/testtask'
32
+
33
+ Rake::TestTask.new(:test) do |t|
34
+ t.libs << 'lib'
35
+ t.libs << 'test'
36
+ t.pattern = 'test/**/*_test.rb'
37
+ t.verbose = false
38
+ end
39
+
40
+
41
+ task :default => :test
@@ -0,0 +1,34 @@
1
+ $(document).ready(function() {
2
+ Blacklight.do_hierarchical_facet_expand_contract_behavior();
3
+ });
4
+
5
+ (function($) {
6
+ Blacklight.do_hierarchical_facet_expand_contract_behavior = function() {
7
+ $( Blacklight.do_hierarchical_facet_expand_contract_behavior.selector ).each (
8
+ Blacklight.hierarchical_facet_expand_contract
9
+ );
10
+ }
11
+ Blacklight.do_hierarchical_facet_expand_contract_behavior.selector = 'li.h-node';
12
+
13
+ Blacklight.hierarchical_facet_expand_contract = function() {
14
+ var li = $(this);
15
+
16
+ $('ul', this).each(function() {
17
+ li.addClass('twiddle');
18
+ if($('span.selected', this).length == 0){
19
+ $(this).hide();
20
+ } else {
21
+ li.addClass('twiddle-open');
22
+ }
23
+ });
24
+
25
+ // attach the toggle behavior to the li tag
26
+ li.click(function(e){
27
+ if (e.target == this) {
28
+ // toggle the content
29
+ $(this).toggleClass('twiddle-open');
30
+ $(this).children('ul').slideToggle();
31
+ }
32
+ });
33
+ };
34
+ })(jQuery);
@@ -0,0 +1,13 @@
1
+ #facets {
2
+ ul.facet-hierarchy {
3
+ margin-left: 8px;
4
+ ul {
5
+ margin-left: 0;
6
+ border-bottom: none;
7
+ padding-bottom: 0;
8
+ li {
9
+ margin-left: 8px; } }
10
+ li.h-node {
11
+ list-style-image: asset_data_url("blacklight/hierarchy/collapsed.png"); }
12
+ li.h-node.twiddle-open {
13
+ list-style-image: asset_data_url("blacklight/hierarchy/expanded.png"); } } }
@@ -0,0 +1,135 @@
1
+ module Blacklight::HierarchyHelper
2
+
3
+ def is_hierarchical?(field_name)
4
+ (prefix,order,suffix) = field_name.split(/_/)
5
+ list = blacklight_config.facet_display[:hierarchy][prefix] and list.include?(order)
6
+ end
7
+
8
+ def facet_order(prefix)
9
+ param_name = "#{prefix}_facet_order".to_sym
10
+ params[param_name] || blacklight_config.facet_display[:hierarchy][prefix].first
11
+ end
12
+
13
+ def facet_after(prefix, order)
14
+ orders = blacklight_config.facet_display[:hierarchy][prefix]
15
+ orders[orders.index(order)+1] || orders.first
16
+ end
17
+
18
+ def hide_facet?(field_name)
19
+ if is_hierarchical?(field_name)
20
+ prefix = field_name.split(/_/).first
21
+ field_name != "#{prefix}_#{facet_order(prefix)}_facet"
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ def rotate_facet_value(val, from, to)
28
+ components = Hash[from.split(//).zip(val.split(/:/))]
29
+ new_values = components.values_at(*(to.split(//)))
30
+ while new_values.last.nil?
31
+ new_values.pop
32
+ end
33
+ if new_values.include?(nil)
34
+ nil
35
+ else
36
+ new_values.compact.join(':')
37
+ end
38
+ end
39
+
40
+ def rotate_facet_params(prefix, from, to, p=params.dup)
41
+ return p if from == to
42
+ from_field = "#{prefix}_#{from}_facet"
43
+ to_field = "#{prefix}_#{to}_facet"
44
+ p[:f] = (p[:f] || {}).dup # the command above is not deep in rails3, !@#$!@#$
45
+ p[:f][from_field] = (p[:f][from_field] || []).dup
46
+ p[:f][to_field] = (p[:f][to_field] || []).dup
47
+ p[:f][from_field].reject! { |v| p[:f][to_field] << rotate_facet_value(v, from, to); true }
48
+ p[:f].delete(from_field)
49
+ p[:f][to_field].compact!
50
+ p[:f].delete(to_field) if p[:f][to_field].empty?
51
+ p
52
+ end
53
+
54
+ def render_facet_rotate(field_name)
55
+ if is_hierarchical?(field_name)
56
+ (prefix,order,suffix) = field_name.split(/_/)
57
+ new_order = facet_after(prefix,order)
58
+ new_params = rotate_facet_params(prefix,order,new_order)
59
+ new_params["#{prefix}_facet_order"] = new_order
60
+ link_to image_tag('icons/rotate.png', :title => new_order.upcase).html_safe, new_params, :class => 'no-underline'
61
+ end
62
+ end
63
+
64
+ # Putting bare HTML strings in a helper sucks. But in this case, with a
65
+ # lot of recursive tree-walking going on, it's an order of magnitude faster
66
+ # than either render(:partial) or content_tag
67
+ def render_facet_hierarchy_item(field_name, data, key)
68
+ item = data[:_]
69
+ subset = data.reject { |k,v| ! k.is_a?(String) }
70
+
71
+ li_class = subset.empty? ? 'h-leaf' : 'h-node'
72
+ li = ul = ''
73
+
74
+ if item.nil?
75
+ li = key
76
+ elsif facet_in_params?(field_name, item.qvalue)
77
+ li = render_selected_qfacet_value(field_name, item)
78
+ else
79
+ li = render_qfacet_value(field_name, item)
80
+ end
81
+
82
+ unless subset.empty?
83
+ subul = subset.keys.sort.collect do |subkey|
84
+ render_facet_hierarchy_item(field_name, subset[subkey], subkey)
85
+ end.join('')
86
+ ul = "<ul>#{subul}</ul>".html_safe
87
+ end
88
+
89
+ %{<li class="#{li_class}">#{li.html_safe}#{ul.html_safe}</li>}.html_safe
90
+ end
91
+
92
+ def render_hierarchy(field)
93
+ prefix = field.field.split(/_/).first
94
+ tree = facet_tree(prefix)[field.field]
95
+ tree.keys.sort.collect do |key|
96
+ render_facet_hierarchy_item(field.field, tree[key], key)
97
+ end.join("\n").html_safe
98
+ end
99
+
100
+ def render_qfacet_value(facet_solr_field, item, options ={})
101
+ (link_to_unless(options[:suppress_link], item.value, add_facet_params(facet_solr_field, item.qvalue), :class=>"facet_select label") + " " + render_facet_count(item.hits)).html_safe
102
+ end
103
+
104
+ # Standard display of a SELECTED facet value, no link, special span
105
+ # with class, and 'remove' button.
106
+ def render_selected_qfacet_value(facet_solr_field, item)
107
+ content_tag(:span, render_qfacet_value(facet_solr_field, item, :suppress_link => true), :class => "selected label") +
108
+ link_to("[remove]", remove_facet_params(facet_solr_field, item.qvalue, params), :class=>"remove")
109
+ end
110
+
111
+ HierarchicalFacetItem = Struct.new :qvalue, :value, :hits
112
+ def facet_tree(prefix)
113
+ @facet_tree ||= {}
114
+ if @facet_tree[prefix].nil?
115
+ @facet_tree[prefix] = {}
116
+ blacklight_config.facet_display[:hierarchy][prefix].each { |key|
117
+ facet_field = [prefix,key,'facet'].compact.join('_')
118
+ @facet_tree[prefix][facet_field] ||= {}
119
+ data = @response.facet_by_field_name(facet_field)
120
+ next if data.nil?
121
+
122
+ data.items.each { |facet_item|
123
+ path = facet_item.value.split(/\s*:\s*/)
124
+ loc = @facet_tree[prefix][facet_field]
125
+ while path.length > 0
126
+ loc = loc[path.shift] ||= {}
127
+ end
128
+ loc[:_] = HierarchicalFacetItem.new(facet_item.value, facet_item.value.split(/\s*:\s*/).last, facet_item.hits)
129
+ }
130
+ }
131
+ end
132
+ @facet_tree[prefix]
133
+ end
134
+
135
+ end
@@ -0,0 +1,3 @@
1
+ <ul class="facet-hierarchy">
2
+ <%= render_hierarchy(facet_field) %>
3
+ </ul>
@@ -0,0 +1,25 @@
1
+ <%
2
+ item = data[:_]
3
+ subset = data.reject { |k,v| ! k.is_a?(String) }
4
+ %>
5
+
6
+ <li class="<%= subset.empty? ? 'h-leaf' : 'h-node' %>">
7
+ <% if item.nil? %>
8
+ <%= key %>
9
+ <% else %>
10
+ <% if facet_in_params?(field_name, item.qvalue) %>
11
+ <%= render_selected_qfacet_value( field_name, item )%>
12
+ <% else %>
13
+ <%= render_qfacet_value(field_name, item) %>
14
+ <% end %>
15
+ <% end %>
16
+ <% unless subset.empty? %>
17
+ <ul>
18
+ <%=
19
+ raw(subset.keys.sort.collect { |subkey|
20
+ render :partial => 'facet_hierarchy_item', :locals => { :field_name => field_name, :data => subset[subkey], :key => subkey }
21
+ })
22
+ %>
23
+ </ul>
24
+ <% end %>
25
+ </li>
@@ -0,0 +1 @@
1
+ require 'blacklight/hierarchy'
@@ -0,0 +1,6 @@
1
+ require 'blacklight/hierarchy/engine'
2
+
3
+ module Blacklight
4
+ module Hierarchy
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Blacklight
2
+ module Hierarchy
3
+ class Engine < Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,75 @@
1
+ module Blacklight
2
+ module Hierarchy
3
+ class FacetItem
4
+ attr_reader :qname, :hits
5
+
6
+ def initialize(qname, hits, facet)
7
+ @qname = qname
8
+ @hits = hits
9
+ @facet = facet
10
+ end
11
+
12
+ def [](value)
13
+ @facet.facets([qname,value].select(&:present?).join(@facet.delimiter))
14
+ end
15
+
16
+ def each_pair
17
+ keys.each { |k| yield k, self[k] }
18
+ end
19
+
20
+ def keys
21
+ @facet.keys(qname)
22
+ end
23
+
24
+ def path
25
+ @qname.split(@facet.delimiter)[0..-2]
26
+ end
27
+
28
+ def name
29
+ @qname.split(@facet.delimiter).last
30
+ end
31
+
32
+ def inspect
33
+ "#<#{self.class.name}:#{name}=>#{hits.inspect} (#{keys.join ', '})>"
34
+ end
35
+ end
36
+
37
+ class FacetGroup
38
+ attr_reader :facet_data, :qname, :hits, :delimiter
39
+ include Enumerable
40
+
41
+ def initialize(facet_data, delimiter=":")
42
+ @facet_data = Hash[*facet_data]
43
+ @delimiter = delimiter
44
+ end
45
+
46
+ def each &block
47
+ facets.each &block
48
+ end
49
+
50
+ def each_pair
51
+ facets.each { |f| yield f.name, f }
52
+ end
53
+
54
+ def keys(prefix=nil)
55
+ if prefix.nil?
56
+ facet_data.collect { |k,v| k.split(delimiter).first }.uniq
57
+ else
58
+ path = prefix.to_s.split(delimiter)
59
+ facet_data.collect do |k,v|
60
+ facet_path = k.split(delimiter)
61
+ facet_path[0..path.length-1] == path ? facet_path[path.length] : nil
62
+ end.compact.uniq
63
+ end
64
+ end
65
+
66
+ def facets(prefix=nil)
67
+ FacetItem.new(prefix.to_s,facet_data[prefix],self)
68
+ end
69
+
70
+ def [](value)
71
+ facets(value)
72
+ end
73
+ end
74
+ end
75
+ end