Ralfp 12 лет назад
Родитель
Сommit
063cac2669

+ 1 - 0
deployment/settings.py

@@ -57,6 +57,7 @@ HAYSTACK_CONNECTIONS = {
     'default': {
     'default': {
         'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', # Misago uses Whoosh by default
         'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', # Misago uses Whoosh by default
         'PATH': 'searchindex',
         'PATH': 'searchindex',
+        'INCLUDE_SPELLING': True,
     },
     },
 }
 }
 
 

+ 2 - 2
misago/apps/privatethreads/search.py

@@ -11,9 +11,9 @@ class SearchPrivateThreadsMixin(object):
                 and self.request.settings['enable_private_threads']):
                 and self.request.settings['enable_private_threads']):
             raise ACLError404()
             raise ACLError404()
 
 
-    def queryset(self):
+    def filter_queryset(self, sqs):
         threads = [t.pk for t in self.request.user.private_thread_set.all()]
         threads = [t.pk for t in self.request.user.private_thread_set.all()]
-        return Post.objects.filter(thread_id__in=threads)
+        return sqs.filter(thread_id__in=threads)
 
 
 
 
 class SearchView(SearchPrivateThreadsMixin, SearchBaseView):
 class SearchView(SearchPrivateThreadsMixin, SearchBaseView):

+ 2 - 2
misago/apps/reports/search.py

@@ -10,8 +10,8 @@ class SearchReportsMixin(object):
         if not self.request.acl.reports.can_handle():
         if not self.request.acl.reports.can_handle():
             raise ACLError404()
             raise ACLError404()
 
 
-    def queryset(self):
-        return Post.objects.filter(forum=Forum.objects.special_pk('reports'))
+    def filter_queryset(self, sqs):
+        return sqs.filter(forum=Forum.objects.special_pk('reports'))
 
 
 
 
 class SearchView(SearchReportsMixin, SearchBaseView):
 class SearchView(SearchReportsMixin, SearchBaseView):

+ 34 - 17
misago/apps/search/views.py

@@ -4,7 +4,7 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
-from haystack.query import RelatedSearchQuerySet
+from haystack.query import SearchQuerySet, RelatedSearchQuerySet
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.acl.exceptions import ACLError403, ACLError404
 from misago.decorators import block_crawlers
 from misago.decorators import block_crawlers
 from misago.forms import FormFields
 from misago.forms import FormFields
@@ -31,15 +31,17 @@ class ViewBase(object):
         return QuickSearchForm
         return QuickSearchForm
 
 
     def render_to_response(self, template, form, context):
     def render_to_response(self, template, form, context):
-        context.update({
-                        'form': FormFields(form),
-                        'search_route': self.search_route,
-                        'results_route': self.results_route,
-                        'search_advanced': self.advanced_route,
-                        'disable_search': True,
-                        })
+        tpl_dict = {
+                    'form': FormFields(form),
+                    'search_route': self.search_route,
+                    'results_route': self.results_route,
+                    'search_advanced': self.advanced_route,
+                    'suggestion': None,
+                    'disable_search': True,
+                    }
+        tpl_dict.update(context)
         return self.request.theme.render_to_response('search/%s.html' % template,
         return self.request.theme.render_to_response('search/%s.html' % template,
-                                                     context,
+                                                     tpl_dict,
                                                      context_instance=RequestContext(self.request))
                                                      context_instance=RequestContext(self.request))
 
 
     def __new__(cls, request, **kwargs):
     def __new__(cls, request, **kwargs):
@@ -86,21 +88,35 @@ class SearchBaseView(ViewBase):
                     self.request.POST = self.request.POST.copy()
                     self.request.POST = self.request.POST.copy()
                     self.request.POST['username'] = form.target
                     self.request.POST['username'] = form.target
                     return users_list(self.request)
                     return users_list(self.request)
