浏览代码

[search] Dynamic dropdown elements based on the already selected ones

Romain Rigaux 12 年之前
父节点
当前提交
23455244c1

+ 25 - 41
apps/search/src/search/models.py

@@ -15,15 +15,14 @@
 # See the License for the specific language governing permissions and
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # limitations under the License.
 
 
-from datetime import datetime
-from lxml import etree
-import re
-
 try:
 try:
   import json
   import json
 except ImportError:
 except ImportError:
   import simplejson as json
   import simplejson as json
-
+  
+from datetime import datetime
+from lxml import etree
+import re
 import logging
 import logging
 
 
 from django.db import models
 from django.db import models
@@ -37,30 +36,19 @@ from search.conf import SOLR_URL
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
 
 
-class RangeFacet(object): pass
-class DateFacet(object): pass
-
 
 
 class Facet(models.Model):
 class Facet(models.Model):
-  _ATTRIBUTES = ['properties', 'fields', 'range', 'date']
+  _ATTRIBUTES = ['properties', 'fields', 'ranges', 'dates']
 
 
   enabled = models.BooleanField(default=True)
   enabled = models.BooleanField(default=True)
   data = models.TextField()
   data = models.TextField()
 
 
   def update_from_post(self, post_data):
   def update_from_post(self, post_data):
     data_dict = json.loads(self.data)
     data_dict = json.loads(self.data)
-    print data_dict
-    if post_data.get('properties'):
-      data_dict['properties'] = json.loads(post_data['properties'])
-
-    if post_data.get('fields'):
-      data_dict['fields'] = json.loads(post_data['fields'])
-
-    if post_data.get('ranges'):
-      data_dict['ranges'] = json.loads(post_data['ranges'])
-
-    if post_data.get('dates'):
-      data_dict['dates'] = json.loads(post_data['dates'])
+    
+    for attr in Sorting._META_TEMPLATE_ATTRS:
+      if post_data.get(attr):
+        data_dict[attr] = json.loads(post_data[attr])
 
 
     self.data = json.dumps(data_dict)
     self.data = json.dumps(data_dict)
 
 
@@ -105,21 +93,16 @@ class Facet(models.Model):
 
 
 
 
 class Result(models.Model):
 class Result(models.Model):
-  _META_TEMPLATE_ATTRS = ['properties', 'template', 'highlighting', 'css']
+  _META_TEMPLATE_ATTRS = ['properties', 'template', 'highlighting']
 
 
   data = models.TextField()
   data = models.TextField()
 
 
   def update_from_post(self, post_data):
   def update_from_post(self, post_data):
     data_dict = json.loads(self.data)
     data_dict = json.loads(self.data)
-    print data_dict
-    if post_data.get('properties'):
-      data_dict['properties'] = json.loads(post_data['properties'])
-
-    if post_data.get('template'):
-      data_dict['template'] = json.loads(post_data['template'])
 
 
-    if post_data.get('highlighting'):
-      data_dict['highlighting'] = json.loads(post_data['highlighting'])
+    for attr in Result._META_TEMPLATE_ATTRS:
+      if post_data.get(attr):
+        data_dict[attr] = json.loads(post_data[attr])
 
 
     self.data = json.dumps(data_dict)
     self.data = json.dumps(data_dict)
 
 
@@ -156,11 +139,9 @@ class Sorting(models.Model):
   def update_from_post(self, post_data):
   def update_from_post(self, post_data):
     data_dict = json.loads(self.data)
     data_dict = json.loads(self.data)
 
 
-    if post_data.get('properties'):
-      data_dict['properties'] = json.loads(post_data['properties'])
-
-    if post_data.get('fields'):
-      data_dict['fields'] = json.loads(post_data['fields'])
+    for attr in Sorting._META_TEMPLATE_ATTRS:
+      if post_data.get(attr):
+        data_dict[attr] = json.loads(post_data[attr])
 
 
     self.data = json.dumps(data_dict)
     self.data = json.dumps(data_dict)
 
 
