"""
This module contains an extension to Blosxom file entries to support
comments.
Contributors:
Ted Leung
Will Guaraldi
Wari Wahab
Robert Wall
Bill Mill
Roberto De Almeida
David Geller
If you make any changes to this plugin, please a send a patch with your
changes to twl+pyblosxom@sauria.com so that we can incorporate your changes.
Thanks!
This plugin requires the PyXML module.
This module supports the following config parameters (they are not
required):
comment_dir - the directory we're going to store all our comments in.
this defaults to datadir + "comments".
comment_ext - the file extension used to denote a comment file.
this defaults to "cmt".
comment_draft_ext - the file extension used for new comments that have
not been manually approved by you. this defaults
to comment_ext (i.e. there is no draft stage)
comment_smtp_server - the smtp server to send comments notifications
through.
comment_smtp_from - the person comment notifications will be from.
If you omit this, the from address will be the
e-mail address as input in the comment form
comment_smtp_to - the person to send comment notifications to.
comment_nofollow - set this to 1 to add rel="nofollow" attributes to
links in the description -- these attributes are embedded
in the stored representation.
Comments are stored 1 per file in a parallel hierarchy to the datadir
hierarchy. The filename of the comment is the filename of the blog
entry, plus the creation time of the comment as a float, plus the
comment extension. The contents of the comment file is an RSS 2.0
formatted item.
Comments now follow the blog_encoding variable specified in config.py
Each entry has to have the following properties in order to work with
comments:
1. absolute_path - the category of the entry. ex. "dev/pyblosxom"
2. fn - the filename of the entry without the file extension and without
the directory. ex. "staticrendering"
3. file_path - the absolute_path plus the fn. ex. "dev/pyblosxom/staticrendering"
Also, for any entry that you don't want to have comments, just add
"nocomments" to the properties of the entry.
If you would like comment previews, you need to do 2 things.
1) Add a preview button to comment-form.html like this:
You may change the contents of the value attribute, but the name of
the input must be "preview".
2) Still in your comment-form.html template, you need to use the comment
values to fill in the values of your input fields like so:
If there is no preview available, these variables will be stripped
from the text and cause no problem.
3) Copy comment.html to a template called comment-preview.html. All of
the available variables from the comment template are available for
this template.
This plugin implements Google's nofollow support for links in the body of the
comment. If you display the link of the comment poster in your HTML template
then you must add the rel="nofollow" attribute to your template as well
Copyright (c) 2003-2005 Ted Leung
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
__author__ = "Ted Leung"
__version__ = "$Id: comments.py,v 1.41.4.7 2005/06/10 13:58:29 willhelm Exp $"
import cgi, glob, os.path, re, time, cPickle, os, codecs
from xml.sax.saxutils import escape
from Pyblosxom import tools
from Pyblosxom.entries.base import EntryBase
def cb_start(args):
request = args["request"]
config = request.getConfiguration()
logdir = config.get("logdir", "/tmp/")
# logfile = os.path.normpath(logdir + os.sep + "comments.log")
# tools.make_logger(logfile)
if not config.has_key('comment_dir'):
config['comment_dir'] = os.path.join(config['datadir'],'comments')
if not config.has_key('comment_ext'):
config['comment_ext'] = 'cmt'
if not config.has_key('comment_draft_ext'):
config['comment_draft_ext'] = config['comment_ext']
if not config.has_key('comment_nofollow'):
config['comment_nofollow'] = 0
def verify_installation(request):
config = request.getConfiguration()
retval = 1
if config.has_key('comment_dir') and not os.path.isdir(config['comment_dir']):
print 'The "comment_dir" property in the config file must refer to a directory'
retval = 0
smtp_keys_defined = []
smtp_keys=['comment_smtp_server', 'comment_smtp_from', 'comment_smtp_to']
for k in smtp_keys:
if config.has_key(k):
smtp_keys_defined.append(k)
if smtp_keys_defined:
for i in smtp_keys:
if i not in smtp_keys_defined:
print("Missing comment SMTP property: '%s'" % i)
retval = 0
optional_keys = ['comment_dir', 'comment_ext', 'comment_draft_ext']
for i in optional_keys:
if not config.has_key(i):
print("missing optional property: '%s'" % i)
return retval
def createhtmlmail (html, headers):
"""Create a mime-message that will render HTML in popular
MUAs, text in better ones
Based on: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/67083"""
import MimeWriter
import mimetools
import cStringIO
out = cStringIO.StringIO() # output buffer for our message
htmlin = cStringIO.StringIO(html)
text = re.sub('<.*?>', '', html)
txtin = cStringIO.StringIO(text)
writer = MimeWriter.MimeWriter(out)
for header,value in headers:
writer.addheader(header, value)
writer.addheader("MIME-Version", "1.0")
writer.startmultipartbody("alternative")
writer.flushheaders()
subpart = writer.nextpart()
subpart.addheader("Content-Transfer-Encoding", "quoted-printable")
pout = subpart.startbody("text/plain", [("charset", 'us-ascii')])
mimetools.encode(txtin, pout, 'quoted-printable')
txtin.close()
subpart = writer.nextpart()
subpart.addheader("Content-Transfer-Encoding", "quoted-printable")
pout = subpart.startbody("text/html", [("charset", 'us-ascii')])
mimetools.encode(htmlin, pout, 'quoted-printable')
htmlin.close()
writer.lastpart()
msg = out.getvalue()
out.close()
return msg
def readComments(entry, config):
"""
@param: a file entry
@type: dict
@returns: a list of comment dicts
"""
encoding = config['blog_encoding']
filelist = glob.glob(cmtExpr(entry, config))
if not entry.has_key('num_comments'):
entry['num_comments'] = len(filelist)
comments = [readComment(f, encoding) for f in filelist]
comments = [(cmt['cmt_time'], cmt) for cmt in comments]
comments.sort()
return [c[1] for c in comments]
def getCommentCount(entry, config):
"""
@param: a file entry
@type: dict
@returns: the number of comments for the entry
"""
if entry['absolute_path'] == None: return 0
filelist = glob.glob(cmtExpr(entry,config))
if filelist is not None:
return len(filelist)
return 0
def cmtExpr(entry, config):
"""
Return a string containing the regular expression for comment entries
@param: a file entry
@type: dict
@returns: a string with the directory path for the comment
@param: configuratioin dictionary
@type: dict
@returns: a string containing the regular expression for comment entries
"""
cmtDir = os.path.join(config['comment_dir'], entry['absolute_path'])
cmtExpr = os.path.join(cmtDir,entry['fn']+'-*.'+config['comment_ext'])
return cmtExpr
def readComment(filename, encoding):
"""
Read a comment from filename
@param filename: filename containing a comment
@type filename: string
@param encoding: encoding of comment files
@type encoding: string
@returns: a comment dict
"""
from xml.sax import make_parser, SAXException
from xml.sax.handler import feature_namespaces, ContentHandler
class cmtHandler(ContentHandler):
def __init__(self, cmt):
self._data = ""
self.cmt = cmt
def startElement(self, name, atts):
self._data = ""
def endElement(self, name):
self.cmt['cmt_'+name] = self._data
def characters(self, content):
self._data += content
cmt = {}
try:
parser = make_parser()
parser.setFeature(feature_namespaces, 0)
handler = cmtHandler(cmt)
parser.setContentHandler(handler)
parser.parse(filename)
cmt['cmt_time'] = float(cmt['cmt_pubDate']) #time.time()
cmt['cmt_pubDate'] = time.ctime(float(cmt['cmt_pubDate'])) #pretty time
return cmt
except: #don't error out on a bad comment
# tools.log('bad comment file: %s' % filename)
pass
def writeComment(request, config, data, comment, encoding):
"""
Write a comment
@param config: dict containing pyblosxom config info
@type config: dict
@param data: dict containing entry info
@type data: dict
@param comment: dict containing comment info
@type comment: dict
@return: The success or failure of creating the comment.
@rtype: string
"""
entry = data['entry_list'][0]
cdir = os.path.join(config['comment_dir'],entry['absolute_path'])
cdir = os.path.normpath(cdir)
if not os.path.isdir(cdir):
os.makedirs(cdir, mode = 0775)
cfn = os.path.join(cdir,entry['fn']+"-"+comment['pubDate']+"."+config['comment_draft_ext'])
argdict = { "request": request, "comment": comment }
reject = tools.run_callback("comment_reject",
argdict,
donefunc=lambda x:x)
if reject == 1:
return "Comment rejected."
def makeXMLField(name, field):
return "<"+name+">" + cgi.escape(field.get(name, "")) + ""+name+">\n";
filedata = '\n' % encoding
filedata += " ,
# , , , , ' + ' '.join(chunk) + ' ( (,
, , , ,
,
# , , , ,
body=re.sub('<a href="([^"]*)">([^&]*)</a>',
'\\2', body)
body=re.sub('<a href=\'([^\']*)\'>([^&]*)</a>',
'\\2', body)
body=re.sub('<em>([^&]*)</em>', '\\1', body)
body=re.sub('<i>([^&]*)</i>', '\\1', body)
body=re.sub('<b>([^&]*)</b>', '\\1', body)
body=re.sub('<blockquote>([^&]*)</blockquote>',
'
\\1
', body)
body=re.sub('<br\s*/?>\n?','\n',body)
body=re.sub('<abbr>([^&]*)</abbr>', '\\1', body)
body=re.sub('<acronym>([^&]*)</acronym>', '\\1', body)
body=re.sub('<big>([^&]*)</big>', '\\1', body)
body=re.sub('<cite>([^&]*)</cite>', '\\1', body)
body=re.sub('<code>([^&]*)</code>', '\\1
', body)
body=re.sub('<dfn>([^&]*)</dfn>', '\\1', body)
body=re.sub('<kbd>([^&]*)</kbd>', '\\1', body)
body=re.sub('<pre>([^&]*)</pre>', '\\1
', body)
body=re.sub('<small>([^&]*)</small>', '\\1', body)
body=re.sub('<strong>([^&]*)</strong>', '\\1', body)
body=re.sub('<sub>([^&]*)</sub>', '\\1', body)
body=re.sub('<sup>([^&]*)</sup>', '\\1', body)
body=re.sub('<tt>([^&]*)</tt>', '\\1', body)
body=re.sub('<var>([^&]*)</var>', '\\1', body)
body=re.sub('</?p>','\n\n',body).strip()
# wiki like support: _em_, *b*, [url title]
body=re.sub(r'\b_(\w.*?)_\b', r'\1', body)
body=re.sub(r'\*(\w.*?)\*', r'\1', body)
body=re.sub(r'\[(\w+:\S+\.gif) (.*?)\]', r'', body)
body=re.sub(r'\[(\w+:\S+\.jpg) (.*?)\]', r'', body)
body=re.sub(r'\[(\w+:\S+\.png) (.*?)\]', r'', body)
body=re.sub(r'\[(\w+:\S+) (.*?)\]', r'\2', body).strip()
# unordered lists: consecutive lines starting with spaces and an asterisk
chunk=re.compile(r'^( *\*.*(?:\n *\*.*)+)',re.M).split(body)
for i in range(1, len(chunk), 2):
(html,stack)=('', [''])
for indent,line in re.findall(r'( +)\* +(.*)', chunk[i]) + [('','')]:
if indent>stack[-1]: (stack,html)=(stack+[indent],html+'\r')
while indent
\n', body)
body=re.compile('.*?
)\r.*?
)