Alessandro Molina avatar Alessandro Molina committed 1bd226a

Enable sub blogs and cache some partials

Comments (0)

Files changed (9)

 will be available when logged with an user inside
 the *smallpress* Group.
 
+Enabling Whoosh Indexing
+----------------------------
+
+SmallPress has bult in posts indexing whoosh based.
+If you have Whoosh installed it will be used to perform indexing of
+the articles for better lookup in search functions.
+
+When enabled Whoosh will store its index into */tmp/smallpress_whoosh*
+you can change this path by changing the `smallpress_whoosh_index`
+variable in your configuration file.
+
+Multiple Blogs Support
+---------------------------
+
+By default smallpress will work with only one blog, but it supports
+a preliminary multiple blogs implementation. Search and TagCloud will
+be shared by all the blogs, but it is possible to filter the articles
+of only one blog and manage only its articles.
+
+To create a blog access */smallpress/blogs* and create a new one,
+you will then be able to access the subblog and manage it by accessing
+*/smallpress/blogname*
+
 Exposed Partials
 ----------------------
 
     "tgext.tagging",
     "tgext.datahelpers",
     "tgext.ajaxforms",
-    "Whoosh"
+    "tgext.crud >= 0.4"
 ]
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 setup(
     name='tgapp-smallpress',
-    version='0.1',
+    version='0.2',
     description='Pluggable Minimalistic Blog for TurboGears2 with Attachments and Tags',
     long_description=README,
     author='Alessandro Molina',

smallpress/__init__.py

 # -*- coding: utf-8 -*-
 """The tgapp-smallpress package"""
-import tg, os
-from whoosh.index import create_in
+import tg, os, logging
+
+log = logging.getLogger('tgapp-smallpress')
 
 def plugme(app_config, options):
     def init_whoosh(app):
-        from smallpress.model.models import WHOOSH_SCHEMA
+        try:
+            from whoosh.index import create_in
+            log.info('Enabling Whoosh Support')
+            whoosh_enabled = True
+        except ImportError:
+            log.info('Whoosh Not Found, disabled')
+            whoosh_enabled = False
 
-        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)
+        if whoosh_enabled:
+            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
 

smallpress/controllers/root.py

 import os
 from tg import TGController
 from tg import expose, flash, require, url, lurl, request, redirect, validate, tmpl_context, config
-from tg.decorators import before_render
+from tg.decorators import before_render, cached_property
 from tg.i18n import ugettext as _, lazy_ugettext as l_
 from repoze.what import predicates
 
-from smallpress.model import DBSession, Article, Attachment, Tagging
+from smallpress.model import DBSession, Article, Attachment, Tagging, Blog
 from smallpress.helpers import *
 from smallpress.lib.forms import ArticleForm, UploadForm
 from tgext.datahelpers.validators import SQLAEntityConverter
 from datetime import datetime
 from tgext.ajaxforms.ajaxform import formexpose, spinner_icon
 from tgext.tagging import TaggingController
-import whoosh
-from whoosh.query import *
+from tgext.crud import EasyCrudRestController
+
+try:
+    import whoosh
+    from whoosh.query import *
+    whoosh_enabled = True
+except ImportError:
+    whoosh_enabled = False
 
 article_form = ArticleForm()
 upload_form = UploadForm()
 articles_table = DataGrid(fields=[(l_('Actions'), lambda row:link_buttons(row, 'edit', 'delete', 'hide', 'publish')),
+                                  (l_('Blog'), lambda row:row.blog and row.blog.name),
                                   (l_('Title'), lambda row:HTML.a(row.title,
                                                                   href=plug_url('smallpress', '/view/%s'%row.uid,
                                                                                 lazy=True))),
 def inject_css(*args, **kw):
     CSSLink(link='/_pluggable/smallpress/css/style.css').inject()
 
+class BlogsController(EasyCrudRestController):
+    allow_only = predicates.in_group('smallpress')
+    title = "Manage Blogs"
+    model = Blog
+
+    __form_options__ = {
+        '__hide_fields__' : ['uid'],
+        '__omit_fields__' : ['articles']
+    }
+
+    __table_options__ = {
+        '__omit_fields__' : ['uid', 'articles']
+    }
+
 class RootController(TGController):
     tagging = TaggingController(model=Article, session=DBSession, allow_edit=None)
     tagging.search = before_render(inject_css)(tagging.search)
 
+    @cached_property
+    def blogs(self):
+        return BlogsController(DBSession.wrapped_session)
+
     @expose('genshi:smallpress.templates.index')
-    def index(self, *args, **kw):
-        articles = Article.get_published().all()
+    def index(self, blog='', *args, **kw):
+        articles = Article.get_published(blog).all()
         tags = Tagging.tag_cloud_for_set(Article, articles).all()
-        return dict(articles=articles, tags=tags)
+        return dict(articles=articles, tags=tags, blog=blog)
 
     @expose('genshi:smallpress.templates.article')
     @validate(dict(article=SQLAEntityConverter(Article)), error_handler=index)
 
     @require(predicates.in_group('smallpress'))
     @expose('genshi:smallpress.templates.manage')
-    def manage(self, *args, **kw):
-        articles = DBSession.query(Article).order_by(Article.publish_date.desc())
+    def manage(self, blog='', *args, **kw):
+        articles = Article.get_all(blog)
         return dict(table=articles_table, articles=articles,
-                    create_action=self.mount_point+'/new')
+                    create_action=self.mount_point+'/new/'+blog)
 
     @require(predicates.in_group('smallpress'))
     @expose('genshi:smallpress.templates.edit')
-    def new(self, **kw):
+    def new(self, blog=None, **kw):
         attachments_table.register_resources()
 
+        if blog:
+            blog = DBSession.query(Blog).filter_by(name=blog).first()
+
         if 'uid' not in kw:
-            article = Article(author=request.identity['user'])
+            article = Article(author=request.identity['user'], blog=blog and blog)
             DBSession.add(article)
             DBSession.flush()
 
             'publish_date':kw['publish_date'],
             'tags':kw.get('tags', ''),
             'description':kw.get('description', ''),
-            'content':kw.get('content', '')
+            'content':kw.get('content', ''),
         }
 