@@ -202,7 +183,7 @@ class CoreManager(models.Manager):
   <div class="span9">
   <div class="span9">
     <h5>{{user_name}}</h5>
     <h5>{{user_name}}</h5>
     {{text}}
     {{text}}
-    <br>    
+    <br>
     <a href="/filebrowser/view/{{file_path}}">
     <a href="/filebrowser/view/{{file_path}}">
       {{file_name}} {{content_type}} {{file_length}}{{#file_length}} bytes{{/file_length}}
       {{file_name}} {{content_type}} {{file_length}}{{#file_length}} bytes{{/file_length}}
     </a>
     </a>
@@ -215,7 +196,7 @@ class CoreManager(models.Manager):
   <div class="span2"><br><a class="btn" href="https://twitter.com/{{user_screen_name}}/status/{{id}}" target="_blank" title="Open in Twitter">
   <div class="span2"><br><a class="btn" href="https://twitter.com/{{user_screen_name}}/status/{{id}}" target="_blank" title="Open in Twitter">
     <i class="icon-twitter"></i></a>
     <i class="icon-twitter"></i></a>
   </div>
   </div>
-</div>                  
+</div>
                   """,
                   """,
                   'highlighting': [],
                   'highlighting': [],
                   'properties': {'highlighting_enabled': False},
                   'properties': {'highlighting_enabled': False},
@@ -227,7 +208,7 @@ class CoreManager(models.Manager):
 
 
 class Core(models.Model):
 class Core(models.Model):
   enabled = models.BooleanField(default=True)
   enabled = models.BooleanField(default=True)
-  name = models.CharField(max_length=40, unique=True, help_text=_t('Name of the Solr collection'))
+  name = models.CharField(max_length=40, unique=True, verbose_name=_t('Solr collection'))
   label = models.CharField(max_length=100)
   label = models.CharField(max_length=100)
   # solr_address?
   # solr_address?
   # results by pages number, autocomplete off...
   # results by pages number, autocomplete off...
@@ -246,10 +227,13 @@ class Core(models.Model):
 
 
   @property
   @property
   def fields(self):
   def fields(self):
+    return ['score'] + sorted([field.get('name') for field in self.fields_data])
+
+  @property
+  def fields_data(self):
     solr_schema = SolrApi(SOLR_URL.get()).schema(self.name)
     solr_schema = SolrApi(SOLR_URL.get()).schema(self.name)
     schema = etree.fromstring(solr_schema)
     schema = etree.fromstring(solr_schema)
 
 
-    return ['score'] + sorted([field.get('name') for fields in schema.iter('fields') for field in fields.iter('field')])
-
+    return sorted([{'name': field.get('name'),'type': field.get('type')}
+                   for fields in schema.iter('fields') for field in fields.iter('field')])
 
 
-class Query(object): pass

+ 58 - 14
apps/search/src/search/templates/admin_core_facets.mako

@@ -34,7 +34,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
   <%def name="content()">
   <%def name="content()">
     <form method="POST" class="form-horizontal" data-bind="submit: submit">
     <form method="POST" class="form-horizontal" data-bind="submit: submit">
 
 
-      <div class="section">       
+      <div class="section">
         <div class="alert alert-info"><h3>${_('Facets')}</h3></div>
         <div class="alert alert-info"><h3>${_('Facets')}</h3></div>
       </div>
       </div>
 
 
@@ -47,24 +47,32 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         </ul>
         </ul>
 
 
         <div id="step1" class="stepDetails">
         <div id="step1" class="stepDetails">
-          <input type='checkbox' data-bind="checked: isEnabled" style="margin-top: -2px; margin-right: 4px"/> ${_('Enabled') }
+          <p>
+            ${_('Facets provide an intuitive way to filter the results.')}
+            ${_('Different types of facets can be added on the following steps.')}
+          </p>
+          <p>
+            <input type='checkbox' data-bind="checked: isEnabled" style="margin-top: -2px; margin-right: 4px"/> ${_('Enabled') }
+          </p>
         </div>
         </div>
 
 
-        <div id="step2" class="stepDetails hide">      
+        <div id="step2" class="stepDetails hide">
           <div class="alert alert-info" style="margin-top: 60px"><h4>${_('Field Facets')}</h4></div>
           <div class="alert alert-info" style="margin-top: 60px"><h4>${_('Field Facets')}</h4></div>
           <div data-bind="visible: fieldFacets().length == 0" style="padding-left: 10px;margin-bottom: 20px">
           <div data-bind="visible: fieldFacets().length == 0" style="padding-left: 10px;margin-bottom: 20px">
             <em>${_('There are currently no field Facets defined.')}</em>
             <em>${_('There are currently no field Facets defined.')}</em>
           </div>
           </div>
           <div data-bind="foreach: fieldFacets">
           <div data-bind="foreach: fieldFacets">
             <div class="bubble">
             <div class="bubble">
-              <strong><span data-bind="text: field"></span></strong>
+              <strong>
+                <span data-bind="text: field"></span>
+              </strong>
               <a class="btn btn-small" data-bind="click: $root.removeFieldFacet"><i class="icon-trash"></i></a>
               <a class="btn btn-small" data-bind="click: $root.removeFieldFacet"><i class="icon-trash"></i></a>
             </div>
             </div>
           </div>
           </div>
           <div class="clearfix"></div>
           <div class="clearfix"></div>
           <div class="miniform">
           <div class="miniform">
             ${_('Field')}
             ${_('Field')}
-            <select id="select-field-facet" data-bind="options: fields, value: selectedFieldFacet"></select>
+            <select id="select-field-facet" data-bind="options: fieldFacetsList, value: selectedFieldFacet"></select>
             <a class="btn" data-bind="click: $root.addFieldFacet"><i class="icon-plus"></i> ${_('Add')}</a>
             <a class="btn" data-bind="click: $root.addFieldFacet"><i class="icon-plus"></i> ${_('Add')}</a>
             &nbsp;<span id="field-facet-error" class="label label-important hide">${_('The field you are trying to add is already in the list.')}</span>
             &nbsp;<span id="field-facet-error" class="label label-important hide">${_('The field you are trying to add is already in the list.')}</span>
           </div>
           </div>
@@ -88,7 +96,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         <div class="clearfix"></div>
         <div class="clearfix"></div>
         <div class="miniform">
         <div class="miniform">
           ${_('Field')}
           ${_('Field')}
-          <select data-bind="options: fields, value: selectedRangeFacet"></select>
+          <select data-bind="options: rangeFacetsList, value: selectedRangeFacet"></select>
           &nbsp;${_('Start')}
           &nbsp;${_('Start')}
           <input type="number" data-bind="value: selectedRangeStartFacet" class="input-mini" />
           <input type="number" data-bind="value: selectedRangeStartFacet" class="input-mini" />
           &nbsp;${_('End')}
           &nbsp;${_('End')}
@@ -117,7 +125,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         <div class="clearfix"></div>
         <div class="clearfix"></div>
         <div class="miniform">
         <div class="miniform">
           ${_('Field')}
           ${_('Field')}
-          <select data-bind="options: fields, value: selectedDateFacet"></select>
+          <select data-bind="options: dateFacetsList, value: selectedDateFacet"></select>
           &nbsp;${_('Start')}
           &nbsp;${_('Start')}
           <input id="dp-start" class="input-small" type="text" data-bind="value: selectedDateStartFacet" />
           <input id="dp-start" class="input-small" type="text" data-bind="value: selectedDateStartFacet" />
           &nbsp;${_('End')}
           &nbsp;${_('End')}
@@ -131,7 +139,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
 
 
       <div class="form-actions" style="margin-top: 80px">
       <div class="form-actions" style="margin-top: 80px">
         <a id="backBtn" class="btn disabled disable-feedback">${ _('Back') }</a>
         <a id="backBtn" class="btn disabled disable-feedback">${ _('Back') }</a>
-        <a id="nextBtn" class="btn btn-primary disable-feedback">${ _('Next') }</a>      
+        <a id="nextBtn" class="btn btn-primary disable-feedback">${ _('Next') }</a>
         <button type="submit" class="btn btn-primary" data-bind="visible: isSaveBtnVisible()" id="save-facets">${_('Save')}</button>
         <button type="submit" class="btn btn-primary" data-bind="visible: isSaveBtnVisible()" id="save-facets">${_('Save')}</button>
       </div>
       </div>
     </form>
     </form>
@@ -139,10 +147,12 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
 </%layout:skeleton>
 </%layout:skeleton>
 
 
 <link rel="stylesheet" href="/static/ext/css/bootstrap-datepicker.min.css" type="text/css" media="screen" title="no title" charset="utf-8" />
 <link rel="stylesheet" href="/static/ext/css/bootstrap-datepicker.min.css" type="text/css" media="screen" title="no title" charset="utf-8" />
+<link href="/static/ext/css/bootstrap-editable.css" rel="stylesheet">
 
 
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/routie-0.3.0.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/bootstrap-datepicker.min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/bootstrap-datepicker.min.js" type="text/javascript" charset="utf-8"></script>
+<script src="/static/ext/js/bootstrap-editable.min.js"></script>
 
 
 <script type="text/javascript">
 <script type="text/javascript">
   var DATE_FORMAT = "mm-dd-yyyy";
   var DATE_FORMAT = "mm-dd-yyyy";
@@ -171,25 +181,52 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
 
 
   function ViewModel() {
   function ViewModel() {
     var self = this;
     var self = this;
-    
+
     self.isSaveBtnVisible = ko.observable(false);
     self.isSaveBtnVisible = ko.observable(false);
-    
+
     self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
     self.fields = ko.observableArray(${ hue_core.fields | n,unicode });
 
 
-    self.isEnabled = ko.observable(${ hue_core.facets.data | n,unicode }.properties.is_enabled);    
+    self.fullFields = {}
+    $.each(${ hue_core.fields_data | n,unicode }, function(index, field) {
+      self.fullFields[field.name] = field;
+    });
+
+    self.isEnabled = ko.observable(${ hue_core.facets.data | n,unicode }.properties.is_enabled);
 
 
     self.fieldFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.fields, function (obj) {
     self.fieldFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.fields, function (obj) {
       return new FieldFacet(obj.field);
       return new FieldFacet(obj.field);
     }));
     }));
 
 
+    // Remove already selected fields
+    self.fieldFacetsList = ko.observableArray(${ hue_core.fields | n,unicode });
+    $.each(self.fieldFacets(), function(index, field) {
+      self.fieldFacetsList.remove(field.field);
+    });
+
     self.rangeFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.ranges, function (obj) {
     self.rangeFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.ranges, function (obj) {
       return new RangeFacet(obj.field, obj.start, obj.end, obj.gap);
       return new RangeFacet(obj.field, obj.start, obj.end, obj.gap);
     }));
     }));
 
 
