Browse Source

[search] Save the template of a collection

Romain Rigaux 11 years ago
parent
commit
dd2159d

+ 43 - 50
apps/search/src/search/models.py

@@ -206,29 +206,9 @@ class CollectionManager(models.Manager):
     try:
       return self.get(name=name), False
     except Collection.DoesNotExist:
-      facets = Facet.objects.create(data=json.dumps({
-                   'properties': {'isEnabled': False, 'limit': 10, 'mincount': 1, 'sort': 'count'},
-                   'ranges': [],
-                   'fields': [],
-                   'dates': []
-                }))
-      result = Result.objects.create(data=json.dumps({
-                  'template': '',
-                  'highlighting': [],
-                  'properties': {'highlighting_enabled': False},
-                  'extracode':
-                  """
-<style>
-em {
-  color: red;
-}
-</style>
-
-<script>
-</script>
-                  """
-              }))
-      sorting = Sorting.objects.create(data=json.dumps({'properties': {'is_enabled': False}, 'fields': []}))
+      facets = Facet.objects.create()
+      result = Result.objects.create()      
+      sorting = Sorting.objects.create()
       cores = json.dumps(solr_properties)
 
       collection = Collection.objects.create(
@@ -242,18 +222,10 @@ em {
           sorting=sorting
       )
 
-      template = """
-<div class="row-fluid">
-  <div class="row-fluid">
-    <div class="span12">%s</div>
-  </div>
-  <br/>
-</div>""" % ' '.join(['{{%s}}' % field['name'] for field in collection.fields_data(user)])
+      collection.update_properties({'collection': collection.get_default(user)})
+      collection.save()
 
-      result.update_from_post({'template': json.dumps(template)})
-      result.save()
-
-      return collection, True
+      return collection, True      
 
 
 class Collection(models.Model):
@@ -271,9 +243,20 @@ class Collection(models.Model):
   result = models.ForeignKey(Result)
   sorting = models.ForeignKey(Sorting)
 
+  _ATTRIBUTES = ['collection', 'autocomplete']
+  ICON = '/search/static/art/icon_search_24.png'
+
   objects = CollectionManager()
 
   def get_c(self, user):
+    props = self.properties_dict
+    print props
+    if 'collection' not in props:
+      props['collection'] = self.get_default(user)
+    
+    return json.dumps(props['collection'])
+
+  def get_default(self, user):      
     fields = self.fields_data(user)
     id_field = [field.name for field in fields if field.get('isId')]
     if not id_field:
@@ -281,32 +264,32 @@ class Collection(models.Model):
     fields = [field.get('name') for field in self.fields_data(user)]
   
     TEMPLATE = {
-      "extracode": "<style type=\"text/css\">\nem {\n  font-weight: bold;\n  background-color: yellow;\n}</style>", "highlighting": ["body"],
+      "extracode": "<style type=\"text/css\">\nem {\n  font-weight: bold;\n  background-color: yellow;\n}</style>", "highlighting": [""],
       "properties": {"highlighting_enabled": True},
-      "template": "{{user_screen_name}} {{user_name}} {{text}}", "isGridLayout": True,
+      "template": """
+      <div class="row-fluid">
+        <div class="row-fluid">
+          <div class="span12">%s</div>
+        </div>
+        <br/>
+      </div>""" % ' '.join(['{{%s}}' % field for field in fields]), 
+      "isGridLayout": True,
       "fields": fields
-    };
-    FACETS = {"dates": [], "fields": [
-#                                      {
-#         "uuid": "f6618a5c-bbba-2886-1886-bbcaf01409ca", "verbatim": "", "isVerbatim": False, "label": "Location", 
-#         "field": "user_location", "type": "field"
-#       }
-       ],
+    }
+    
+    FACETS = {"dates": [], "fields": [],
        "charts": [], "properties": {"sort": "count", "mincount": 1, "isEnabled": True, "limit": 10, 'schemd_id_field': 'id'}, "ranges": [], "order": []
     };  
     
-    m = {
+    collection_properties = {
       'id': self.id, 'name': self.name, 'template': TEMPLATE, 'facets': FACETS['fields'], 
       'fields': fields, 'idField': id_field
-    };
+    };      
     
-    return json.dumps(m)
-
-  def get_query(self, client_query=None):
-    return self.facets.get_query_params() + self.result.get_query_params() + self.sorting.get_query_params(client_query)
+    return collection_properties
 
   def get_absolute_url(self):
-    return reverse('search:admin_collection', kwargs={'collection_id': self.id})
+    return reverse('search:index') + '?collection=%s' % self.id
 
   def fields(self, user):
     return sorted([str(field.get('name', '')) for field in self.fields_data(user)])
@@ -328,8 +311,18 @@ class Collection(models.Model):
     # Backward compatibility
     if 'autocomplete' not in properties_python:
       properties_python['autocomplete'] = False
+    # TODO: Should convert old format here
     return properties_python
 
+  def update_properties(self, post_data):
+    prop_dict = self.properties_dict
+
+    for attr in Collection._ATTRIBUTES:
+      if post_data.get(attr) is not None:
+        prop_dict[attr] = post_data[attr]
+
+    self.properties = json.dumps(prop_dict)
+
   @property
   def autocomplete(self):
     return self.properties_dict['autocomplete']

+ 85 - 97
apps/search/src/search/templates/search2.mako

@@ -128,10 +128,12 @@ ${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
 
 <div class="search-bar">
   % if user.is_superuser:
-    <div class="pull-right" style="margin-top: 6px; margin-right: 40px">
-      <button type="button" data-bind="click: toggleEditing, css: {'btn': true, 'btn-inverse': isEditing}"><i class="fa fa-pencil"></i></button>    
-      <a class="change-settings" href="#"><i class="fa fa-save"></i> ${ _('Save') }</a> &nbsp;&nbsp;
-      <a href="${ url('search:admin_collections') }"><i class="fa fa-sitemap"></i> ${ _('Collection manager') }</a>
+    <div class="pull-right" style="margin-top: 6px; padding-right:50px">
+      <button type="button" title="${ _('Edit') }" rel="tooltip" data-placement="bottom" data-bind="click: toggleEditing, css: {'btn': true, 'btn-inverse': isEditing}"><i class="fa fa-pencil"></i></button>
+      <button type="button" title="${ _('Settings') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}"><i class="fa fa-cogs"></i></button>    
+      <button type="button" title="${ _('Save') }" rel="tooltip" data-placement="bottom" data-loading-text="${ _("Saving...") }"  data-bind="click: save, css: {'btn': true}"><i class="fa fa-save"></i></button>
+      <button type="button" title="${ _('History') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}"><i class="fa fa-archive"></i></button>
+      <a class="btn" href="${ url('search:admin_collections') }" title="${ _('Collections') }" rel="tooltip" data-placement="bottom" data-bind="css: {'btn': true}"><i class="fa fa-tags"></i></a> 
     </div>
   % endif
   
@@ -139,10 +141,7 @@ ${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
     <strong>${_("Search")}</strong>
     <div class="input-append">
       <div class="selectMask">
-        <span class="current-collection"></span>
-        <ul class="unstyled">
-          <li><a class="dropdown-collection" href="#" data-value="${ hue_collection.id }" data-settings-url="${ hue_collection.get_absolute_url() }">${ hue_collection.label }</a></li>
-        </ul>
+        <span class="current-collection">${ collection.label }</span>        
       </div>
 
       <input data-bind="value: query.q" name="query" maxlength="256" type="text" class="search-query input-xxlarge" id="id_query" style="cursor: auto;">
@@ -240,11 +239,14 @@ ${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
   <div data-bind="css: klass">
     <h2 class="card-heading simple">
       <ul class="inline" data-bind="visible: $root.isEditing">
-        <li><a href="javascript:void(0)" data-bind="click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a></li>
+        <li><a href="javascript:void(0)" data-bind="click: function(){remove($parent, this)}"><i class="fa fa-pencil"></i></a></li>
         <li><a href="javascript:void(0)" data-bind="click: compress, visible: size() > 1"><i class="fa fa-step-backward"></i></a></li>
         <li><a href="javascript:void(0)" data-bind="click: expand, visible: size() < 12"><i class="fa fa-step-forward"></i></a></li>
       </ul>
       <span data-bind="text: name"></span>
+      <div class="inline pull-right" data-bind="visible: $root.isEditing">
+        <a href="javascript:void(0)" data-bind="click: function(){remove($parent, this)}"><i class="fa fa-times"></i></a>
+      </div>
     </h2>
     <div class="card-body">
       <p>
@@ -268,60 +270,80 @@ ${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
 </script>
 
 <script type="text/html" id="facet-widget">
-      <span class="pull-right">
-        <a data-bind="click: showAddFacetModal" href="javascript:void(0)"><i class="fa fa-plus"></i></a>
-      </span>
-
-      ## Need to pick the facet ID from norm_facets instead of looping on all
-      <div class="row" data-bind="foreach: $root.norm_facets">
-        <span class="pull-right">
-          <a href="javascript:void(0)" data-bind="click: editFacet"><i class="fa fa-pencil"></i></a>
-          <a href="javascript:void(0)" data-bind="click: $root.removeFacet"><i class="fa fa-times"></i></a>
-        </span>
-        <div data-bind="text: label"></div>
-        <div data-bind="foreach: counts">
-          <div>
-            <a href="script:void(0)">
-            <!-- ko if: selected -->
-              <span data-bind="text: value, click: $root.query.unselectFacet"></span>
-              <i data-bind="click: $root.query.unselectFacet" class="fa fa-times"></i>            
-            <!-- /ko -->
-            <!-- ko if: !selected -->           
-              <span data-bind="text: value, click: $root.query.selectFacet"></span> (<span data-bind="text: count, click: $root.query.selectFacet"></span>)            
-            <!-- /ko -->
-            </a>
-          </div>
-        </div>
-      </div>    
-
+  <span class="pull-right">
+    <a data-bind="click: showAddFacetModal" href="javascript:void(0)"><i class="fa fa-plus"></i></a>
+  </span>
+
+  ## Need to pick the facet ID from norm_facets instead of looping on all
+  <div class="row" data-bind="foreach: $root.norm_facets">
+    <span class="pull-right">
+      <a href="javascript:void(0)" data-bind="click: editFacet"><i class="fa fa-pencil"></i></a>
+      <a href="javascript:void(0)" data-bind="click: $root.removeFacet"><i class="fa fa-times"></i></a>
+    </span>
+    <div data-bind="text: label"></div>
+    <div data-bind="foreach: counts">
+      <div>
+        <a href="script:void(0)">
+        <!-- ko if: selected -->
+          <span data-bind="text: value, click: $root.query.unselectFacet"></span>
+          <i data-bind="click: $root.query.unselectFacet" class="fa fa-times"></i>            
+        <!-- /ko -->
+        <!-- ko if: !selected -->           
+          <span data-bind="text: value, click: $root.query.selectFacet"></span> (<span data-bind="text: count, click: $root.query.selectFacet"></span>)            
+        <!-- /ko -->
+        </a>
+      </div>
+    </div>
+  </div>
 </script>
 
-<script type="text/html" id="resultset-widget">
-      <div class="row">
-        <span class="pull-right">
-          <a data-bind="click: editTemplate" href="javascript:void(0)"><i class="fa fa-pencil"></i></a>
-        </span>
-      </div>
-  
-      <!-- ko if: $root.collection.template.isGridLayout() -->
-      <table id="result-container">        
-        <thead>
-          <tr data-bind="foreach: $root.collection.template.fields">
-            <th data-bind="text: $data"></th>
-          </tr>
-        </thead>
-        <tbody data-bind="foreach: $root.results">
-          <tr class="result-row" data-bind="foreach: $data">
-            <td data-bind="html: $data"></td>
-          </tr>
-        </tbody>
-      </table>
-      <!-- /ko -->
-      <!-- ko if: ! $root.collection.template.isGridLayout() -->
-      <div id="result-container" data-bind="foreach: $root.results">
-        <div class="result-row" data-bind="html: $data"></div>
+<script type="text/html" id="resultset-widget">  
+  <div class="row">
+    <div class="span2">
+    <p>
+      <div class="clearfix"></div>
+      <div style="margin-top: 20px">        
+        <p data-bind="visible: $root.isEditing">
+          ${ _('Grid result') }: <input type="checkbox" data-bind="checked: $root.collection.template.isGridLayout" />
+        </p>
+        
+        <!-- ko if: $root.collection.template.isGridLayout() -->
+        <p>
+          ## Todo add a toggle to show fields or not in non edit mode
+          ${ _('Fields') }
+          <select data-bind="options: $root.collection.fields, selectedOptions: $root.collection.template.fields" size="5" multiple="true"></select>
+        </p>  
+        <!-- /ko -->
+        
+        <!-- ko if: !$root.collection.template.isGridLayout() && $root.isEditing() -->
+        <textarea data-bind="value: $root.collection.template.template, valueUpdate:'afterkeydown'"></textarea>
+        <!-- /ko -->
       </div>
-      <!-- /ko -->  
+    </p>
+    </div>
+    
+    <div class="span10">
+	  <!-- ko if: $root.collection.template.isGridLayout() -->
+	  <table id="result-container">        
+	    <thead>
+	      <tr data-bind="foreach: $root.collection.template.fields">
+	        <th data-bind="text: $data"></th>
+	      </tr>
+	    </thead>
+	    <tbody data-bind="foreach: $root.results">
+	      <tr class="result-row" data-bind="foreach: $data">
+	        <td data-bind="html: $data"></td>
+	      </tr>
+	    </tbody>
+	  </table>
+	  <!-- /ko -->
+	  <!-- ko if: ! $root.collection.template.isGridLayout() -->
+	  <div id="result-container" data-bind="foreach: $root.results">
+	    <div class="result-row" data-bind="html: $data"></div>
+	  </div>
+	  <!-- /ko -->  
+    </span>
+  </span>
 </script>
 
 <script type="text/html" id="timeline-widget">
@@ -401,37 +423,6 @@ ${ commonheader(_('Search'), "search", user, "90px") | n,unicode }
 </div>
 
 
-<div id="editTemplateModal" class="modal hide fade">
-  <div class="modal-header">
-    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-    <h3>${_('Edit Template')}</h3>
-  </div>
-  <div class="modal-body">
-    <p>
-      <div class="clearfix"></div>
-      <div style="margin-top: 20px">        
-        <p>
-          ${ _('Grid result') }: <input type="checkbox" data-bind="checked: collection.template.isGridLayout" />
-        </p>
-        
-        <!-- ko if: $root.collection.template.isGridLayout() -->
-        <p>
-          ${ _('Fields') }
-          <select data-bind="options: collection.fields, selectedOptions: collection.template.fields" size="5" multiple="true"></select>
-        </p>  
-        <!-- /ko -->
-        
-        <!-- ko if: ! $root.collection.template.isGridLayout() -->
-        <textarea data-bind="value: collection.template.template, valueUpdate:'afterkeydown'"></textarea>
-        <!-- /ko -->
-      </div>
-    </p>
-  </div>
-  <div class="modal-footer">
-    <a href="#" data-dismiss="modal" class="btn">${_('Ok')}</a>
-  </div>
-</div>
-
 <script src="/static/ext/js/knockout-min.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/mustache.js"></script>
@@ -487,14 +478,15 @@ $(document).ready(function () {
   };
 
 
-  viewModel = new SearchViewModel(${ hue_collection.get_c(user) | n,unicode }, ${ hue_query | n,unicode });
+  viewModel = new SearchViewModel(${ collection.get_c(user) | n,unicode }, ${ query | n,unicode });
   ko.applyBindings(viewModel);
   
-  
+  % if not layout:
   fullLayout();
   viewModel.isEditing(true);
   viewModel.columns()[0].rows()[0].addWidget(viewModel.draggableResultset());  
   viewModel.search();
+  % endif
 });
 
   function showAddFacetModal(facet) {
@@ -507,10 +499,6 @@ $(document).ready(function () {
     viewModel.search();
   };
 
-  function editTemplate(facet) {
-    $("#editTemplateModal").modal("show");
-  };
-
   function editFacet(facet) {
     viewModel.selectSingleFacet(facet);
     $("#editFacetModal").modal("show");

+ 3 - 4
apps/search/src/search/urls.py

@@ -18,15 +18,14 @@
 from django.conf.urls.defaults import patterns, url
 
 urlpatterns = patterns('search.views',
-  url(r'^$', 'index2', name='index'),
-  url(r'^index2$', 'index2', name='index2'),
-  url(r'^query2$', 'index2', name='query2'),
+  url(r'^$', 'index', name='index'),
   url(r'^search$', 'search', name='search'),
-  url(r'^query$', 'index', name='query'),
+  url(r'^save$', 'save', name='save'),
   url(r'^download/(?P<format>(csv|xls))$', 'download', name='download'),
 
   url(r'^dashboard$', 'dashboard', name='dashboard'),
 
+  # All admin is deprecated
   url(r'^admin/collections$', 'admin_collections', name='admin_collections'),
   url(r'^admin/collections_import$', 'admin_collections_import', name='admin_collections_import'),
 

+ 33 - 63
apps/search/src/search/views.py

@@ -17,7 +17,6 @@
 
 import json
 import logging
-import math
 
 from django.core.urlresolvers import reverse
 from django.http import HttpResponse
@@ -34,8 +33,7 @@ from search.data_export import download as export_download
 from search.decorators import allow_admin_only
 from search.forms import QueryForm, CollectionForm
 from search.management.commands import search_setup
-from search.models import Collection, augment_solr_response, augment_solr_response2,\
-  augment_solr_exception
+from search.models import Collection, augment_solr_response2, augment_solr_exception
 from search.search_controller import SearchController
 
 from django.utils.encoding import force_unicode
@@ -49,93 +47,47 @@ def initial_collection(request, hue_collections):
   return hue_collections[0].id
 
 
-def index(request):
-  hue_collections = SearchController(request.user).get_search_collections()
-
-  if not hue_collections:
-    if request.user.is_superuser:
-      return admin_collections(request, True)
-    else:
-      return no_collections(request)
-
-  init_collection = initial_collection(request, hue_collections)
-
-  search_form = QueryForm(request.GET, initial_collection=init_collection)
-  response = {}
-  error = {}
-  solr_query = {}
-
-  if search_form.is_valid():
-    try:
-      collection_id = search_form.cleaned_data['collection']
-      hue_collection = Collection.objects.get(id=collection_id)
-
-      solr_query = search_form.solr_query_dict
-      response = SolrApi(SOLR_URL.get(), request.user).query(solr_query, hue_collection)
-
-      solr_query['total_pages'] = int(math.ceil((float(response['response']['numFound']) / float(solr_query['rows']))))
-      solr_query['search_time'] = response['responseHeader']['QTime']
-    except Exception, e:
-      error['title'] = force_unicode(e.title) if hasattr(e, 'title') else ''
-      error['message'] = force_unicode(str(e))
-  else:
-    error['message'] = _('There is no collection to search.')
-
-  if hue_collection is not None:
-    response = augment_solr_response(response, hue_collection.facets.get_data(), solr_query)
-
-  if request.GET.get('format') == 'json':
-    return HttpResponse(json.dumps(response), mimetype="application/json")
-
-  return render('search.mako', request, {
-    'search_form': search_form,
-    'response': response,
-    'error': error,
-    'solr_query': solr_query,
-    'hue_collection': hue_collection,
-    'current_collection': collection_id,
-    'json': json,
-  })
-
 def dashboard(request):
   return render('dashboard.mako', request, {})
 
 
-def index2(request):
+def index(request):
   hue_collections = SearchController(request.user).get_search_collections()
-
-  if not hue_collections:
+  collection_id = request.GET.get('collection')
+  
+  if not hue_collections or not collection_id:
     if request.user.is_superuser:
       return admin_collections(request, True)
     else:
       return no_collections(request)
 
-  collection_id = request.GET.get('collection')
-  hue_collection = Collection.objects.get(id=collection_id) # TODO perms HUE-1987
-  hue_query = {'q': '', 'fq': {}}
+  collection = Collection.objects.get(id=collection_id) # TODO perms HUE-1987
+  query = {'q': '', 'fq': {}}
 
   return render('search2.mako', request, {
-    'hue_collection': hue_collection,
-    'hue_query': hue_query,
+    'collection': collection,
+    'query': query,
+    'layout': {},
   })
 
 
 def search(request):
   response = {}  
   
-  collection = json.loads(request.POST.get('collection', '{}')) # TODO perms
+  collection = json.loads(request.POST.get('collection', '{}')) # TODO decorator with doc model perms
   query = json.loads(request.POST.get('query', '{}'))
+  hue_collection = Collection.objects.get(id=collection['id']) # TODO perms
+  # collection['name']=
   
   print request.POST
   print collection
     
   if collection:
     solr_query = {}    
-    try:
-      hue_collection = Collection.objects.get(id=collection['id']) # TODO perms
+    try:      
       solr_query = {}      
       
-      solr_query['collection'] = collection['name'] # TODO perms
+      solr_query['collection'] = collection['name']
       solr_query['rows'] = 10
       solr_query['start'] = 0
       solr_query['fq'] = query['fq']
@@ -161,6 +113,24 @@ def search(request):
   return HttpResponse(json.dumps(response), mimetype="application/json")
 
 
+def save(request):
+  response = {'status': -1}  
+  
+  collection = json.loads(request.POST.get('collection', '{}')) # TODO perms decorator
+    
+  if collection:
+    hue_collection = Collection.objects.get(id=collection['id'])
+    hue_collection.update_properties({'collection': collection})
+    # Todo update certain atttributes like, label, enabled...
+    hue_collection.save()
+    response['status'] = 0
+    response['message'] = _('Page saved !')
+  else:
+    response['message'] = _('There is no collection to search.')
+
+  return HttpResponse(json.dumps(response), mimetype="application/json")
+
+
 def download(request, format):
   hue_collections = SearchController(request.user).get_search_collections()
 

+ 18 - 2
apps/search/static/js/search.ko.js

@@ -102,6 +102,7 @@ var Column = function (size, rows) {
     }
 
     self.remove = function (row, widget) {
+      // widget.remove() --> callback to removeFacet() or other depending on type of widget
       row.widgets.remove(widget);
     }
   };
@@ -216,8 +217,7 @@ var Collection = function (vm, collection) {
 	  }).fail(function(xhr, textStatus, errorThrown) {}
 	);
   }
-    
-  // Init
+
   self.addDynamicFields();
 };
 
@@ -288,6 +288,7 @@ var SearchViewModel = function (collection_json, query_json) {
      });
   };
     
+  // To move to collection
   self.selectSingleFacet = function(normalized_facet_json) {
 	$.each(self.collection.facets(), function(index, facet) {
       if (facet.field() == normalized_facet_json.field) {
@@ -304,4 +305,19 @@ var SearchViewModel = function (collection_json, query_json) {
 	});
 	self.search();
   }
+  
+  self.save = function () {
+    $.post("/search/save", {
+        collection: ko.mapping.toJSON(self.collection),
+        layout: ko.mapping.toJSON(self.columns)
+      }, function (data) {
+   	   if (data.status == 0) {
+   		 $(document).trigger("info", data.message);
+   	   } else {
+   		$(document).trigger("error", data.message);
+   	   }
+     }).fail(function(xhr, textStatus, errorThrown) {    	
+       $(document).trigger("error", xhr.responseText);
+     });
+  };
 };