-                sqs = RelatedSearchQuerySet().auto_query(form.cleaned_data['search_query']).order_by('-id').load_all()
-                sqs = sqs.load_all_queryset(Post, self.queryset().filter(deleted=False).filter(moderated=False).select_related('thread', 'forum', 'user'))[:60]
 
 
+                sqs = self.filter_queryset(SearchQuerySet().auto_query(form.cleaned_data['search_query'])).load_all()[:60]
+                suggestion = SearchQuerySet().spelling_suggestion(form.cleaned_data['search_query'])
+                
                 if self.request.user.is_authenticated():
                 if self.request.user.is_authenticated():
                     self.request.user.last_search = timezone.now()
                     self.request.user.last_search = timezone.now()
                     self.request.user.save(force_update=True)
                     self.request.user.save(force_update=True)
                 if self.request.user.is_anonymous():
                 if self.request.user.is_anonymous():
                     self.request.session['last_search'] = timezone.now()
                     self.request.session['last_search'] = timezone.now()
-
+                
                 if not sqs:
                 if not sqs:
-                    raise SearchException(_("Search returned no results. Change search query and try again."))
+                    raise SearchException(_("Search returned no results. Change search query and try again."), suggestion)
+
+                if (suggestion.lower() == form.cleaned_data['search_query'].lower()
+                        or suggestion.lower() in form.cleaned_data['search_query'].lower()):
+                    suggestion = None
+
+                if suggestion:
+                    new_sqs = self.filter_queryset(SearchQuerySet().auto_query(form.cleaned_data['search_query'])).load_all()[:60]
+                    sqs_len = len(sqs)
+                    new_len = len(new_sqs)
+                    if not new_len or new_len < sqs_len * 0.8:
+                        suggestion = None # We are assuming suggestion is wrong
+
                 self.request.session[self.results_route] = {
                 self.request.session[self.results_route] = {
-                                                               'search_query': form.cleaned_data['search_query'],
-                                                               'search_results': [p.object for p in sqs],
-                                                               }
+                                                            'search_query': form.cleaned_data['search_query'],
+                                                            'search_suggestion': suggestion,
+                                                            'search_results': [p.object for p in sqs],
+                                                            }
                 return redirect(reverse(self.results_route))
                 return redirect(reverse(self.results_route))
             else:
             else:
                 if 'search_query' in form.errors:
                 if 'search_query' in form.errors:
@@ -108,7 +124,7 @@ class SearchBaseView(ViewBase):
                 raise SearchException(form.errors['__all__'][0])
                 raise SearchException(form.errors['__all__'][0])
         except SearchException as e:
         except SearchException as e:
             return self.render_to_response('error', form,  
             return self.render_to_response('error', form,  
-                                           {'message': unicode(e)})
+                                           {'message': unicode(e), 'suggestion': unicode(e.suggestion)})
 
 
 
 
 class ResultsBaseView(ViewBase):
 class ResultsBaseView(ViewBase):
