# HG changeset patch # User k0s # Date 1264372745 18000 # Node ID 1c95a3fa76c1357c822696a1227b6cb557b636cd initial commit of commentator diff -r 000000000000 -r 1c95a3fa76c1 README.txt --- /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 + +The URL pattern is a +`python regular expression `_ +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 `_ +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 `_ : + + ``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 diff -r 000000000000 -r 1c95a3fa76c1 commentator.ini --- /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 diff -r 000000000000 -r 1c95a3fa76c1 commentator/__init__.py --- /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 diff -r 000000000000 -r 1c95a3fa76c1 commentator/example.py --- /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) + diff -r 000000000000 -r 1c95a3fa76c1 commentator/handlers.py --- /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) diff -r 000000000000 -r 1c95a3fa76c1 commentator/middleware.py --- /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 diff -r 000000000000 -r 1c95a3fa76c1 commentator/model.py --- /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 diff -r 000000000000 -r 1c95a3fa76c1 commentator/templates/comment.html --- /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 @@ + + +
+
+ + ${comment['author']} + +

${comment['body']}

+
+ +
+
+
Name:
+
URL:
+ +
+ +
+
+ diff -r 000000000000 -r 1c95a3fa76c1 example/foo.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 @@ + + +
+ comment on me +
+
+ don't comment on this! +
+ + diff -r 000000000000 -r 1c95a3fa76c1 setup.py --- /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 + """, + ) +