Commits

Alessandro Molina  committed 416848e

Add search support

  • Participants
  • Parent commits 556451c

Comments (0)

Files changed (11)

     "tgext.pluggable",
     "tgext.tagging",
     "tgext.datahelpers",
-    "tgext.ajaxforms"
+    "tgext.ajaxforms",
+    "Whoosh"
 ]
 
 setup(

File smallpress/__init__.py

 # -*- coding: utf-8 -*-
 """The tgapp-smallpress package"""
+import tg, os
+from whoosh.index import create_in
 
 def plugme(app_config, options):
+    def init_whoosh(app):
+        from smallpress.model.models import WHOOSH_SCHEMA
+
+        index_path = tg.config.get('smallpress_whoosh_index', '/tmp/smallpress_whoosh')
+        if not os.path.exists(index_path):
+            os.mkdir(index_path)
+            ix = create_in(index_path, WHOOSH_SCHEMA)
+
+        return app
+
+    app_config.register_hook('before_config', init_whoosh)
     return dict(appid='smallpress', global_helpers=False)

File smallpress/bootstrap.py

 
 from smallpress import model
 from tgext.pluggable import app_model
+import tg
 
 def bootstrap(command, conf, vars):
     print 'Bootstrapping smallpress...'

File smallpress/controllers/root.py

 """Main Controller"""
 import os
 from tg import TGController
-from tg import expose, flash, require, url, lurl, request, redirect, validate, tmpl_context
+from tg import expose, flash, require, url, lurl, request, redirect, validate, tmpl_context, config
 from tg.decorators import before_render
 from tg.i18n import ugettext as _, lazy_ugettext as l_
 from repoze.what import predicates
 
 from tw.api import CSSLink
 from tw.forms import DataGrid
+from tw.forms.validators import UnicodeString
 from datetime import datetime
 from tgext.ajaxforms.ajaxform import formexpose, spinner_icon
 from tgext.tagging import TaggingController
+import whoosh
+from whoosh.query import *
 
 article_form = ArticleForm()
 upload_form = UploadForm()
     def delete(self, article):
         DBSession.delete(article)
         flash(_('Article successfully removed'))
-        return redirect(self.mount_point+'/manage')
+        return redirect(self.mount_point+'/manage')
+
+    @expose('genshi:smallpress.templates.index')
+    @validate(dict(what=UnicodeString(not_empty=True)), error_handler=index)
+    def search(self, what=None):
+        articles = []
+
+        index_path = config.get('smallpress_whoosh_index', '/tmp/smallpress_whoosh')
+        ix = whoosh.index.open_dir(index_path)
+        with ix.searcher() as searcher:
+            query = Or([Term("content", what),
+                        Term("title", what),
+                        Term("description", what)])
+            found = searcher.search(query)
+            if len(found):
+                articles = Article.get_published().filter(Article.uid.in_([e['uid'] for e in found])).all()
+
+        tags = Tagging.tag_cloud_for_set(Article)
+        return dict(articles=articles, tags=tags)

File smallpress/lib/forms.py

     action = plug_url('smallpress', '/attach', lazy=True)
     ajaxurl = plug_url('smallpress', '/upload_form_show', lazy=True)
     submit_text = 'Attach'
+
+class SearchForm(ListForm):
+    class fields(WidgetsList):
+        text = TextField(suppress_label=True, validator=UnicodeString(not_empty=True),
+                         attrs=dict(placeholder=l_('Search...')))
+
+    submit_text = 'Search'

File smallpress/model/models.py

-from tg import request
+from tg import request, config
 
 from sqlalchemy import Table, ForeignKey, Column
 from sqlalchemy.types import Unicode, Integer, DateTime
 from sqlalchemy.orm import backref, relation
+from sqlalchemy import event
 
 from datetime import datetime
 from smallpress.model import DeclarativeBase, DBSession
 from tgext.datahelpers.fields import Attachment as DataHelpersAttachment
 from tgext.pluggable import call_partial
 
+import whoosh
+import whoosh.index
+import whoosh.fields
+WHOOSH_SCHEMA = whoosh.fields.Schema(uid=whoosh.fields.ID(stored=True),
+                                     title=whoosh.fields.TEXT(stored=True),
+                                     description=whoosh.fields.TEXT,
+                                     content=whoosh.fields.TEXT)
+
 class Article(DeclarativeBase):
     __tablename__ = 'smallpress_articles'
 
     uid = Column(Integer, autoincrement=True, primary_key=True)
-    title = Column(Unicode(150), nullable=False, default="Untitled", index=True)
+    title = Column(Unicode(150), nullable=False, default=u"Untitled", index=True)
     published = Column(Integer, nullable=False, default=0)
     private = Column(Integer, nullable=False, default=0)
 
     author = relation(app_model.User, backref=backref('articles'))
 
     publish_date = Column(DateTime, nullable=False, default=datetime.now)
-    description = Column(Unicode(150), nullable=False, default='Empty article, edit or delete this')
-    content = Column(Unicode(32000), nullable=False, default='')
+    description = Column(Unicode(150), nullable=False, default=u'Empty article, edit or delete this')
+    content = Column(Unicode(32000), nullable=False, default=u'')
+
+    def refresh_whoosh(self, update=False):
+        index_path = config.get('smallpress_whoosh_index', '/tmp/smallpress_whoosh')
+        ix = whoosh.index.open_dir(index_path)
+        writer = ix.writer()
+        if not update:
+            writer.add_document(uid=unicode(self.uid), title=self.title,
+                                content=self.content,
+                                description=self.description)
+        else:
+            writer.update_document(uid=unicode(self.uid), title=self.title,
+                                   content=self.content,
+                                   description=self.description)
+        writer.commit()
+
+    @staticmethod
+    def after_update(mapper, connection, obj):
+        obj.refresh_whoosh(True)
+
+    @staticmethod
+    def after_insert(mapper, connection, obj):
+        obj.refresh_whoosh(False)
 
     @staticmethod
     def get_published():
 
         return identity['user'] == self.author
 
+event.listen(Article, 'after_update', Article.after_update)
+event.listen(Article, 'after_insert', Article.after_insert)
+
 class Attachment(DeclarativeBase):
     __tablename__ = 'smallpress_attachments'
 

File smallpress/partials.py

 from model import Article, Tagging
 from tgext.pluggable import plug_url
 from tgext.tagging import TagCloud
+from smallpress.lib.forms import SearchForm
+
+search_form = SearchForm()
 
 @expose('genshi:smallpress.templates.articles')
 def articles(articles=None):
     tagcloud=TagCloud(tagging_url=plug_url('smallpress', '/tagging'))
     if not tags:
         tags = Tagging.tag_cloud_for_set(Article)
-    return dict(tagcloud=tagcloud, tags=tags)
+    return dict(tagcloud=tagcloud, tags=tags)
+
+@expose('genshi:smallpress.templates.search')
+def search():
+    return dict(form=search_form, action=plug_url('smallpress', '/search'))

File smallpress/public/css/style.css

 .smallpress_article h2 a {
     text-decoration: none;
     color: black;
+}
+
+#smallpress_search #text_container {
+    float: left;
+}
+
+#smallpress_search ul {
+    list-style: none;
+    padding-left: 10px;
 }

File smallpress/templates/article.html

 
 <body>
     <div id="smallpress_rightbar">
-        <div id="smallpress_search">
-            <h3>Search</h3>
-        </div>
+        ${h.call_partial('smallpress.partials:search')}
         ${h.call_partial('smallpress.partials:tagcloud')}
         <div id="smallpress_history">
             <h3>Archive</h3>

File smallpress/templates/index.html

 
 <body>
     <div id="smallpress_rightbar">
-        <div id="smallpress_search">
-            <h3>Search</h3>
-        </div>
+        ${h.call_partial('smallpress.partials:search')}
         ${h.call_partial('smallpress.partials:tagcloud', tags=tags)}
         <div id="smallpress_history">
             <h3>Archive</h3>

File smallpress/templates/search.html

+<div id="smallpress_search">
+    <h3>Search</h3>
+    ${form(action=action)}
+</div>