-        return dict(article=article, value=value,
+        return dict(article=article, value=value, blog=blog and blog.name or '',
                     form=article_form, action=url(self.mount_point+'/save'),
                     upload_form=upload_form, upload_action=url(self.mount_point+'/attach'))
 
             'content':article.content
         }
 
-        return dict(article=article, value=value,
+        return dict(article=article, value=value, blog=article.blog and article.blog.name or '',
                     form=article_form, action=url(self.mount_point+'/save'),
                     upload_form=upload_form, upload_action=url(self.mount_point+'/attach'))
 
         article.description = kw['description']
         article.content = kw['content']
         article.publish_date = datetime.strptime(kw['publish_date'], '%Y-%m-%d %H:%M')
+        article.update_date = datetime.now()
         Tagging.set_tags(article, kw['tags'])
 
         flash(_('Articles successfully saved'))
-        return redirect(self.mount_point+'/manage')
+        return redirect(self.mount_point+'/manage/'+article.blog_name)
 
     @require(predicates.in_group('smallpress'))
     @formexpose(upload_form, 'smallpress.templates.attachments')
     def publish(self, article):
         article.published=True
         flash(_('Article published'))
-        return redirect(self.mount_point+'/manage')
+        return redirect(self.mount_point+'/manage/'+article.blog_name)
 
     @require(predicates.in_group('smallpress'))
     @validate(dict(article=SQLAEntityConverter(Article)), error_handler=manage)
     def hide(self, article):
         article.published=False
         flash(_('Article hidden'))
-        return redirect(self.mount_point+'/manage')
+        return redirect(self.mount_point+'/manage/'+article.blog_name)
 
     @require(predicates.in_group('smallpress'))
     @validate(dict(article=SQLAEntityConverter(Article)), error_handler=manage)
     def delete(self, article):
         DBSession.delete(article)
         flash(_('Article successfully removed'))
-        return redirect(self.mount_point+'/manage')
+        return redirect(self.mount_point+'/manage/'+article.blog_name)
 
     @expose('genshi:smallpress.templates.index')
     @validate(dict(text=UnicodeString(not_empty=True)), error_handler=index)
     def search(self, text=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", text),
-                        Term("title", text),
-                        Term("description", text)])
-            found = searcher.search(query)
-            if len(found):
-                articles = Article.get_published().filter(Article.uid.in_([e['uid'] for e in found])).all()
+        if whoosh_enabled:
+            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", text),
+                            Term("title", text),
+                            Term("description", text)])
+                found = searcher.search(query)
+                if len(found):
+                    articles = Article.get_published().filter(Article.uid.in_([e['uid'] for e in found])).all()
+        else:
+            articles = Article.get_published().filter(Article.content.like('%'+text+'%')).all()
 
         tags = Tagging.tag_cloud_for_set(Article).all()
-        return dict(articles=articles, tags=tags)
+        return dict(articles=articles, tags=tags, blog='')

smallpress/model/__init__.py

                                           'User':app_model.User,
                                           'metadata':DeclarativeBase.metadata})
 
-from models import Article, Attachment
+from models import Article, Attachment, Blog
 

smallpress/model/models.py

 from sqlalchemy import Table, ForeignKey, Column
 from sqlalchemy.types import Unicode, Integer, DateTime
 from sqlalchemy.orm import backref, relation
-from sqlalchemy import event
 
 import os
 from datetime import datetime
 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)
+try:
+    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)
+    whoosh_enabled = True
+except ImportError:
+    whoosh_enabled = False
+
+try:
+    from sqlalchemy import event
+    sqla_events = True
+except ImportError:
+    sqla_events = False
+
+class Blog(DeclarativeBase):
+    __tablename__ = 'smallpress_blogs'
+
+    uid = Column(Integer, autoincrement=True, primary_key=True)
+    name = Column(Unicode(100), nullable=False, default=u"Untitled", index=True)
 
 class Article(DeclarativeBase):
     __tablename__ = 'smallpress_articles'
     author_id = Column(Integer, ForeignKey(primary_key(app_model.User)))
     author = relation(app_model.User, backref=backref('articles'))
 