+    // Only ranges
+    self.rangeFacetsList = ko.observableArray([]);
+    $.each(self.fields(), function(index, field) {
+      if (self.fullFields[field] && ['tdate', 'tint', 'long'].indexOf(self.fullFields[field].type) >= 0) {
+        self.rangeFacetsList.push(field);
+      }
+    });
+
     self.dateFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.dates, function (obj) {
     self.dateFacets = ko.observableArray(ko.utils.arrayMap(${ hue_core.facets.data | n,unicode }.dates, function (obj) {
       return new DateFacet(obj.field, obj.start, obj.end, obj.gap);
       return new DateFacet(obj.field, obj.start, obj.end, obj.gap);
     }));
     }));
 
 
+    // Only dates
+    self.dateFacetsList = ko.observableArray([]);
+    $.each(self.fields(), function(index, field) {
+      if (self.fullFields[field] && self.fullFields[field].type == 'tdate') {
+        self.dateFacetsList.push(field);
+      }
+    });
+
     self.selectedFieldFacet = ko.observable();
     self.selectedFieldFacet = ko.observable();
     self.selectedRangeFacet = ko.observable();
     self.selectedRangeFacet = ko.observable();
     self.selectedRangeStartFacet = ko.observable("");
     self.selectedRangeStartFacet = ko.observable("");
