Просмотр исходного кода

Search more or less implemented.

Ralfp 12 лет назад
Родитель
Сommit
3f69549065

+ 0 - 3
misago/acl/permissions/search.py

@@ -21,9 +21,6 @@ class SearchACL(BaseACL):
     def search_cooldown(self):
         return self.acl['search_cooldown']
 
-    def allow_search(self, user):
-        pass
-
 
 def build(acl, roles):
     acl.search = SearchACL()

+ 26 - 0
misago/apps/privatethreads/search.py

@@ -0,0 +1,26 @@
+from misago.decorators import block_crawlers
+from misago.models import Post
+from misago.apps.errors import error404
+from misago.apps.search.views import do_search, results
+
+def allow_search(f):
+    def decorator(*args, **kwargs):
+        if not (request.acl.private_threads.can_participate()
+                and request.settings['enable_private_threads']):
+            return error404()
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_crawlers
+@allow_search
+def search_private_threads(request):
+    threads = [t.pk for t in request.user.private_thread_set.all()]
+    queryset = Post.objects.filter(thread_id__in=threads)
+    return do_search(request, queryset, 'private_threads')
+
+
+@block_crawlers
+@allow_search
+def show_private_threads_results(request):
+    return results(request, 'private_threads')

+ 3 - 0
misago/apps/privatethreads/urls.py

@@ -37,4 +37,7 @@ urlpatterns = patterns('misago.apps.privatethreads',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="private_thread_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="private_thread_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="private_thread_changelog_revert"),
+    # Extra search routes
+    url(r'^search/$', 'search.search_private_threads', name="private_threads_search"),
+    url(r'^search/results/$', 'search.show_private_threads_results', name="private_threads_search_results"),
 )

+ 24 - 0
misago/apps/reports/search.py

@@ -0,0 +1,24 @@
+from misago.decorators import block_crawlers
+from misago.models import Forum, Post
+from misago.apps.errors import error404
+from misago.apps.search.views import do_search, results
+
+def allow_search(f):
+    def decorator(*args, **kwargs):
+        if not request.acl.reports.can_handle():
+            return error404()
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_crawlers
+@allow_search
+def search_reports(request):
+    queryset = Post.objects.filter(forum=Forum.objects.special_pk('reports'))
+    return do_search(request, queryset, 'reports')
+
+
+@block_crawlers
+@allow_search
+def show_reports_results(request):
+    return results(request, 'reports')

+ 3 - 0
misago/apps/reports/urls.py

@@ -29,4 +29,7 @@ urlpatterns = patterns('misago.apps.reports',
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/$', 'changelog.ChangelogView', name="report_changelog"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/$', 'changelog.ChangelogDiffView', name="report_changelog_diff"),
     url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/changelog/(?P<change>\d+)/revert/$', 'changelog.ChangelogRevertView', name="report_changelog_revert"),
+    # Extra search routes
+    url(r'^search/$', 'search.search_reports', name="reports_search"),
+    url(r'^search/results/$', 'search.show_reports_results', name="reports_search_results"),
 )

+ 48 - 0
misago/apps/search/forms.py

