changeset 0:1c95a3fa76c1

initial commit of commentator
author k0s <k0scist@gmail.com>
date Sun, 24 Jan 2010 17:39:05 -0500
parents
children aebfbb10aefb
files README.txt commentator.ini commentator/__init__.py commentator/example.py commentator/handlers.py commentator/middleware.py commentator/model.py commentator/templates/comment.html example/foo.html setup.py
diffstat 10 files changed, 465 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.txt	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,76 @@
+commentator
+===========
+
+WSGI commenting middleware
+
+To use
+------
+
+Make a factory wrapping your app in the commentator middleware.
+Currently, commentator only pickles comments.  To the constructor of
+Commentator, pass a database (the path to the pickle) and a pattern.
+The pattern is in the form of
+
+<URL pattern>#<xpath pattern> -> URL
+
+The URL pattern is a 
+`python regular expression <ttp://docs.python.org/library/re.html>`_
+to match against the request's PATH_INFO.  
+
+The xpath pattern is where you want to place the comments on the
+page.  See http://www.w3schools.com/XPath/ for more about xpath
+expressions.
+
+The URL is a 
+`python string template <http://docs.python.org/library/string.html>`_
+that is substituted for groups in the URL regular expression and
+element attributes in the found nodes.  The element attributes are
+referenced by name (``${id}``, ``${class}``, etc) and the groups are
+referenced by number (``${1}``, ...).
+
+
+Example
+-------
+
+A reference implementation is illustrated in the commentator.ini
+file.  This uses the pattern:
+
+ ``commentator.pattern = (.*)#.//div[@id='comment_on_this'] -> ${1}``
+
+What this pattern says is
+
+ * comment on every PATH_INFO ``(.*)``
+ * append the rendered content template to ``div[@id='comment_on_this']``
+ * reference the PATH_INFO as the canonical URL ``${1}``
+
+To comment on every HTML page at the end of the body, you would use
+
+ ``commentator.pattern = (.*)#.//body -> ${1}``
+
+A more complex example is in the ``.ini`` file, commented out, for use with
+`bitsyblog <http://k0s.org/hg/bitsyblog>`_ :
+ 
+  ``commentator.pattern = /blog/.*#.//div[@class='blog-entry'] -> /blog/${id}``
+
+This pattern says:
+
+ * comment on all paths under blog
+ * put the comments at the end of each ``div[@class='blog-entry']``
+ * get the URI from the ``div``'s id, not from the ``PATH_INFO``
+
+
+TODO
+----
+
+This is very alpha.  I'd be happy to work more on this if anyone wants
+it.  A few outstanding issues:
+
+ * fix weird lxml issue where you have to put .// for elements
+ * allow commenting on multiple resources (multiple patterns per instance)
+ * locking pickle files
+ * fix couch....not sure what's wrong
+ * allow use of CSS classes, not just xpath
+
+--
+
+http://k0s.org
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator.ini	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,25 @@
+#!/usr/bin/env paster
+
+[DEFAULT]
+debug = true
+email_to = k0scist@gmail.com
+smtp_server = localhost
+error_email_from = paste@localhost
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 1080
+
+[composite:main]
+use = egg:Paste#urlmap
+/ = commentator
+
+set debug = false
+
+[app:commentator]
+paste.app_factory = commentator.example:factory
+directory = %(here)s/example
+commentator.database = %(here)s/test.pck
+commentator.pattern = (.*)#.//div[@id='comment_on_this'] -> ${1}
+#commentator.pattern = /blog/.*##([0-9]{14})->/blog/$1 # for bitsyblog
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/__init__.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,2 @@
+#
+from middleware import Commentator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/example.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,19 @@
+import os
+
+from middleware import Commentator
+from paste.httpexceptions import HTTPExceptionHandler
+from paste.urlparser import StaticURLParser
+from pkg_resources import resource_filename
+
+
+def factory(global_conf, **app_conf):
+    """create a webob view and wrap it in middleware"""
+
+    keystr = 'commentator.'
+    args = dict([(key.split(keystr, 1)[-1], value)
+                 for key, value in app_conf.items()
+                 if key.startswith(keystr) ])
+    app = StaticURLParser(app_conf['directory'])
+    commentator = Commentator(app, **args)
+    return HTTPExceptionHandler(commentator)
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/handlers.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,95 @@
+"""
+request handlers:
+these are instantiated for every request, then called
+"""
+
+from urlparse import urlparse
+from webob import Response, exc
+
+class HandlerMatchException(Exception):
+    """the handler doesn't match the request"""
+
+class Handler(object):
+
+    methods = set(['GET']) # methods to listen to
+    handler_path = [] # path elements to match        
+
+    @classmethod
+    def match(cls, app, request):
+
+        # check the method
+        if request.method not in cls.methods:
+            return None
+
+        # check the path
+        if request.environ['path'][:len(cls.handler_path)] != cls.handler_path:
+            return None
+
+        try:
+            return cls(app, request)
+        except HandlerMatchException:
+            return None
+    
+    def __init__(self, app, request):
+        self.app = app
+        self.request = request
+        self.application_path = urlparse(request.application_url)[2]
+
+    def link(self, path=(), permanant=False):
+        if isinstance(path, basestring):
+            path = [ path ]
+        path = [ i.strip('/') for i in path ]
+        if permanant:
+            application_url = [ self.request.application_url ]
+        else:
+            application_url = [ self.application_path ]
+        path = application_url + path
+        return '/'.join(path)
+
+    def redirect(self, location):
+        raise exc.HTTPSeeOther(location=location)
+
+class GenshiHandler(Handler):
+
+    def __init__(self, app, request):
+        Handler.__init__(self, app, request)
+        self.data = { 'request': request,
+                      'link': self.link }
+
+    def __call__(self):
+        return getattr(self, self.request.method.title())()
+
+    def Get(self):
+        # needs to have self.template set
+        template = self.app.loader.load(self.template)
+        return Response(content_type='text/html',
+                        body=template.generate(**self.data).render('html'))
+
+class PostComment(Handler):
+    methods=set(['POST'])
+
+    @classmethod
+    def match(cls, app, request):
+
+        # check the method
+        if request.method not in cls.methods:
+            return None
+
+        # check the path        
+        if request.path_info.endswith('/%s' % app.url):
+            try:
+                return cls(app, request)
+            except HandlerMatchException:
+                return None
+    
+
+    def __call__(self):
+        
+        # get URL
+        url = self.request.path_info.rsplit('/' + self.app.url, 1)[0]
+
+        # add comment to DB
+        self.app.model.comment(url, **dict(self.request.POST))
+
+        # redirect to original resource
+        return exc.HTTPSeeOther(location=url)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/middleware.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,120 @@
+"""
+request dispatcher:
+data persisting across requests should go here
+"""
+
+import os
+import re
+
+from handlers import PostComment
+from model import CouchComments, PickleComments
+from genshi.template import TemplateLoader
+from lxml import etree
+from lxmlmiddleware import LXMLMiddleware
+from paste.fileapp import FileApp
+from pkg_resources import resource_filename
+from string import Template
+from webob import Request, Response, exc
+
+class LaxTemplate(Template):
+    idpattern = r'[_a-z0-9]+'
+
+class Commentator(LXMLMiddleware):
+
+    ### class level variables
+    defaults = { 'auto_reload': 'False',
+                 'database': 'commentator',
+                 'template_dirs': '',
+                 'pattern': '.*',
+                 'path': 'html',
+                 'url': '.comment',
+                 'template': 'comment.html' }
+
+    def __init__(self, app, **kw):
+
+        self.app = app
+
+        # set instance parameters from kw and defaults
+        for key in self.defaults:
+            setattr(self, key, kw.get(key, self.defaults[key]))
+        self.auto_reload = self.auto_reload.lower() == 'true'
+
+        # request handlers
+        self.handlers = [ PostComment ]
+
+        # template loader
+        self.template_dirs = self.template_dirs.split()
+        self.template_dirs.append(resource_filename(__name__, 'templates'))
+        self.loader = TemplateLoader(self.template_dirs,
+                                     auto_reload=self.auto_reload)
+
+        # URL,path
+        assert '#' in self.pattern
+        self.url_pattern, self.xpath_pattern = self.pattern.split('#', 1)
+        assert '->' in self.xpath_pattern
+        self.xpath_pattern, self.mapping = [i.strip() for i in self.xpath_pattern.split('->')]
+
+        # string template for URL substitution
+        self.mapping = LaxTemplate(self.mapping)
+
+        # backend: comment storage
+        self.model = PickleComments(self.database)
+
+    def __call__(self, environ, start_response):
+
+        # get a request object
+        request = Request(environ)
+
+        # get the path 
+        path = request.path_info.strip('/').split('/')
+        if path == ['']:
+            path = []
+        request.environ['path'] = path
+
+        # save the path;  not sure why i need to do this
+        environ['commentator.path_info'] = request.path_info
+
+        # match the request to a handler
+        for h in self.handlers:
+            handler = h.match(self, request)
+            if handler is not None:
+                break
+        else:
+            return LXMLMiddleware.__call__(self, environ, start_response)
+
+        # get response
+        res = handler()
+        return res(environ, start_response)
+
+    def manipulate(self, environ, tree):
+        url_match = re.match(self.url_pattern, environ['commentator.path_info'])
+        if not url_match:
+            return tree
+
+        # make string template of the groups
+        groups_dict = dict([(str(index+1), value) 
+                         for index, value in enumerate(url_match.groups())])
+
+        for element in tree.findall(self.xpath_pattern):
+
+            # get url
+            str_dict = groups_dict.copy()
+            for key in element.keys():
+                str_dict[key] = element.get(key)
+            uri = self.mapping.substitute(str_dict)
+
+            # get comments
+            # TODO
+
+            # genshi data
+            data = {}
+            data['comments'] = self.model.comments(uri)
+            data['action'] = '%s/%s' % (uri, self.url)
+
+            # render template
+            template = self.loader.load(self.template)
+            comments = template.generate(**data).render()
+            comments = etree.fromstring(comments)
+            element.append(comments)
+
+        return tree
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/model.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,57 @@
+import os
+import pickle
+
+from datetime import datetime
+        
+class PickleComments(object):
+    # TODO: locking
+    def __init__(self, database):
+        self.database = database
+        if not os.path.exists(database):
+            f = file(database, 'w')
+            pickle.dump({}, f)
+            f.close()
+
+    def comment(self, uri, **kw):
+        f = file(self.database)
+        comments = pickle.load(f)
+        f.close()
+        comments.setdefault(uri, []).append(kw)
+        f = file(self.database, 'w')
+        comments = pickle.dump(comments, f)
+        f.close()
+
+    def comments(self, uri):
+        f = file(self.database)
+        comments = pickle.load(f)
+        f.close()
+        return comments.get(uri, [])
+
+    
+try:
+    import couchdb
+
+    class CouchComments(object):
+        def __init__(self, db):
+            self.couch = couchdb.Server()
+            if db not in self.couch:
+                self.db = self.couch.create(db)
+            else:
+                self.db = self.couch[db]
+
+        def comment(self, uri, **kw):
+            if uri in self.db:
+                comments = self.db[uri]['comments']
+                comments.append(kw)
+                self.db[uri] = { 'comments': comments }
+            else:
+                self.db[uri] = { 'comments': [ kw ] }
+
+        def comments(self, uri):
+            if uri in self.db:
+                doc = self.db[uri]
+                return doc ['comments']
+            return []
+
+except ImportError:
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/commentator/templates/comment.html	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,25 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      py:strip="True">
+<div class="comments">
+  <div py:for="comment in comments" class="comment">
+    <a py:strip="not comment['link']" href="${comment['link']}">
+      ${comment['author']}
+    </a>
+    <p>${comment['body']}</p>
+  </div>
+
+  <form method="post" action="${action}">
+    <dl>
+      <dt>Name:</dt><dd><input type="text" name="author"/></dd>
+      <dt>URL:</dt><dd><input type="text" name="link"/></dd>
+      <textarea name="body"></textarea>
+    </dl>
+    <input type="submit" value="Comment"/>
+  </form>
+</div>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example/foo.html	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,10 @@
+<html>
+  <body>
+    <div id="comment_on_this">
+      comment on me
+    </div>
+    <div id="dont_comment">
+      don't comment on this!
+    </div>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Sun Jan 24 17:39:05 2010 -0500
@@ -0,0 +1,36 @@
+from setuptools import setup, find_packages
+
+try:
+    description = file('README.txt').read()
+except IOError: 
+    description = ''
+
+version = "0.1"
+
+setup(name='commentator',
+      version=version,
+      description="WSGI commenting middleware",
+      long_description=description,
+      classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      author='Jeff Hammel',
+      author_email='k0scist@gmail.com',
+      url='http://k0s.org',
+      license="",
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+         'WebOb',	
+         'Paste',
+         'PasteScript',
+         'genshi',
+#         'CouchDB'
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [paste.app_factory]
+      commentator = commentator.example:factory
+      """,
+      )
+