@@ -202,6 +239,8 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
 
 
     self.removeFieldFacet = function (facet) {
     self.removeFieldFacet = function (facet) {
       self.fieldFacets.remove(facet);
       self.fieldFacets.remove(facet);
+      self.fieldFacetsList.push(facet.field);
+      self.fieldFacetsList.sort();
     };
     };
 
 
     self.removeRangeFacet = function (facet) {
     self.removeRangeFacet = function (facet) {
@@ -221,6 +260,8 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
       });
       });
       if (!found){
       if (!found){
         self.fieldFacets.push(new FieldFacet(self.selectedFieldFacet()));
         self.fieldFacets.push(new FieldFacet(self.selectedFieldFacet()));
+        self.fieldFacetsList.remove(self.selectedFieldFacet());
+        self.isEnabled(true);
       }
       }
       else {
       else {
         $("#field-facet-error").show();
         $("#field-facet-error").show();
@@ -269,20 +310,23 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
   $(document).ready(function () {
   $(document).ready(function () {
     $(".btn-primary").button("reset");
     $(".btn-primary").button("reset");
     ko.applyBindings(viewModel);
     ko.applyBindings(viewModel);
+
     $("#select-field-facet").click(function(){
     $("#select-field-facet").click(function(){
       $("#field-facet-error").hide();
       $("#field-facet-error").hide();
     });
     });
+
     $("#dp-start").datepicker({
     $("#dp-start").datepicker({
       format: DATE_FORMAT
       format: DATE_FORMAT
     }).on("changeDate", function(e){
     }).on("changeDate", function(e){
       viewModel.selectedDateStartFacet($(this).val());
       viewModel.selectedDateStartFacet($(this).val());
     });
     });
+
     $("#dp-end").datepicker({
     $("#dp-end").datepicker({
       format: DATE_FORMAT
       format: DATE_FORMAT
     }).on("changeDate", function(e){
     }).on("changeDate", function(e){
       viewModel.selectedDateEndFacet($(this).val());
       viewModel.selectedDateEndFacet($(this).val());
     });
     });
-    
+
     var currentStep = "step1";
     var currentStep = "step1";
 
 
     routie({
     routie({
@@ -309,7 +353,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         }
         }
       }
       }
     });
     });
-    
+
       function showStep(step) {
       function showStep(step) {
         currentStep = step;
         currentStep = step;
         if (step != "step1") {
         if (step != "step1") {
@@ -361,7 +405,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         if (nextStep <= $(".step").length) {
         if (nextStep <= $(".step").length) {
           routie("step" + nextStep);
           routie("step" + nextStep);
         }
         }
-      });        
+      });
   });
   });
 </script>
 </script>
 
 

