Browse Source

HUE-2300 [search] Pivot facet backend and front end logic

Romain Rigaux 11 years ago
parent
commit
c873323341

+ 1 - 1
apps/search/src/search/api.py

@@ -53,7 +53,7 @@ def _guess_range_facet(widget_type, solr_api, collection, facet_field, propertie
         if widget_type == 'line-widget':
           start, _ = _round_thousand_range(stats_min)
         else:
-          start, _ =  _round_number_range(stats_min)
+          start, _ = _round_number_range(stats_min)
       else:
         start = int(start)
       if end is None:

+ 14 - 1
apps/search/src/search/models.py

@@ -487,7 +487,20 @@ def augment_solr_response(response, collection, query):
             'count': value,
           }
           normalized_facets.append(facet)
-      # pivot_facet
+      elif category == 'pivot':
+        name = ','.join([facet['field']] + [f['field'] for f in facet['properties']['facets']])
+        if 'facet_pivot' in response['facet_counts'] and name in response['facet_counts']['facet_pivot']:
+          count = response['facet_counts']['facet_pivot'][name]
+        else:
+          count = []
+        facet = {
+          'id': facet['id'],
+          'query': name,
+          'type': category,
+          'label': name,
+          'count': count,
+        }
+        normalized_facets.append(facet)
 
   # HTML escaping
   for doc in response['response']['docs']:

+ 48 - 26
apps/search/src/search/templates/search.mako

@@ -161,6 +161,15 @@ ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
                        <i class="fa fa-filter"></i>
          </a>
     </div>