+    blog_id = Column(Integer, ForeignKey(primary_key(Blog)), nullable=True)
+    blog = relation(Blog, backref=backref('articles'), lazy='joined')
+
     publish_date = Column(DateTime, nullable=False, default=datetime.now)
+    update_date = Column(DateTime, nullable=False, default=datetime.now)
     description = Column(Unicode(150), nullable=False, default=u'Empty article, edit or delete this')
     content = Column(Unicode(32000), nullable=False, default=u'')
 
         obj.refresh_whoosh(1)
 
     @staticmethod
+    def whoosh_before_delete(mapper, connection, obj):
+        obj.refresh_whoosh(-1)
+
+    @staticmethod
     def before_delete(mapper, connection, obj):
-        obj.refresh_whoosh(-1)
+        if whoosh_enabled:
+            Article.whoosh_before_delete(mapper, connection, obj)
 
         from smallpress.model import Tagging
         DBSession.query(Tagging).filter(Tagging.taggable_type == 'Article')\
                                 .filter(Tagging.taggable_id == obj.uid).delete()
 
     @staticmethod
-    def get_published():
+    def get_published(blog=None):
         now = datetime.now()
         articles = DBSession.query(Article).filter_by(published=True)\
-                                           .filter(Article.publish_date<=now)\
-                                           .order_by(Article.publish_date.desc())
+                                           .filter(Article.publish_date<=now)
+
+        if blog:
+            try:
+                blog = blog.name
+            except:
+                pass
+            articles = articles.join(Blog).filter(Blog.name==blog)
+
+        articles = articles.order_by(Article.publish_date.desc())
         return articles
 
+    @staticmethod
+    def get_all(blog=None):
+        articles = DBSession.query(Article)
+        if blog:
+            try:
+                blog = blog.name
+            except:
+                pass
+            articles = articles.join(Blog).filter(Blog.name==blog)
+        articles = articles.order_by(Article.publish_date.desc())
+        return articles
+
+    @property
+    def blog_name(self):
+        blog = self.blog
+        if blog:
+            return blog.name
+        else:
+            return ''
+
     def tagging_display(self):
         return call_partial('smallpress.partials:article_preview', article=self)
 
 
         return identity['user'] == self.author
 
-event.listen(Article, 'after_update', Article.after_update)
-event.listen(Article, 'after_insert', Article.after_insert)
-event.listen(Article, 'before_delete', Article.before_delete)
+if whoosh_enabled and sqla_events:
+    event.listen(Article, 'after_update', Article.after_update)
+    event.listen(Article, 'after_insert', Article.after_insert)
+
+if sqla_events:
+    event.listen(Article, 'before_delete', Article.before_delete)
 
 class Attachment(DeclarativeBase):
     __tablename__ = 'smallpress_attachments'
     def url(self):
         return self.content.url
 
-event.listen(Attachment, 'before_delete', Attachment.delete_file)
+if sqla_events:
+    event.listen(Attachment, 'before_delete', Attachment.delete_file)

smallpress/partials.py

 search_form = SearchForm()
 
 @expose('genshi:smallpress.templates.articles')
-def articles(articles=None):
+def articles(articles=None, blog=None):
     if articles is None:
-        articles=Article.get_published()
+        articles=Article.get_published(blog)
     return dict(articles=articles)
 
 @expose('genshi:smallpress.templates.article_preview')
 def article_preview(article):
-    return dict(article=article)
+    return dict(article=article, tg_cache=dict(key='%s-%s' % (article.uid,
+                                                              article.update_date.strftime('%Y-%m-%d_%H:%M:%S')),
+                                               expire=None,
+                                               type='memory'))
 
 @expose('genshi:smallpress.templates.tagcloud')
 def tagcloud(tags=None):
 
 @expose('genshi:smallpress.templates.search')
 def search():
-    return dict(form=search_form, action=plug_url('smallpress', '/search'))
+    return dict(form=search_form, action=plug_url('smallpress', '/search'),
+                tg_cache=dict(key='search', expire=None, type='memory'))

smallpress/templates/edit.html

 
 <body>
     <div id="smallpress_new_article" class="smallpress">
-        <a href="${h.plug_url('smallpress', '/manage')}">back to management</a>
+        <a href="${h.plug_url('smallpress', '/manage/'+blog)}">back to management</a>
         <h1>Edit Article</h1>
         ${form(value=value, action=action)}
         <div id="smallpress_attachments">

smallpress/templates/index.html

-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude">
     </div>
     <div id="smallpress_articles_box">
         <div py:if="request.identity and 'smallpress' in request.identity['groups']">
-            <a href="${h.plug_url('smallpress', '/manage')}">manage articles</a>
+            <a href="${h.plug_url('smallpress', '/manage/'+blog)}">manage articles</a>
         </div>
         <div>${h.call_partial('smallpress.partials:articles', articles=articles)}</div>
     </div>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.