+ 7 - 2
apps/search/src/search/templates/admin_core_highlighting.mako

@@ -45,8 +45,12 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
           <h4>${_('Highlighting')}</h4>
           <h4>${_('Highlighting')}</h4>
         </div>
         </div>
         <div style="padding-left: 10px;margin-bottom: 20px">
         <div style="padding-left: 10px;margin-bottom: 20px">
-          <p>${_('Highlights the query keywords matching some of the fields below.')}</p>
-          <em>${_('Please select some fields to highlight in the result below.')}</em>
+          <p>
+            ${_('Highlights the query keywords matching some of the fields below.')}
+            <span data-bind="visible: ! isEnabled()">
+              ${_('Highlighting is currently disabled.')}
+            </span>
+          </p>
         </div>
         </div>
 
 
         <div class="selector">
         <div class="selector">
@@ -138,6 +142,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
       height:340,
       height:340,
       onChange: function(){
       onChange: function(){
         viewModel.highlightedFields($("#fields").val());
         viewModel.highlightedFields($("#fields").val());
+        viewModel.isEnabled(viewModel.highlightedFields() != null);
       }
       }
     });
     });
   });
   });

+ 27 - 5
apps/search/src/search/templates/admin_core_properties.mako

@@ -21,6 +21,8 @@
 
 
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="macros" file="macros.mako" />
 <%namespace name="macros" file="macros.mako" />