+    <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableChart() },
+                    draggable: {data: draggableTree(), isEnabled: availableDraggableChart,
+                    options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
+                              'stop': function(event, ui){$('.card-body').slideDown('fast', function(){$(window).scrollTop(lastWindowScrollPosition)});}}}"
+         title="${_('Tree')}" rel="tooltip" data-placement="top">
+         <a data-bind="style: { cursor: $root.availableDraggableChart() ? 'move' : 'default' }">
+                       <i class="fa fa-sitemap fa-rotate-270"></i>
+         </a>
+   </div>
     <div data-bind="css: { 'draggable-widget': true, 'disabled': !availableDraggableChart() },
                     draggable: {data: draggableMap(), isEnabled: availableDraggableChart,
                     options: {'start': function(event, ui){lastWindowScrollPosition = $(window).scrollTop();$('.card-body').slideUp('fast');},
@@ -213,6 +222,20 @@ ${ dashboard.layout_skeleton() }
       </a>
     </div>
 
+    <!-- ko if: type() == 'pivot' -->
+      <select data-bind="options: $root.collection.template.availableWidgetFieldsNames, value: properties.facets_form.field, optionsCaption: ' '"></select>
+      <input type="text" class="input-medium" data-bind="value: properties.facets_form.limit"/>
+      <input type="text" class="input-medium" data-bind="value: properties.facets_form.mincount"/>
+
+      <a href="javascript: void(0)" data-bind="click: $root.collection.addPivotFacetValue">
+        <i class="fa fa-plus"></i>
+      </a>
+
+      ${ _('Limit') } <input type="text" class="input-medium" data-bind="value: properties.limit"/>
+      ${ _('Mincount') } <input type="text" class="input-medium" data-bind="value: properties.mincount"/>
+      ${ _('Plot') } <input type="checkbox" data-bind="checked: properties.graph"></span>
+    <!-- /ko -->
+
     <!-- ko if: type() == 'range' -->
       <!-- ko ifnot: properties.isDate() -->
         <div class="slider-cnt" data-bind="slider: {start: properties.min, end: properties.max, gap: properties.gap, min: properties.start, max: properties.end}"></div>
@@ -741,40 +764,38 @@ ${ dashboard.layout_skeleton() }
   </div>
 </script>
 
-<script type="text/html" id="filter-widget">
-  <div data-bind="visible: $root.query.fqs().length == 0" style="margin-top: 10px">${_('There are currently no filters applied.')}</div>
-  <div data-bind="foreach: { data: $root.query.fqs, afterRender: function(){ isLoading(false); } }">
-    <!-- ko if: $data.type() == 'field' -->
-    <div class="filter-box">
-      <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ chartsUpdatingState(); viewModel.query.removeFilter($data); viewModel.search() }"><i class="fa fa-times"></i></a>
-      <strong>${_('field')}</strong>:
-      <span data-bind="text: $data.field"></span>
-      <br/>
-      <strong>${_('value')}</strong>:
-      <span data-bind="text: $data.filter"></span>
-    </div>
-    <!-- /ko -->
-    <!-- ko if: $data.type() == 'range' -->
-    <div class="filter-box">
-      <a href="javascript:void(0)" class="pull-right" data-bind="click: function(){ chartsUpdatingState(); viewModel.query.removeFilter($data); viewModel.search() }"><i class="fa fa-times"></i></a>
-      <strong>${_('field')}</strong>:
-      <span data-bind="text: $data.field"></span>
-      <br/>
-      <span data-bind="foreach: $data.properties" style="font-weight: normal">
-        <strong>${_('from')}</strong>: <span data-bind="text: $data.from"></span>
-        <br/>
-        <strong>${_('to')}</strong>: <span data-bind="text: $data.to"></span>
+
+<script type="text/html" id="tree-widget">
+  <!-- ko if: $root.getFacetFromQuery(id()) -->
+  <div class="row-fluid" data-bind="with: $root.getFacetFromQuery(id())">
+    <div data-bind="visible: $root.isEditing, with: $root.collection.getFacetById($parent.id())" style="margin-bottom: 20px">
+      <span data-bind="template: { name: 'facet-toggle', afterRender: function(){ $root.getWidgetById($parent.id).isLoading(false); } }">
       </span>
     </div>
-    <!-- /ko -->
+
+    <div data-bind="with: $root.collection.getFacetById($parent.id())">
+      ${ _('Cross with') }
+      <div data-bind="foreach: $data.properties.facets">
+        <span data-bind="text: field"></span>
+        ${ _('Limit') } <input type="text" class="input-medium" data-bind="value: limit"/>
+        ${ _('Mincount') } <input type="text" class="input-medium" data-bind="value: mincount"/>
+        <a href="javascript: void(0)" data-bind="click: function() { $root.collection.removePivotFacetValue({'pivot_facet': $parent, 'value': $data}); }">
+          <i class="fa fa-minus"></i>
+        </a>
+      </div>
+    </div>
+
+    <span data-bind="text: ko.mapping.toJSON(count)"></span>
+
   </div>
-  <div class="clearfix"></div>
-  <div class="widget-spinner" data-bind="visible: isLoading() &&  $root.query.fqs().length > 0">
+  <!-- /ko -->
+  <div class="widget-spinner" data-bind="visible: isLoading()">
     <!--[if !IE]> --><i class="fa fa-spinner fa-spin"></i><!-- <![endif]-->
     <!--[if IE]><img src="/static/art/spinner.gif" /><![endif]-->
   </div>
 </script>
 
+
 <script type="text/html" id="map-widget">
   <!-- ko if: $root.getFacetFromQuery(id()) -->
   <div class="row-fluid" data-bind="with: $root.getFacetFromQuery(id())">
@@ -803,6 +824,7 @@ ${ dashboard.layout_skeleton() }
   </div>
 </script>
 
+
 <script type="text/html" id="analysis-window">
   <!-- ko if: $root.fieldAnalysesName() -->
   <div data-bind="with: $root.getFieldAnalysis()">

+ 18 - 10
apps/search/src/search/views.py

@@ -432,7 +432,7 @@ def new_facet(request):
     widget_type = request.POST['widget_type']
 
     result['message'] = ''
-    result['facet'] =  _create_facet(collection, request.user, facet_id, facet_label, facet_field, widget_type)
+    result['facet'] = _create_facet(collection, request.user, facet_id, facet_label, facet_field, widget_type)
     result['status'] = 0
   except Exception, e:
     result['message'] = unicode(str(e), "utf8")
@@ -451,21 +451,29 @@ def _create_facet(collection, user, facet_id, facet_label, facet_field, widget_t
     'andUp': False,  # Not used yet
   }
 
-  solr_api = SolrApi(SOLR_URL.get(), user)
-  range_properties = _new_range_facet(solr_api, collection, facet_field, widget_type)
-
-  if range_properties:
-    facet_type = 'range'
-    properties.update(range_properties)
-  elif widget_type == 'hit-widget':
-    facet_type = 'query'
+  if widget_type == 'tree-widget':
+    facet_type = 'pivot'
   else:
-    facet_type = 'field'
+    solr_api = SolrApi(SOLR_URL.get(), user)
+    range_properties = _new_range_facet(solr_api, collection, facet_field, widget_type)
+
+    if range_properties:
+      facet_type = 'range'
+      properties.update(range_properties)
+    elif widget_type == 'hit-widget':
+      facet_type = 'query'
+    else:
+      facet_type = 'field'
 
   if widget_type == 'map-widget':
     properties['scope'] = 'world'
     properties['mincount'] = 1
     properties['limit'] = 100
+  elif widget_type == 'tree-widget':
+    properties['mincount'] = 1
+    properties['facets'] = []
+    properties['facets_form'] = {'field': '', 'mincount': 1, 'limit': 10}
+    properties['graph'] = False
 
   return {
     'id': facet_id,

+ 38 - 4
apps/search/static/js/search.ko.js

@@ -235,7 +235,7 @@ var FieldAnalysis = function (vm, field_name) {
       collection: ko.mapping.toJSON(vm.collection),
       analysis: ko.mapping.toJSON(self)
     }, function (data) {
-      if (data.status == 0) {  
+      if (data.status == 0) {
         $.each(data.terms, function(key, val) {
           self.terms.data.push({'key': key, 'val': val});
         });
@@ -259,7 +259,7 @@ var FieldAnalysis = function (vm, field_name) {
       query: ko.mapping.toJSON(vm.query),
       analysis: ko.mapping.toJSON(self)
     }, function (data) {
-      if (data.status == 0) {  
+      if (data.status == 0) {
         $.each(data.stats.stats.stats_fields[self.name()], function(key, val) {
           self.stats.data.push({'key': key, 'val': val});
         });
@@ -374,6 +374,39 @@ var Collection = function (vm, collection) {
     }).fail(function (xhr, textStatus, errorThrown) {});
   };
 
+  self.addPivotFacetValue = function(facet) {
+    var pivot;
+  
+    if (typeof facet.properties.facets_form.field == 'string') { // Hackish but we load back properties as simple objects
+      pivot = ko.mapping.fromJS({
+          'field': facet.properties.facets_form.field,
+          'limit': facet.properties.facets_form.limit,
+          'mincount': facet.properties.facets_form.mincount,
+      });  
+      facet.properties.facets_form.field = null;
+      facet.properties.facets_form.limit = 10;
+      facet.properties.facets_form.mincount = 1;  
+    } else {
+      pivot = ko.mapping.fromJS({
+          'field': facet.properties.facets_form.field(),
+          'limit': facet.properties.facets_form.limit(),
+          'mincount': facet.properties.facets_form.mincount(),
+      });
+      facet.properties.facets_form.field(null);
+      facet.properties.facets_form.limit(10);
+      facet.properties.facets_form.mincount(1);    
+    }
+
+    facet.properties.facets.push(pivot);
+    vm.search();
+  }
+
+  self.removePivotFacetValue = function(facet) {
+    facet['pivot_facet'].properties.facets.remove(facet['value']);
+
+    vm.search();
+  }
+
   self.removeFacet = function (widget_id) {
     $.each(self.facets(), function (index, facet) {
       if (facet.id() == widget_id()) {
@@ -529,7 +562,7 @@ var Collection = function (vm, collection) {
     self.template.fieldsSelected.removeAll(_toDelete);
     var bulk = $.grep(currentObservable(), function(field) {
       return (_toDelete.indexOf(field.name()) != -1)
-    });  
+    });
     currentObservable.removeAll(bulk);
 
     // New fields
@@ -786,7 +819,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
   self.columns = ko.observable([]);
   loadLayout(self, collection_json.layout);
 
-  self.isEditing = ko.observable(false);
+  self.isEditing = ko.observable(true);
   self.toggleEditing = function () {
     self.isEditing(! self.isEditing());
   };
@@ -811,6 +844,7 @@ var SearchViewModel = function (collection_json, query_json, initial_json) {
   self.draggableLine = ko.observable(bareWidgetBuilder("Line Chart", "line-widget"));
   self.draggablePie = ko.observable(bareWidgetBuilder("Pie Chart", "pie-widget"));
   self.draggableFilter = ko.observable(bareWidgetBuilder("Filter Bar", "filter-widget"));
+  self.draggableTree = ko.observable(bareWidgetBuilder("Tree", "tree-widget"));
 
   self.availableDateFields = ko.computed(function() {
     return $.grep(self.collection.availableFacetFields(), function(field) { return DATE_TYPES.indexOf(field.type()) != -1; });

+ 12 - 1
desktop/libs/libsolr/src/libsolr/api.py

@@ -91,6 +91,7 @@ class SolrApi(object):
         ('facet.mincount', 0),
         ('facet.limit', 10),
       )
+
       for facet in collection['facets']:
         if facet['type'] == 'query':
           params += (('facet.query', '%s' % facet['field']),)
@@ -108,7 +109,17 @@ class SolrApi(object):
               ('f.%s.facet.limit' % facet['field'], int(facet['properties'].get('limit', 10)) + 1),
               ('f.%s.facet.mincount' % facet['field'], int(facet['properties']['mincount'])),
           )
-
+        elif facet['type'] == 'pivot':  
+          if facet['properties']['facets']:
+            fields = facet['field']
+            for f in facet['properties']['facets']:
+              params += (('f.%s.facet.limit' % f['field'], f['limit']),)
+              fields += ',' + f['field']
+            params += (
+                ('facet.pivot', '{!ex=%s}%s' % (fields, fields)),
+                ('f.%s.facet.limit' % facet['field'], int(facet['properties'].get('limit', 10)) + 1),
+                ('facet.pivot.mincount', int(facet['properties']['mincount'])),
+            )
     for fq in query['fqs']:
       if fq['type'] == 'field':
         # This does not work if spaces in Solr: