Skip to content

Commit

Permalink
Add a comment system on pelican.
Browse files Browse the repository at this point in the history
This system is based on email.
Users have to send comment by mail :
- By thereselve using right format (pretty difficult)
- Using a html form "actioning" to a script sending the mail as necessary
  (far easier for them)

You will have to setup the following config variable :
- IMAP_HOSTNAME : The hostname where to read comments/emails.
- IMAP_USERNAME : The username to use.
- IMAP_PASSWORD : The password to use.
- IMAP_READBOX  : In wish directory read emails (INBOX, INBOX/validated)

In samples/commentsystem :
- send_comment.py :
  A python cgi script to send the mail where it need to be sent
- comments.html
  A template to insert at the end of your article template:

    <div id="article">
      ...
      {{ article.content }}
      ...
      {% include 'comments.html' %}
      ...
    </div>

This code worked on my early tests. However it never has been push
in production. It is published here cause somes want it to be shared.
Feel free to use it as you need it but use it at your own risk.

License is the same of pelican (AGPL) cause code is part of pelican.
But I will not bother you if you take it in your project on
another license. (Maybe write my name somewhere on your sources :P )
  • Loading branch information
mgautierfr committed Nov 26, 2013
1 parent 8864b55 commit c590eca
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 6 deletions.
4 changes: 2 additions & 2 deletions pelican/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time

from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator)
StaticGenerator, PdfGenerator, CommentsGenerator)
from pelican.settings import read_settings
from pelican.utils import clean_output_dir, files_changed
from pelican.writers import Writer
Expand Down Expand Up @@ -77,7 +77,7 @@ def run(self):


def get_generator_classes(self):
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator]
generators = [ArticlesGenerator, PagesGenerator, StaticGenerator, CommentsGenerator]
if self.settings['PDF_GENERATOR']:
generators.append(PdfGenerator)
return generators
Expand Down
48 changes: 48 additions & 0 deletions pelican/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from os import getenv
from sys import platform, stdin

from datetime import datetime
from dateutil.parser import parse as dateparse

from hashlib import md5

class Page(object):
"""Represents a page
Given a content, and metadata, create an adequate object.
Expand Down Expand Up @@ -113,6 +118,49 @@ class Article(Page):

class Quote(Page):
base_properties = ('author', 'date')

class Comment(Page):
mandatory_properties = ('author', 'date', 'slug', 'lang', )

def __init__(self, content, metadata=None, settings=None, msg=None):
# init parameters
if not metadata:
metadata = {}
if not settings:
settings = _DEFAULT_CONFIG

self._content = content

local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
local_metadata.update(metadata)

# set metadata as attributes
for key, value in local_metadata.items():
setattr(self, key.lower(), value)

if msg:
self.msg = msg

# manage the date format
if hasattr(self, 'lang') and self.lang in settings['DATE_FORMATS']:
self.date_format = settings['DATE_FORMATS'][self.lang]
else:
self.date_format = settings['DEFAULT_DATE_FORMAT']

if not hasattr(self, 'id'):
setattr(self, 'id', md5(self._content.encode('utf8')).hexdigest())

if not hasattr(self, 'parent'):
setattr(self, 'parent', 0)

if not hasattr(self, 'date'):
setattr(self, 'date', dateparse(msg['date']))
# setattr(self, 'date', datetime.strptime(msg['date'], "%X %x"))

if hasattr(self, 'website') and getattr(self, 'website').strip() == u"http://":
setattr(self, 'website', u"")

self.locale_date = self.date.strftime(self.date_format.encode('ascii','xmlcharrefreplace')).decode('utf')


def is_valid_content(content, f):
Expand Down
70 changes: 69 additions & 1 deletion pelican/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from pelican.utils import copy, get_relative_path, process_translations, open
from pelican.utils import slugify
from pelican.contents import Article, Page, is_valid_content
from pelican.contents import Article, Page, is_valid_content, Comment
from pelican.readers import read_file
from pelican.log import *

Expand Down Expand Up @@ -313,6 +313,74 @@ def generate_output(self, writer):
self.generate_feeds(writer)
self.generate_pages(writer)

class CommentsGenerator(Generator):
"""Generate comments"""

def __init__(self, *args, **kwargs):
self.comments = {}
super(CommentsGenerator, self).__init__(*args, **kwargs)

def generate_context(self):
for article in self.context['articles']:
if article.slug not in self.comments.keys():
self.comments[article.slug] = []
try:
import imaplib,email
from StringIO import StringIO
except ImportError:
raise Exception("unable to find imaplib or mail")
try:
connection = imaplib.IMAP4_SSL(self.settings['IMAP_HOSTNAME'])
connection.login(self.settings['IMAP_USERNAME'], self.settings['IMAP_PASSWORD'])
except:
raise Exception("unable to connect to imap server "+self.settings['IMAP_HOSTNAME']+" with "+self.settings['IMAP_USERNAME'])

try:
connection.select(self.settings['IMAP_READBOX'],readonly=True)
except:
raise Exception("unable to select box "+self.settings['IMAP_READBOX'])

try:
typ, msg_ids = connection.search(None, 'UNDELETED')
except:
raise Exception("oups")
if True:
# try:
for msg_id in msg_ids[0].split():
typ, msg_data = connection.fetch(msg_id, '(RFC822)')
msg_data = msg_data[0]
raw_content = None
if isinstance(msg_data, tuple):
msg = email.message_from_string(msg_data[1])
if msg.is_multipart():
print "should no be multipart"
continue
raw_content = msg.get_payload(decode=True)
if raw_content:
charset = msg.get_content_charset()
if not charset:
charset = "utf8"
try:
unicode_content = raw_content.decode(charset)
except:
charset = "iso-8859-1"
unicode_content = raw_content.decode(charset)
content, metadata = read_file(unicode_content,"comment")
comment = Comment(content, metadata, settings=self.settings, msg=msg)
if not is_valid_content(comment, str(msg_id)):
continue


if comment.slug not in self.comments.keys():
self.comments[comment.slug] = []
self.comments[comment.slug].append(comment)

for slug, commentlist in self.comments.items():
commentlist.sort(key=lambda comment : comment.date)
self._update_context(('comments', ))
# except:
# raise Exception("unable to connect to get data")


class PagesGenerator(Generator):
"""Generate pages"""
Expand Down
31 changes: 30 additions & 1 deletion pelican/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,35 @@ def read(self, filename):
metadata[name] = _process_metadata(name, value[0])
return content, metadata

class CommentReader(Reader):
enabled = bool(Markdown)
extension = "comment"

def read(self, text):
metaText = []
contentText = []
metaSection = True
for line in text.split("\n"):
if line.startswith("----------"):
metaSection = False
continue
if metaSection:
metaText.append(line)
else:
contentText.append(line)

md = Markdown(extensions = ['meta'], safe_mode="escape" )
md.convert("\n".join(metaText))
metadata = {}
for name, value in md.Meta.items():
name = name.lower()
metadata[name] = _process_metadata(name, value[0])

md = Markdown(extensions = ['codehilite'],safe_mode="escape")
content = md.convert("\n".join(contentText))

print metadata
return content, metadata

class HtmlReader(Reader):
extension = "html"
Expand All @@ -139,7 +168,7 @@ def read_file(filename, fmt=None, settings=None):
if not fmt:
fmt = filename.split('.')[-1]
if fmt not in _EXTENSIONS.keys():
raise TypeError('Pelican does not know how to parse %s' % filename)
raise TypeError('Pelican does not know how to parse %s of extension %s' % (filename, fmt))
reader = _EXTENSIONS[fmt]()
settings_key = '%s_EXTENSIONS' % fmt.upper()
if settings and settings_key in settings:
Expand Down
6 changes: 5 additions & 1 deletion pelican/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@
'DEFAULT_METADATA': (),
'FILES_TO_COPY': (),
'DEFAULT_STATUS': 'published',
'ARTICLE_PERMALINK_STRUCTURE': ''
'ARTICLE_PERMALINK_STRUCTURE': '',
'IMAP_HOSTNAME': '',
'IMAP_USERNAME': '',
'IMAP_PASSWORD': '',
'IMAP_READBOX': ''
}

def read_settings(filename):
Expand Down
42 changes: 42 additions & 0 deletions samples/commentsystem/comments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<div id="comments">

<h2>Commentaires:</h2>
<div id="commentSection">
<ol id="commentsList">
{% for slug, _comments in comments %}
{% if article.slug == slug %}
{% for comment in _comments %}
<li>
Par
{% if comment.website %}
<a class="author" href="{{comment.website|e}}">{{ comment.author|e }}</a>
{% else %}
{{ comment.author|e }}
{% endif %}
le <span class="date">{{ comment.locale_date }}</span>
<div class="commentContent">
{{ comment.content }}
</div>
</li>
{% endfor %}
{% endif %}
{% endfor %}
</ol>
</div>

<div class="answer">
<h3>Répondre :</h3>
<form name='comment' method='POST' accept-charset="UTF-8" action='http://your.website/send_comment.py'>
<input type='hidden' name='slug' value="{{ article.slug }}" />
<input type='hidden' name='lang' value="{{ article.lang }}" />
<table>
<tr><td>Nom:</td><td><input type='text' name='realname' size='20'/>*</td></tr>
<tr><td>Email:</td><td><input type='text' name='email' size='20'/>*</td></tr>
<tr><td>Site web:</td><td><input type='text' name='website' size='20' value='http://'/></td></tr>
<tr><td>Commentaire: *</td><td/></tr>
</table>
<textarea name='comment' cols='100' rows='10'></textarea><br/>
<input type='submit' name='Submit' value="Poster un commentaire"/>
</form>
</div>
</div>
78 changes: 78 additions & 0 deletions samples/commentsystem/send_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

import cgi

import cgitb
cgitb.enable()

import sys

form = cgi.FieldStorage()


if not ( form.has_key("slug")
and form.has_key("lang")
and form.has_key("realname")
and form.has_key("email")
and form.has_key("comment")
):
print 'Location: /pages/comment-error.html'
print

sys.exit(0)

from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from email import Charset
from email.Utils import formataddr
from subprocess import Popen, PIPE
from hashlib import md5


slug = form.getvalue('slug').decode('utf8')
lang = form.getvalue('lang').decode('utf8')
realname = form.getvalue('realname').decode('utf8')
email = form.getvalue('email').decode('utf8')
website = form.getvalue('website','').decode('utf8')
website = website.strip()
# [TODO] handle https
if website == u"http://":
website = u""
if not website.startswith(u"http://"):
website = u"http://" + website
comment = form.getvalue('comment').decode('utf8')

id_ = md5(comment.encode('utf8')).hexdigest()

content = u"""slug: %(slug)s
lang: %(lang)s
author: %(realname)s
email: %(email)s
website: %(website)s
id: %(id)s
----------
%(comment)s
""" % {'slug':slug, 'lang':lang, 'realname':realname, 'email':email, 'website':website, 'id':id_, 'comment':comment}
content = content.encode('utf8')

subject = u"[Comment] %(slug)s - %(name)s" % {'slug': slug, 'name' : realname}

send_name = str(Header(realname, "utf8"))
email = str(Header(email, "utf8"))


To=u"comment-box@hostname.example"
msg = MIMEText(content, "plain", "utf8")
msg["From"] = formataddr((realname, email))
msg["To"] = Header(To, 'utf8')
msg.add_header('reply-to', str(Header(To, 'utf8')))

msg["Subject"] = Header(subject, 'utf8')

p = Popen(["/usr/lib/sendmail", "-t"], stdin=PIPE)
p.communicate(msg.as_string())

print 'Location: /pages/comment-ok.html'
print
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

VERSION = "2.7.2" # find a better way to do so.

requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz']
requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz','dateutils']
if sys.version_info < (2,7):
requires.append('argparse')

Expand Down

0 comments on commit c590eca

Please sign in to comment.