@@ -130,6 +146,7 @@ class ResultsBaseView(ViewBase):
         return self.render_to_response('results', form,  
         return self.render_to_response('results', form,  
                                        {
                                        {
                                         'search_query': result['search_query'],
                                         'search_query': result['search_query'],
+                                        'suggestion': result['search_suggestion'],
                                         'results': items[pagination['start']:pagination['stop']],
                                         'results': items[pagination['start']:pagination['stop']],
                                         'items_total': items_total,
                                         'items_total': items_total,
                                         'pagination': pagination,
                                         'pagination': pagination,

+ 2 - 2
misago/apps/threads/search.py

@@ -2,8 +2,8 @@ from misago.models import Forum, Post
 from misago.apps.search.views import SearchBaseView, ResultsBaseView
 from misago.apps.search.views import SearchBaseView, ResultsBaseView
 
 
 class SearchThreadsMixin(object):
 class SearchThreadsMixin(object):
-    def queryset(self):
-        return Post.objects.filter(forum__in=Forum.objects.readable_forums(self.request.acl))
+    def filter_queryset(self, sqs):
+        return sqs.filter(forum__in=Forum.objects.readable_forums(self.request.acl))
 
 
 
 
 class SearchView(SearchThreadsMixin, SearchBaseView):
 class SearchView(SearchThreadsMixin, SearchBaseView):

+ 6 - 1
misago/search.py

@@ -1,7 +1,12 @@
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 class SearchException(Exception):
 class SearchException(Exception):
-    pass
+    def __init__(self, message=None, suggestion=None):
+        self.message = message
+        self.suggestion = suggestion
+
+    def __unicode__(self):
+         return self.message
 
 
 
 
 class SearchQuery(object):
 class SearchQuery(object):

+ 4 - 1
misago/search_indexes.py

@@ -24,5 +24,8 @@ class PostIndex(indexes.SearchIndex, indexes.Indexable):
             return False
             return False
         return True
         return True
 
 
+    def read_queryset(self, using=None):
+        return Post.objects.all().select_related('forum', 'thread', 'user')
+
     def index_queryset(self, using=None):
     def index_queryset(self, using=None):
-        return self.get_model().objects.all()
+        return self.get_model().objects.all().select_related('thread')

+ 3 - 0
static/cranefly/css/cranefly.css

@@ -1219,6 +1219,9 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .reports-list .thread-name .report-id{color:#999999 !important;}
 .reports-list .thread-name .report-id{color:#999999 !important;}
 .reports-list .thread-name,.reports-list .thread-details{margin-left:0px !important;}
 .reports-list .thread-name,.reports-list .thread-details{margin-left:0px !important;}
 .reports-list .thread-icon{display:none !important;float:none;}
 .reports-list .thread-icon{display:none !important;float:none;}
+.search-suggestion{overflow:auto;}.search-suggestion p,.search-suggestion form{float:left;}
+.search-suggestion .lead{color:#7b7b7b;}
+.search-suggestion form .btn-link{margin-top:1px;color:#3c85a3;font-size:21px;font-style:italic;font-weight:200;text-decoration:underline;}.search-suggestion form .btn-link:hover,.search-suggestion form .btn-link:active{color:#3c85a3;text-decoration:underline !important;}
 .search-resume .muted{color:#7b7b7b;}.search-resume .muted a{color:#333333;}
 .search-resume .muted{color:#7b7b7b;}.search-resume .muted a{color:#333333;}
 .search-results .results-list .result{border-bottom:1px solid #eeeeee;margin-bottom:10px;padding-bottom:10px;}.search-results .results-list .result h3{margin:0px;line-height:20px;}.search-results .results-list .result h3 a:link,.search-results .results-list .result h3 a:visited{color:#555555;font-weight:normal;font-size:18.2px;text-decoration:underline;}
 .search-results .results-list .result{border-bottom:1px solid #eeeeee;margin-bottom:10px;padding-bottom:10px;}.search-results .results-list .result h3{margin:0px;line-height:20px;}.search-results .results-list .result h3 a:link,.search-results .results-list .result h3 a:visited{color:#555555;font-weight:normal;font-size:18.2px;text-decoration:underline;}
 .search-results .results-list .result h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}
 .search-results .results-list .result h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}

+ 29 - 0
static/cranefly/css/cranefly/search.less

@@ -1,6 +1,35 @@
 // Search forum
 // Search forum
 // ------------------------
 // ------------------------
 
 
+.search-suggestion {
+  overflow: auto;
+
+  p, form {
+    float: left;
+  }
+
+  .lead {
+    color: lighten(@gray, 15%);
+  }
+
+  form {
+    .btn-link {
+      margin-top: 1px;
+
+      color: @bluePale;
+      font-size: @baseFontSize * 1.5;
+      font-style: italic;
+      font-weight: 200;
+      text-decoration: underline;
+
+      &:hover, &:active {
+        color: @bluePale;
+        text-decoration: underline !important;
+      }
+    }
+  }
+}
+
 .search-resume {
 .search-resume {
   .muted {
   .muted {
     color: lighten(@gray, 15%);
     color: lighten(@gray, 15%);

+ 10 - 0
templates/cranefly/search/layout.html

@@ -16,6 +16,16 @@
 </div>
 </div>
 
 
 <div class="container container-primary">
 <div class="container container-primary">
+  {% if suggestion %}
+  <div class="search-suggestion">
+    <p class="lead muted">{% trans %}Did you mean:{% endtrans %}</p>
+    <form action="{{ search_route|url() }}" class="inline-form" method="post">
+      <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+      <input type="hidden" name="search_query" value="{{ suggestion }}">
+      <button type="submit" class="btn btn-link">{{ suggestion }}</button>
+    </form>
+  </div>
+  {% endif %}
   {% block action %}{% endblock %}
   {% block action %}{% endblock %}
 </div>
 </div>
 {% endblock %}
 {% endblock %}