@@ -0,0 +1,48 @@
+from django import forms
+from django.utils import timezone
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
+from misago.forms import Form
+
+class QuickSearchForm(Form):
+    search_query = forms.CharField(max_length=255)
+
+    def clean_search_query(self):
+        data = self.cleaned_data['search_query']
+        if len(data) < 3:
+            raise forms.ValidationError(_("Search query should be at least 3 characters long."))
+
+        self.mode = None
+
+        if data[0:6].lower() == 'forum:':
+            forum_name = data[6:].strip()
+            if len(forum_name) < 2:
+                raise forms.ValidationError(_("In order to jump to forum, You have to enter full forum name or first few characters of it."))
+            self.mode = 'forum'
+            self.target = forum_name
+
+        if data[0:5].lower() == 'user:':
+            username = data[5:].strip()
+            if len(username) < 2:
+                raise forms.ValidationError(_("In order to jump to user profile, You have to enter full user name or first few characters of it."))
+            self.mode = 'user'
+            self.target = username
+
+        return data
+
+    def clean(self):
+        cleaned_data = super(QuickSearchForm, self).clean()
+        if self.request.user.last_search:
+            diff = timezone.now() - self.request.user.last_search
+            diff = diff.seconds + (diff.days * 86400)
+            wait_for = self.request.acl.search.search_cooldown() - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext(
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+        return cleaned_data

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

@@ -1,6 +1,6 @@
 from django.conf.urls import patterns, url
 
 urlpatterns = patterns('misago.apps.search.views',
-    url(r'^$', 'do_search', name="search"),
-    url(r'^results/$', 'show_sesults', name="search_results"),
+    url(r'^$', 'search', name="search"),
+    url(r'^results/$', 'show_results', name="search_results"),
 )

+ 100 - 10
misago/apps/search/views.py

@@ -1,19 +1,109 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
 from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
 from haystack.query import RelatedSearchQuerySet
-from misago.models import Post
+from misago.decorators import block_crawlers
+from misago.forms import FormFields
+from misago.models import Forum, Thread, Post, User
+from misago.search import SearchException
+from misago.apps.errors import error403, error404
+from misago.apps.profiles.views import list as users_list
+from misago.apps.search.forms import QuickSearchForm
 
-def do_search(request):
-    query = request.POST.get('search_query')
-    sqs = RelatedSearchQuerySet().auto_query(query).order_by('-id').load_all()
-    sqs = sqs.load_all_queryset(Post, Post.objects.all().select_related('thread', 'forum'))
+@block_crawlers
+def search(request):
+    queryset = Post.objects.filter(forum__in=Forum.objects.readable_forums(request.acl))
+    return do_search(request, queryset)
 
+
+def do_search(request, queryset, search_route=None):
+    if not request.acl.search.can_search():
+        return error403(request, _("You don't have permission to search community."))
+
+    search_route = search_route or 'search'
+
+    if request.method != "POST":
+        form = QuickSearchForm(request=request)
+        return request.theme.render_to_response('search/home.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'search_result': request.session.get('%s_result' % search_route),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+        
+    try:
+        form = QuickSearchForm(request.POST, request=request)
+        if form.is_valid():
+            if form.mode == 'forum':
+                jump_to = Forum.objects.forum_by_name(form.target, request.acl)
+                if jump_to:
+                    if jump_to.level == 1:
+                        return redirect(reverse('index') + ('#%s' % jump_to.slug))
+                    return redirect(jump_to.url)
+                else:
+                    raise SearchException(_('Forum "%(forum)s" could not be found.') % {'forum': form.target})
+            if form.mode == 'user':
+                request.POST = request.POST.copy()
+                request.POST['username'] = form.target
+                return users_list(request)
+            sqs = RelatedSearchQuerySet().auto_query(form.cleaned_data['search_query']).order_by('-id').load_all()
+            sqs = sqs.load_all_queryset(Post, queryset.select_related('thread', 'forum'))[:24]
+            request.user.last_search = timezone.now()
+            request.user.save(force_update=True)
+            if not sqs:
+                raise SearchException(_("Search returned no results. Change search query and try again."))
+            request.session['%s_result' % search_route] = {
+                                                           'search_query': form.cleaned_data['search_query'],
+                                                           'search_results': [p.object.pk for p in sqs],
+                                                           }
+            return redirect(reverse('%s_results' % search_route))
+        else:
+            raise SearchException(form.errors['search_query'][0])
+    except SearchException as e:
+        return request.theme.render_to_response('search/error.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'message': unicode(e),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+
+
+@block_crawlers
+def show_results(request):
+    return results(request)
+
+
+def results(request, search_route=None):
+    if not request.acl.search.can_search():
+        return error403(request, _("You don't have permission to search community."))
+
+    search_route = search_route or 'search'
+    result = request.session.get('%s_result' % search_route)
+    if not result:
+        form = QuickSearchForm(request=request)
+        return request.theme.render_to_response('search/error.html',
+                                                {
+                                                 'form': FormFields(form),
+                                                 'search_route': search_route,
+                                                 'message': _("No search results were found."),
+                                                 'disable_search': True,
+                                                },
+                                                context_instance=RequestContext(request))
+
+    form = QuickSearchForm(request=request, initial={'search_query': result['search_query']})
     return request.theme.render_to_response('search/results.html',
                                             {
-                                             'search_phrase': query,
-                                             'results': sqs,
+                                             'form': FormFields(form),
+                                             'search_route': search_route,
+                                             'search_query': result['search_query'],
+                                             'results': Post.objects.filter(id__in=result['search_results']).select_related('thread', 'forum', 'user'),
+                                             'disable_search': True,
                                             },
                                             context_instance=RequestContext(request))
 
-
-def show_sesults(request):
-    pass

+ 15 - 14
misago/apps/threadtype/mixins.py

@@ -6,20 +6,21 @@ from misago.utils.strings import slugify
 class FloodProtectionMixin(object):
     def clean(self):
         cleaned_data = super(FloodProtectionMixin, self).clean()
-        diff = timezone.now() - self.request.user.last_post
-        diff = diff.seconds + (diff.days * 86400)
-        flood_limit = 35
-        wait_for = flood - diff
-        if wait_for > 0:
-            if wait_for < 5:
-                raise forms.ValidationError(_("You can't post one message so quickly after another. Please wait a moment and try again."))
-            else:
-                raise forms.ValidationError(ungettext(
-                        "You can't post one message so quickly after another. Please wait %(seconds)d second and try again.",
-                        "You can't post one message so quickly after another. Please wait %(seconds)d seconds and try again.",
-                    wait_for) % {
-                        'seconds': wait_for,
-                    })
+        if self.request.user.last_post:
+            diff = timezone.now() - self.request.user.last_post
+            diff = diff.seconds + (diff.days * 86400)
+            flood_limit = 35
+            wait_for = flood - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't post one message so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext(
+                            "You can't post one message so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't post one message so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
         return cleaned_data
 
 

+ 1 - 0
misago/context_processors.py

@@ -32,6 +32,7 @@ def common(request):
             'stopwatch': request.stopwatch.time(),
             'user': request.user,
             'version': __version__,
+            'disable_search': False,
         })
         context.update({
             'csrf_id': request.csrf.csrf_id,

+ 37 - 4
misago/models/forummodel.py

@@ -137,6 +137,21 @@ class ForumManager(TreeManager):
                 readable.append(forum.pk)
         return readable
 
+    def forum_by_name(self, forum, acl):
+        forums = self.readable_forums(acl, True)
+        forum = forum.lower()
+        for f in forums:
+            f = self.forums_tree[f]
+            if forum == unicode(f).lower():
+                return f
+        forum_len = len(forum)
+        for f in forums:
+            f = self.forums_tree[f]
+            name = unicode(f).lower()
+            if forum == unicode(f).lower()[0:forum_len]:
+                return f
+        return None
+
 
 class Forum(MPTTModel):
     parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
@@ -198,8 +213,23 @@ class Forum(MPTTModel):
            reverse('private_threads')
         if self.special == 'reports':
            reverse('reports')
+        if self.type == 'category':
+            return reverse('category', kwargs={'forum': self.pk, 'slug': self.slug})
+        if self.type == 'redirect':
+            return reverse('redirect', kwargs={'forum': self.pk, 'slug': self.slug})
         return reverse('forum', kwargs={'forum': self.pk, 'slug': self.slug})
 
+    def thread_link(self, extra):
+        if self.special == 'private_threads':
+           route_prefix = 'private_thread'
+        if self.special == 'reports':
+           route_prefix = 'report'
+        else:
+            route_prefix = 'thread'
+        if extra:
+            return '%s_%s' % (route_prefix, extra) if extra else route_prefix
+        return route_prefix
+
     def thread_url(self, thread, route=None):
         route_prefix = 'thread'
         if self.special:
@@ -254,9 +284,7 @@ class Forum(MPTTModel):
         self.last_poster_slug = thread.last_poster_slug
         self.last_poster_style = thread.last_poster_style
 
-    def sync(self):
-        self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
-        self.posts = self.post_set.filter(moderated=False).count()
+    def sync_last(self):
         self.last_poster = None
         self.last_poster_name = None
         self.last_poster_slug = None
@@ -266,7 +294,7 @@ class Forum(MPTTModel):
         self.last_thread_name = None
         self.last_thread_slug = None
         try:
-            last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[0:][0]
+            last_thread = self.thread_set.filter(moderated=False).filter(deleted=False).order_by('-last').all()[:1][0]
             self.last_poster_name = last_thread.last_poster_name
             self.last_poster_slug = last_thread.last_poster_slug
             self.last_poster_style = last_thread.last_poster_style
@@ -279,6 +307,11 @@ class Forum(MPTTModel):
         except (IndexError, AttributeError):
             pass
 
+    def sync(self):
+        self.threads = self.thread_set.filter(moderated=False).filter(deleted=False).count()
+        self.posts = self.post_set.filter(moderated=False).count()
+        self.sync_last()
+
     def prune(self):
         pass
 

+ 0 - 0
misago/search/__init__.py → misago/search.py


+ 8 - 1
static/cranefly/css/cranefly.css

@@ -871,7 +871,8 @@ footer .container .credits p{margin-bottom:0px;color:#555555;font-size:90%;}foot
 ::-moz-selection{background:#f89406;color:#ffffff;}
 .navbar-header .navbar-inner{background:none;background-color:#f3f3f3;border-bottom:1px solid #dfdfdf;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.navbar-header .navbar-inner .container{background:url("../img/logo.png");background-position:left center;background-repeat:no-repeat;}
 .navbar-header .navbar-inner .brand{margin-left:6px;text-shadow:none;}.navbar-header .navbar-inner .brand:link,.navbar-header .navbar-inner .brand:active,.navbar-header .navbar-inner .brand:visited,.navbar-header .navbar-inner .brand:hover{color:#c24a3b;font-size:200%;}.navbar-header .navbar-inner .brand:link span,.navbar-header .navbar-inner .brand:active span,.navbar-header .navbar-inner .brand:visited span,.navbar-header .navbar-inner .brand:hover span{color:#c0c0c0;}
-.navbar-header .navbar-inner .navbar-search-form{background-color:#ffffff;border:1px solid #dfdfdf;border-radius:3px;margin-top:9px;padding:1px 0px;color:#333333;}.navbar-header .navbar-inner .navbar-search-form input{border:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;margin:0px;}
+.navbar-header .navbar-inner .navbar-search-form{background-color:#ffffff;border:1px solid #dfdfdf;border-radius:3px;margin-top:9px;padding:1px 0px;color:#333333;}.navbar-header .navbar-inner .navbar-search-form.search-disabled{opacity:0.6;filter:alpha(opacity=60);}
+.navbar-header .navbar-inner .navbar-search-form input,.navbar-header .navbar-inner .navbar-search-form input:disabled{border:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;background:none;margin:0px;}
 .navbar-header .navbar-inner .navbar-search-form button{margin:0px;opacity:0.3;filter:alpha(opacity=30);}.navbar-header .navbar-inner .navbar-search-form button:hover,.navbar-header .navbar-inner .navbar-search-form button:active{opacity:0.8;filter:alpha(opacity=80);}
 .navbar-header .navbar-inner .navbar-blocks{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li{margin-left:6px;}.navbar-header .navbar-inner .navbar-blocks li form{margin:0px;padding:0px;}
 .navbar-header .navbar-inner .navbar-blocks li a:link,.navbar-header .navbar-inner .navbar-blocks li a:visited,.navbar-header .navbar-inner .navbar-blocks li .btn-link{background-color:#f8f8f8;border:1px solid #dadada;border-radius:3px;padding:5px 8px;margin-top:9px;}.navbar-header .navbar-inner .navbar-blocks li a:link i,.navbar-header .navbar-inner .navbar-blocks li a:visited i,.navbar-header .navbar-inner .navbar-blocks li .btn-link i{opacity:0.7;filter:alpha(opacity=70);}
@@ -1217,6 +1218,12 @@ 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,.reports-list .thread-details{margin-left:0px !important;}
 .reports-list .thread-icon{display:none !important;float:none;}
+.search-header h1{float:left;}.search-header h1 form{float:right;margin:0px;}.search-header h1 form input{margin-left:21px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;font-size:17.5px;}.search-header h1 form input:focus,.search-header h1 form input:active{border-color:#4dc3ff;-webkit-box-shadow:0px 0px 0px 3px #b3e5ff;-moz-box-shadow:0px 0px 0px 3px #b3e5ff;box-shadow:0px 0px 0px 3px #b3e5ff;}
+.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 h3 a:hover,.search-results .results-list .result h3 a:active{color:#333333;}
+.search-results .results-list .result .post-extra{margin:0px;color:#999999;font-size:11.200000000000001px;line-height:20px;}.search-results .results-list .result .post-extra a{color:#333333;}
+.search-results .results-list .result .post-preview{margin:0px;color:#333333;font-size:15.400000000000002px;line-height:20px;}.search-results .results-list .result .post-preview strong{color:#cf402e;}
 .index-rank-team ul li .label{background-color:#cf402e;color:#ffffff;text-shadow:0px 1px 0px #3d130e;}
 .post-label-team{background-color:#cf402e;}
 .index-rank-mvp ul li .label{background-color:#049cdb;color:#ffffff;text-shadow:0px 1px 0px #011f2c;}

+ 1 - 0
static/cranefly/css/cranefly.less

@@ -91,6 +91,7 @@
 @import "cranefly/changelog.less";
 @import "cranefly/report.less";
 @import "cranefly/reports.less";
+@import "cranefly/search.less";
 
 // Keep ranks last for easy overrides!
 @import "ranks.less";

+ 6 - 1
static/cranefly/css/cranefly/navbar.less

@@ -37,9 +37,14 @@
 
       color: @textColor;
 
-      input {
+      &.search-disabled {
+        .opacity(60);
+      }
+
+      input, input:disabled {
         border: none;
         .box-shadow(none);
+        background: none;
         margin: 0px;
       }
 

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

@@ -0,0 +1,86 @@
+// Search forum
+// ------------------------
+
+.search-header {
+  h1 {
+    float: left;
+
+    form {
+      float: right;
+      margin: 0px;
+
+      input {
+        margin-left: @baseFontSize * 1.5;
+        .box-shadow(0px 0px 0px 3px darken(@bodyBackground, 5%));
+
+        font-size: @fontSizeLarge;
+
+        &:focus, &:active {
+          border-color: lighten(@linkColor, 25%);
+          .box-shadow(0px 0px 0px 3px lighten(@linkColor, 45%));
+        }
+      }
+    }
+  }
+}
+
+.search-resume {
+  .muted {
+    color: lighten(@gray, 15%);
+
+    a {
+      color: @textColor;
+    }
+  }
+}
+
+.search-results {
+  .results-list {
+    .result {
+      border-bottom: 1px solid darken(@bodyBackground, 5%);
+      margin-bottom: (@baseLineHeight / 2);
+      padding-bottom: (@baseLineHeight / 2);
+
+      h3 {
+        margin: 0px;
+
+        line-height: @baseLineHeight;
+
+        a:link, a:visited {
+          color: @gray;
+          font-weight: normal;
+          font-size: @baseFontSize * 1.3;
+          text-decoration: underline;
+        }
+
+        a:hover, a:active {
+          color: @textColor;
+        }
+      }
+
+      .post-extra {
+        margin: 0px;
+
+        color: @grayLight;
+        font-size: @baseFontSize * 0.8;
+        line-height: @baseLineHeight;
+
+        a {
+          color: @textColor;
+        }
+      }
+
+      .post-preview {
+        margin: 0px;
+
+        color: @textColor;
+        font-size: @baseFontSize * 1.1;
+        line-height: @baseLineHeight;
+
+        strong {
+          color: @red;
+        }
+      }
+    }
+  }
+}

+ 6 - 6
templates/cranefly/layout.html

@@ -7,12 +7,12 @@
     <div class="navbar-inner">
       <div class="container">
         <a href="{% url 'index' %}" class="brand">{% if settings.board_header %}{{ settings.board_header }}{% else %}{{ settings.board_name }}{% endif %}</a>
-        {% if not user.is_crawler() %}
+        {% if acl.search.can_search() and not user.is_crawler() %}
         <form action="{% url 'search' %}" method="post" class="navbar-form pull-left">
-          <div class="navbar-search-form">
+          <div class="navbar-search-form{% if disable_search %} search-disabled{% endif %}">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
-            <input type="text" name="search_query" class="span2" placeholder="{% trans %}Search community...{% endtrans %}"{% if search_phrase is defined %} value="{{ search_phrase }}"{% endif %}>
-            <button type="submit" class="btn btn-link"><i class="icon-search"></i></button>
+            <input type="text" name="search_query"{% if disable_search %} disabled="disabled"{% endif %} class="span2" placeholder="{% trans %}Search forums...{% endtrans %}"{% if search_phrase is defined %} value="{{ search_query }}"{% endif %}>
+            <button type="submit"{% if disable_search %} disabled="disabled"{% endif %} class="btn btn-link"><i class="icon-search"></i></button>
           </div>
         </form>
         {% endif %}
@@ -21,8 +21,8 @@
           {{ hook_primary_menu_prepend|safe }}
           <li><a href="{% url 'popular_threads' %}" title="{% trans %}Popular Threads{% endtrans %}" class="hot tooltip-bottom"><i class="icon-fire"></i></a></li>
           <li><a href="{% url 'new_threads' %}" title="{% trans %}New Threads{% endtrans %}" class="fresh tooltip-bottom"><i class="icon-leaf"></i></a></li>{% if not user.crawler %}
-          {% if not user.is_crawler() %}
-          <li><a href="#" title="{% trans %}Search Community{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
+          {% if acl.search.can_search() and not user.is_crawler() %}
+          <li><a href="{% url 'search' %}" title="{% trans %}Search Forums{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
           {% endif %}
           <li><a href="{% url 'users' %}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-user"></i></a></li>
           {% if settings.tos_url or settings.tos_content %}<li><a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{% url 'tos' %}{% endif %}" title="{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Forum Terms of Service{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-certificate"></i></a></li>{% endif %}

+ 6 - 0
templates/cranefly/search/error.html

@@ -0,0 +1,6 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block action %}
+<p class="lead">{{ message }}</p>
+{% endblock %}

+ 17 - 0
templates/cranefly/search/home.html

@@ -0,0 +1,17 @@
+{% extends "cranefly/search/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block action %}
+{% if search_result %}
+<div class="search-resume">
+  <p class="lead muted">{% trans search_query=style_query(search_result.search_query) %}Your search for "{{ search_query }}" is still available.{% endtrans %}</p>
+  <p class="lead muted">{% trans %}To discard it and start new search, enter phrases you want to find in text field above and press search button.{% endtrans %}</p>
+</div>
+{% else %}
+<p class="lead">{% trans %}To search forums, enter phrases you want to find in text field above and press search button.{% endtrans %}</p>
+{% endif %}
+{% endblock %}
+
+{% macro style_query(query) -%}
+<a href="{{ ('%s_results'.format(search_route))|url }}">{{ query }}</a></strong>
+{%- endmacro %}

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

@@ -0,0 +1,21 @@
+{% extends "cranefly/layout.html" %}
+{% import "cranefly/macros.html" as macros with context %}
+{% import "_forms.html" as forms with context %}
+
+{% block title %}{{ macros.page_title(title=_('Search Community')) }}{% endblock %}
+
+{% block container %}
+<div class="page-header header-primary search-header">
+  <div class="container">
+    <h1>{% trans %}Search{% endtrans %} <form action="{{ search_route|url() }}" class="form-inline" method="post">
+       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+       {{ forms.input_text(form.fields.search_query, width=6) }}
+       <button type="submit" class="btn btn-primary">{% trans %}Search{% endtrans %}</button>
+    </form></h1>
+  </div>
+</div>
+
+<div class="container container-primary">
+  {% block action %}{% endblock %}
+</div>
+{% endblock %}

+ 29 - 17
templates/cranefly/search/results.html

@@ -1,23 +1,35 @@
-{% extends "cranefly/layout.html" %}
+{% extends "cranefly/search/layout.html" %}
 {% import "cranefly/macros.html" as macros with context %}
 
-{% block title %}{{ macros.page_title(title=_('Search Community')) }}{% endblock %}
+{% block title %}{{ macros.page_title(title=_("Results"),parent=_('Search Community')) }}{% endblock %}
 
-{% block container %}
-<div class="page-header header-primary">
-  <div class="container">
-    <h1>{% trans %}Search Community{% endtrans %}</h1>
-  </div>
-</div>
-
-<div class="container container-primary">
+{% block action %}
   <div class="search-results">
-    {% for result in results %}
-    <h2><a href="">{{ result.object.thread.name }}</a> <a href="">{{ result.object.forum.name }}</a></h2>
-    <p>{{ result.object.post_clean|highlight(search_phrase)|safe }}</p>
+    {% if results %}
+    <h2>{% trans results=results|length|intcomma %}Search has returned one result{% pluralize %}Search has returned {{ results }} results:{% endtrans %}</h2>
+    <div class="results-list">
+      {% for result in results %}
+      <div class="result">
+        <h3><a href="{{ result.forum.thread_link('find')|url(thread=result.thread_id, slug=result.thread.slug, post=result.pk) }}">{{ result.thread.name }}</a></h3>
+        <p class="post-extra">{% trans forum=forum(result.forum), user=username(result), date=result.date|reltimesince|low %}In {{ forum }} by {{ user }} {{ date }}{% endtrans %}</p>
+        <p class="post-preview">{{ result.post_clean|highlight(search_query)|safe }}</p>
+      </div>
+      {% endfor %}
+    </div>
     {% else %}
-    <p class="lead">Nothing was found :C</p>
-    {% endfor %}
+    <p class="lead">{% trans %}Looks like your search has expired. Please try searching again.{% endtrans %}</p>
+    {% endif %}
   </div>
-</div>
-{% endblock %}
+{% endblock %}
+
+{% macro forum(forum) -%}
+<a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}" class="forum-link">{{ forum.name }}</a>
+{%- endmacro %}
+
+{% macro username(post) -%}
+{% if post.user_id -%}
+<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="user-link">{{ post.user.username }}</a>
+{%- else -%}
+{{ post.user_name }}
+{%- endif %}
+{%- endmacro %}