+<%namespace name="utils" file="utils.inc.mako" />
+
 
 
 ${ commonheader(_('Search'), "search", user) | n,unicode }
 ${ commonheader(_('Search'), "search", user) | n,unicode }
 
 
@@ -48,16 +50,36 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
   <%def name="content()">
   <%def name="content()">
   <form method="POST">
   <form method="POST">
     <ul class="nav nav-tabs">
     <ul class="nav nav-tabs">
-      <li class="active"><a href="#index" data-toggle="tab">${_('Core')}</a></li>
+      <li class="active">
+        <a href="#index" data-toggle="tab">${_('Core')}</a>
+      </li>
+      <li>
+        <a href="#schema" data-toggle="tab">${_('Schema')}</a>
+      </li>
+      <li>
+        <a href="#properties" data-toggle="tab">${_('Properties')}</a>
+      </li>
     </ul>
     </ul>
     <div class="tab-content">
     <div class="tab-content">
       <div class="tab-pane active" id="index">
       <div class="tab-pane active" id="index">
-        ${ core_form | n,unicode }
+        <div class="fieldWrapper">
+          ${ utils.render_field(core_form['enabled']) }
+          ${ utils.render_field(core_form['name']) }
+          ${ utils.render_field(core_form['label']) }
+        </div>
+
+	    <div class="form-actions">
+	      <button type="submit" class="btn btn-primary" id="save-sorting">${_('Save')}</button>
+	    </div>
       </div>
       </div>
-    </div>
 
 
-    <div class="form-actions">
-      <button type="submit" class="btn btn-primary" id="save-sorting">${_('Save')}</button>
+      <div class="tab-pane" id="schema">
+        todo?
+      </div>
+
+      <div class="tab-pane" id="properties">
+        todo?
+      </div>
     </div>
     </div>
   </form>
   </form>
   </%def>
   </%def>

+ 21 - 4
apps/search/src/search/templates/admin_core_sorting.mako

@@ -41,15 +41,19 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
             <label>
             <label>
               <input type='checkbox' data-bind="checked: isEnabled" style="margin-top: -2px; margin-right: 4px"/> ${_('Enabled') }
               <input type='checkbox' data-bind="checked: isEnabled" style="margin-top: -2px; margin-right: 4px"/> ${_('Enabled') }
             </label>
             </label>
-          </div>        
+          </div>
           <h4>${_('Sorting')}</h4>
           <h4>${_('Sorting')}</h4>
         </div>
         </div>
+        ${_('Specify on which field and order the results are sorted.')}
+        <span data-bind="visible: ! isEnabled()">
+          ${_('Sorting is currently disabled.')}
+        </span>
       </div>
       </div>
 
 
       <div class="section">
       <div class="section">
         <div class="alert alert-info" style="margin-top: 60px"><h4>${_('Sorting Fields')}</h4></div>
         <div class="alert alert-info" style="margin-top: 60px"><h4>${_('Sorting Fields')}</h4></div>
         <div data-bind="visible: sortingFields().length == 0" style="padding-left: 10px;margin-bottom: 20px">
         <div data-bind="visible: sortingFields().length == 0" style="padding-left: 10px;margin-bottom: 20px">
-          <em>${_('There are currently no Sorting Fields defined. Please add at least one from the bottom.')}</em>
+          <em>${_('There are currently no fields defined.')}</em>
         </div>
         </div>
         <div data-bind="foreach: sortingFields">
         <div data-bind="foreach: sortingFields">
           <div class="bubble">
           <div class="bubble">
@@ -63,7 +67,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
         <div class="clearfix"></div>
         <div class="clearfix"></div>
         <div class="miniform">
         <div class="miniform">
           ${_('Field')}
           ${_('Field')}
-          <select data-bind="options: fields, value: newFieldSelect"></select>
+          <select data-bind="options: sortingFieldsList, value: newFieldSelect"></select>
           &nbsp;${_('Label')}
           &nbsp;${_('Label')}
           <input type="text" data-bind="value: newFieldLabel" class="input" />
           <input type="text" data-bind="value: newFieldLabel" class="input" />
           &nbsp;${_('Sorting')}
           &nbsp;${_('Sorting')}
