--- a
+++ b/forgeblog/main.py
@@ -0,0 +1,361 @@
+#-*- python -*-
+import logging
+from datetime import datetime
+import urllib2
+
+# Non-stdlib imports
+import pkg_resources
+import pymongo
+from tg import expose, validate, redirect, flash
+from tg.decorators import with_trailing_slash, without_trailing_slash
+from pylons import g, c, request, response
+from formencode import validators
+from webob import exc
+
+# Pyforge-specific imports
+from allura.app import Application, ConfigOption, SitemapEntry
+from allura.app import DefaultAdminController
+from allura.lib import helpers as h
+from allura.lib.search import search
+from allura.lib.decorators import require_post
+from allura.lib.security import has_access, require_access
+from allura.lib import widgets as w
+from allura.lib.widgets.subscriptions import SubscribeForm
+from allura.lib.widgets import form_fields as ffw
+from allura import model as M
+from allura.controllers import BaseController, AppDiscussionController
+
+# Local imports
+from forgeblog import model as BM
+from forgeblog import version
+from forgeblog import widgets
+
+log = logging.getLogger(__name__)
+
+class W:
+ thread=w.Thread(
+ page=None, limit=None, page_size=None, count=None,
+ style='linear')
+ pager = widgets.BlogPager()
+ new_post_form = widgets.NewPostForm()
+ edit_post_form = widgets.EditPostForm()
+ view_post_form = widgets.ViewPostForm()
+ label_edit = ffw.LabelEdit()
+ attachment_add = ffw.AttachmentAdd()
+ attachment_list = ffw.AttachmentList()
+ preview_post_form = widgets.PreviewPostForm()
+ subscribe_form = SubscribeForm()
+
+class ForgeBlogApp(Application):
+ __version__ = version.__version__
+ tool_label='Blog'
+ default_mount_label='Blog'
+ default_mount_point='blog'
+ permissions = ['configure', 'read', 'write',
+ 'unmoderated_post', 'post', 'moderate', 'admin']
+ ordinal=14
+ installable=True
+ config_options = Application.config_options
+ icons={
+ 24:'images/blog_24.png',
+ 32:'images/blog_32.png',
+ 48:'images/blog_48.png'
+ }
+
+ def __init__(self, project, config):
+ Application.__init__(self, project, config)
+ self.root = RootController()
+ self.admin = BlogAdminController(self)
+
+ @property
+ @h.exceptionless([], log)
+ def sitemap(self):
+ menu_id = self.config.options.mount_label.title()
+ with h.push_config(c, app=self):
+ return [
+ SitemapEntry(menu_id, '.')[self.sidebar_menu()] ]
+
+ @property
+ def show_discussion(self):
+ if 'show_discussion' in self.config.options:
+ return self.config.options['show_discussion']
+ else:
+ return True
+
+ @h.exceptionless([], log)
+ def sidebar_menu(self):
+ base = c.app.url
+ links = [
+ SitemapEntry('Home', base),
+ SitemapEntry('Search', base + 'search'),
+ ]
+ if has_access(self, 'write')():
+ links += [ SitemapEntry('New Post', base + 'new') ]
+ return links
+
+ def admin_menu(self):
+ return super(ForgeBlogApp, self).admin_menu(force_options=True)
+
+ def install(self, project):
+ 'Set up any default permissions and roles here'
+ super(ForgeBlogApp, self).install(project)
+
+ # Setup permissions
+ role_admin = M.ProjectRole.by_name('Admin')._id
+ role_developer = M.ProjectRole.by_name('Developer')._id
+ role_auth = M.ProjectRole.by_name('*authenticated')._id
+ role_anon = M.ProjectRole.by_name('*anonymous')._id
+ self.config.acl = [
+ M.ACE.allow(role_anon, 'read'),
+ M.ACE.allow(role_auth, 'post'),
+ M.ACE.allow(role_auth, 'unmoderated_post'),
+ M.ACE.allow(role_developer, 'write'),
+ M.ACE.allow(role_developer, 'moderate'),
+ M.ACE.allow(role_admin, 'configure'),
+ M.ACE.allow(role_admin, 'admin'),
+ ]
+
+ def uninstall(self, project):
+ "Remove all the tool's artifacts from the database"
+ BM.Attachment.query.remove(dict(app_config_id=c.app.config._id))
+ BM.BlogPost.query.remove(dict(app_config_id=c.app.config._id))
+ BM.BlogPostSnapshot.query.remove(dict(app_config_id=c.app.config._id))
+ super(ForgeBlogApp, self).uninstall(project)
+
+class RootController(BaseController):
+
+ def __init__(self):
+ setattr(self, 'feed.atom', self.feed)
+ setattr(self, 'feed.rss', self.feed)
+ self._discuss = AppDiscussionController()
+
+ @expose('jinja:forgeblog:templates/blog/index.html')
+ @with_trailing_slash
+ def index(self, page=0, limit=10, **kw):
+ query_filter = dict(app_config_id=c.app.config._id)
+ if not has_access(c.app, 'write')():
+ query_filter['state'] = 'published'
+ q = BM.BlogPost.query.find(query_filter)
+ post_count = q.count()
+ limit, page = h.paging_sanitizer(limit, page, post_count)
+ posts = q.sort('timestamp', pymongo.DESCENDING) \
+ .skip(page * limit).limit(limit)
+ c.form = W.preview_post_form
+ c.pager = W.pager
+ return dict(posts=posts, page=page, limit=limit, count=post_count)
+
+ @expose('jinja:forgeblog:templates/blog/search.html')
+ @validate(dict(q=validators.UnicodeString(if_empty=None),
+ history=validators.StringBool(if_empty=False)))
+ def search(self, q=None, history=None, **kw):
+ 'local tool search'
+ results = []
+ count=0
+ if not q:
+ q = ''
+ else:
+ results = search(
+ q,
+ fq=[
+ 'state_s:published',
+ 'is_history_b:%s' % history,
+ 'project_id_s:%s' % c.project._id,
+ 'mount_point_s:%s'% c.app.config.options.mount_point ])
+ if results: count=results.hits
+ return dict(q=q, history=history, results=results or [], count=count)
+
+ @expose('jinja:forgeblog:templates/blog/edit_post.html')
+ @without_trailing_slash
+ def new(self, **kw):
+ require_access(c.app, 'write')
+ now = datetime.utcnow()
+ post = dict(
+ state='draft')
+ c.form = W.new_post_form
+ return dict(post=post)
+
+ @expose()
+ @require_post()
+ @validate(form=W.edit_post_form, error_handler=new)
+ @without_trailing_slash
+ def save(self, **kw):
+ require_access(c.app, 'write')
+ post = BM.BlogPost()
+ for k,v in kw.iteritems():
+ setattr(post, k, v)
+ post.neighborhood_id=c.project.neighborhood_id
+ post.make_slug()
+ post.commit()
+ M.Thread(discussion_id=post.app_config.discussion_id,
+ ref_id=post.index_id(),
+ subject='%s discussion' % post.title)
+ redirect(h.really_unicode(post.url()).encode('utf-8'))
+
+
+ @without_trailing_slash
+ @expose()
+ @validate(dict(
+ since=h.DateTimeConverter(if_empty=None, if_invalid=None),
+ until=h.DateTimeConverter(if_empty=None, if_invalid=None),
+ offset=validators.Int(if_empty=None),
+ limit=validators.Int(if_empty=None)))
+ def feed(self, since=None, until=None, offset=None, limit=None):
+ if request.environ['PATH_INFO'].endswith('.atom'):
+ feed_type = 'atom'
+ else:
+ feed_type = 'rss'
+ title = '%s - %s' % (c.project.name, c.app.config.options.mount_label)
+ feed = M.Feed.feed(
+ dict(project_id=c.project._id, app_config_id=c.app.config._id),
+ feed_type,
+ title,
+ c.app.url,
+ title,
+ since, until, offset, limit)
+ response.headers['Content-Type'] = ''
+ response.content_type = 'application/xml'
+ return feed.writeString('utf-8')
+
+ @with_trailing_slash
+ @expose('jinja:allura:templates/markdown_syntax_dialog.html')
+ def markdown_syntax_dialog(self):
+ 'Static dialog page about how to use markdown.'
+ return dict()
+
+ @expose()
+ def _lookup(self, year, month, name, *rest):
+ slug = '/'.join((year, month, urllib2.unquote(name).decode('utf-8')))
+ post = BM.BlogPost.query.get(slug=slug, app_config_id=c.app.config._id)
+ if post is None:
+ raise exc.HTTPNotFound()
+ return PostController(post), rest
+
+class PostController(BaseController):
+
+ def __init__(self, post):
+ self.post = post
+ setattr(self, 'feed.atom', self.feed)
+ setattr(self, 'feed.rss', self.feed)
+
+ def _check_security(self):
+ require_access(self.post, 'read')
+
+ @expose('jinja:forgeblog:templates/blog/post.html')
+ @with_trailing_slash
+ def index(self, **kw):
+ if self.post.state == 'draft':
+ require_access(self.post, 'write')
+ c.form = W.view_post_form
+ c.subscribe_form = W.subscribe_form
+ c.thread = W.thread
+ version = kw.pop('version', None)
+ post = self._get_version(version)
+ base_post = self.post
+ return dict(post=post, base_post=base_post)
+
+ @expose('jinja:forgeblog:templates/blog/edit_post.html')
+ @without_trailing_slash
+ def edit(self, **kw):
+ require_access(self.post, 'write')
+ c.form = W.edit_post_form
+ c.attachment_add = W.attachment_add
+ c.attachment_list = W.attachment_list
+ c.label_edit = W.label_edit
+ return dict(post=self.post)
+
+ @without_trailing_slash
+ @expose('jinja:forgeblog:templates/blog/post_history.html')
+ def history(self):
+ posts = self.post.history()
+ return dict(title=self.post.title, posts=posts)
+
+ @without_trailing_slash
+ @expose('jinja:forgeblog:templates/blog/post_diff.html')
+ def diff(self, v1, v2):
+ p1 = self._get_version(int(v1))
+ p2 = self._get_version(int(v2))
+ result = h.diff_text(p1.text, p2.text)
+ return dict(p1=p1, p2=p2, edits=result)
+
+ @expose()
+ @require_post()
+ @validate(form=W.edit_post_form, error_handler=edit)
+ @without_trailing_slash
+ def save(self, delete=None, **kw):
+ require_access(self.post, 'write')
+ if delete:
+ self.post.delete()
+ flash('Post deleted', 'info')
+ redirect(h.really_unicode(c.app.url).encode('utf-8'))
+ for k,v in kw.iteritems():
+ setattr(self.post, k, v)
+ self.post.commit()
+ redirect('.')
+
+ @without_trailing_slash
+ @require_post()
+ @expose()
+ def revert(self, version):
+ require_access(self.post, 'write')
+ orig = self._get_version(version)
+ if orig:
+ self.post.text = orig.text
+ self.post.commit()
+ redirect('.')
+
+ @expose()
+ @validate(W.subscribe_form)
+ def subscribe(self, subscribe=None, unsubscribe=None):
+ if subscribe:
+ self.post.subscribe(type='direct')
+ elif unsubscribe:
+ self.post.unsubscribe()
+ redirect(h.really_unicode(request.referer).encode('utf-8'))
+
+ @without_trailing_slash
+ @expose()
+ @validate(dict(
+ since=h.DateTimeConverter(if_empty=None, if_invalid=None),
+ until=h.DateTimeConverter(if_empty=None, if_invalid=None),
+ offset=validators.Int(if_empty=None),
+ limit=validators.Int(if_empty=None)))
+ def feed(self, since=None, until=None, offset=None, limit=None):
+ if request.environ['PATH_INFO'].endswith('.atom'):
+ feed_type = 'atom'
+ else:
+ feed_type = 'rss'
+ feed = M.Feed.feed(
+ dict(ref_id=self.post.index_id()),
+ feed_type,
+ 'Recent changes to %s' % self.post.title,
+ self.post.url(),
+ 'Recent changes to %s' % self.post.title,
+ since, until, offset, limit)
+ response.headers['Content-Type'] = ''
+ response.content_type = 'application/xml'
+ return feed.writeString('utf-8')
+
+ def _get_version(self, version):
+ if not version: return self.post
+ try:
+ return self.post.get_version(version)
+ except ValueError:
+ raise exc.HTTPNotFound()
+
+class BlogAdminController(DefaultAdminController):
+ def __init__(self, app):
+ self.app = app
+
+ @without_trailing_slash
+ @expose('jinja:forgeblog:templates/blog/admin_options.html')
+ def options(self):
+ return dict(app=self.app,
+ allow_config=has_access(self.app, 'configure')())
+
+ @without_trailing_slash
+ @expose()
+ @require_post()
+ def set_options(self, show_discussion=False):
+ self.app.config.options['show_discussion'] = show_discussion and True or False
+ flash('Blog options updated')
+ redirect(h.really_unicode(c.project.url()+'admin/tools').encode('utf-8'))