@@ -71,7 +75,7 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
             <button id="newFieldAsc" type="button" data-bind="css: {'active': newFieldAscDesc() == 'asc', 'btn': true}"><i class="icon-arrow-up"></i></button>
             <button id="newFieldAsc" type="button" data-bind="css: {'active': newFieldAscDesc() == 'asc', 'btn': true}"><i class="icon-arrow-up"></i></button>
             <button id="newFieldDesc" type="button" data-bind="css: {'active': newFieldAscDesc() == 'desc', 'btn': true}"><i class="icon-arrow-down"></i></button>
             <button id="newFieldDesc" type="button" data-bind="css: {'active': newFieldAscDesc() == 'desc', 'btn': true}"><i class="icon-arrow-down"></i></button>
           </div>
           </div>
-          &nbsp;&nbsp;<a class="btn" data-bind="click: $root.addSortingField"><i class="icon-plus"></i> ${_('Add field to Sorting Fields')}</a>
+          &nbsp;&nbsp;<a class="btn" data-bind="click: $root.addSortingField"><i class="icon-plus"></i> ${_('Add')}</a>
         </div>
         </div>
       </div>
       </div>
 
 
@@ -128,12 +132,23 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
       return new SortingField(obj.field, obj.label, obj.asc);
       return new SortingField(obj.field, obj.label, obj.asc);
     }));
     }));
 
 
+    // Remove already selected fields
+    self.sortingFieldsList = ko.observableArray(${ hue_core.fields | n,unicode });
+    $.each(self.sortingFields(), function(index, field) {
+      self.sortingFieldsList.remove(field.field);
+    });
+
     self.newFieldSelect = ko.observable();
     self.newFieldSelect = ko.observable();
     self.newFieldLabel = ko.observable("");
     self.newFieldLabel = ko.observable("");
     self.newFieldAscDesc = ko.observable("asc");
     self.newFieldAscDesc = ko.observable("asc");
 
 
     self.removeSortingField = function (field) {
     self.removeSortingField = function (field) {
       self.sortingFields.remove(field);
       self.sortingFields.remove(field);
+      self.sortingFieldsList.push(field.field);
+      self.sortingFieldsList.sort();
+      if (self.sortingFields().length == 0) {
+        self.isEnabled(false);
+      }
     };
     };
 
 
     self.addSortingField = function () {
     self.addSortingField = function () {
@@ -143,6 +158,8 @@ ${ commonheader(_('Search'), "search", user) | n,unicode }
       self.sortingFields.push(new SortingField(self.newFieldSelect(), self.newFieldLabel(), self.newFieldAscDesc()=="asc"));
       self.sortingFields.push(new SortingField(self.newFieldSelect(), self.newFieldLabel(), self.newFieldAscDesc()=="asc"));
       self.newFieldLabel("");
       self.newFieldLabel("");
       self.newFieldAscDesc("asc");
       self.newFieldAscDesc("asc");
+      self.sortingFieldsList.remove(self.newFieldSelect());
+      self.isEnabled(true);
     };
     };
 
 
     self.submit = function () {
     self.submit = function () {

+ 19 - 0
apps/search/src/search/templates/utils.inc.mako

@@ -22,3 +22,22 @@
       return ""
       return ""
   %>
   %>
 </%def>
 </%def>
+
+<%def name="render_field(field, show_label=True, extra_attrs={})">
+  % if not field.is_hidden:
+    <% group_class = field.errors and "error" or "" %>
+    <div class="control-group ${group_class}"
+      rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
+      % if show_label:
+        <label class="control-label">${ field.label }</label>
+      % endif
+      <div class="controls">
+        <% field.field.widget.attrs.update(extra_attrs) %>
+        ${ field | n,unicode }
+        % if field.errors:
+          <span class="help-inline">${ field.errors | n,unicode }</span>
+        % endif
+      </div>
+    </div>
+  %endif
+</%def>