[pelican_comment_system] Added comment feeds

This commit is contained in:
Bernhard Scheirle
2014-04-14 11:55:29 +02:00
parent 98e2f16059
commit 24fce038c3
6 changed files with 132 additions and 78 deletions

View File

@@ -6,6 +6,7 @@ The comments are stored in Markdown files. Each comment in it own file.
- Static comments for each article - Static comments for each article
- Replies to comments - Replies to comments
- Avatars and [Identicons](https://en.wikipedia.org/wiki/Identicon) - Avatars and [Identicons](https://en.wikipedia.org/wiki/Identicon)
- Comment Atom Feed for each article
- Easy styleable via the themes - Easy styleable via the themes
@@ -18,6 +19,7 @@ Bernhard Scheirle | <http://blog.scheirle.de> | <https://github.com/Scheirle>
## Instructions ## Instructions
- [Installation and basic usage](doc/installation.md) - [Installation and basic usage](doc/installation.md)
- [Avatars and Identicons](doc/avatars.md) - [Avatars and Identicons](doc/avatars.md)
- [Comment Atom Feed](doc/feed.md)
- [Comment Form (aka: never gather Metadata)](doc/form.md) - [Comment Form (aka: never gather Metadata)](doc/form.md)
## Requirements ## Requirements

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
"""
from pelican import contents
from pelican.contents import Content
class Comment(Content):
mandatory_properties = ('author', 'date')
default_template = 'None'
def __init__(self, id, avatar, content, metadata, settings, source_path, context):
super(Comment,self).__init__( content, metadata, settings, source_path, context )
self.id = id
self.replies = []
self.avatar = avatar
self.title = "Posted by: " + str(metadata['author'])
def addReply(self, comment):
self.replies.append(comment)
def getReply(self, id):
for reply in self.replies:
if reply.id == id:
return reply
else:
deepReply = reply.getReply(id)
if deepReply != None:
return deepReply
return None
def __lt__(self, other):
return self.metadata['date'] < other.metadata['date']
def sortReplies(self):
for r in self.replies:
r.sortReplies()
self.replies = sorted(self.replies)
def countReplies(self):
amount = 0
for r in self.replies:
amount += r.countReplies()
return amount + len(self.replies)

View File

@@ -23,7 +23,7 @@ PELICAN_COMMENT_SYSTEM_AUTHORS = {
``` ```
## Theme ## Theme
To display the avatars and identicons simply add the following in the "comment for loop" in you theme: To display the avatars and identicons simply add the following in the "comment for loop" in your theme:
```html ```html
<img src="{{ SITEURL }}/{{ comment.avatar }}" <img src="{{ SITEURL }}/{{ comment.avatar }}"

View File

@@ -0,0 +1,28 @@
# Comment Atom Feed
## Custom comment url
Be sure that the id of the html tag containing the comment matches `COMMENT_URL`.
##### pelicanconf.py
```python
COMMENT_URL = "#my_own_comment_id_{path}"
```
##### Theme
```html
{% for comment in article.comments recursive %}
...
<article id="my_own_comment_id_{{comment.id}}">{{ comment.content }}</article>
...
{% endfor %}
```
## Theme
#### Link
To display a link to the article feed simply add the following to your theme:
```html
{% if article %}
<a href="{{ FEED_DOMAIN }}/{{ PELICAN_COMMENT_SYSTEM_FEED|format(article.slug) }}">Comment Atom Feed</a>
{% endif %}
```

View File

@@ -8,15 +8,16 @@ Activate the plugin by adding it to your `pelicanconf.py`
And modify your `article.html` theme (see below). And modify your `article.html` theme (see below).
## Settings ## Settings
Name | Type | Default | Description Name | Type | Default | Description
-----------------------------------------------|-----------|--------------------|------- -----------------------------------------------|-----------|----------------------------|-------
`PELICAN_COMMENT_SYSTEM` | `boolean` | `False` | Activates or deactivates the comment system `PELICAN_COMMENT_SYSTEM` | `boolean` | `False` | Activates or deactivates the comment system
`PELICAN_COMMENT_SYSTEM_DIR` | `string` | `comments` | Folder where the comments are stored `PELICAN_COMMENT_SYSTEM_DIR` | `string` | `comments` | Folder where the comments are stored
`PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH` | `string` | `images/identicon` | Output folder where the identicons are stored `PELICAN_COMMENT_SYSTEM_IDENTICON_OUTPUT_PATH` | `string` | `images/identicon` | Relative URL to the output folder where the identicons are stored
`PELICAN_COMMENT_SYSTEM_IDENTICON_DATA` | `tuple` | `()` | Contains all Metadata tags, which in combination identifies a comment author (like `('author', 'email')`) `PELICAN_COMMENT_SYSTEM_IDENTICON_DATA` | `tuple` | `()` | Contains all Metadata tags, which in combination identifies a comment author (like `('author', 'email')`)
`PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE` | `int` | `72` | Width and height of the identicons. Has to be a multiple of 3. `PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE` | `int` | `72` | Width and height of the identicons. Has to be a multiple of 3.
`PELICAN_COMMENT_SYSTEM_AUTHORS` | `dict` | `{}` | Comment authors, which should have a specific avatar. More infos [here](avatars.md) `PELICAN_COMMENT_SYSTEM_AUTHORS` | `dict` | `{}` | Comment authors, which should have a specific avatar. More info [here](avatars.md)
`PELICAN_COMMENT_SYSTEM_FEED` | `string` |`feeds/comment.%s.atom.xml` | Relative URL to output the Atom feed for each article.`%s` gets replaced with the slug of the article. More info [here](http://docs.getpelican.com/en/latest/settings.html#feed-settings)
`COMMENT_URL` | `string` | `#comment-{path}` | `{path}` gets replaced with the id of the comment. More info [here](feed.md)
## Folder structure ## Folder structure
Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`. Every comment file has to be stored in a sub folder of `PELICAN_COMMENT_SYSTEM_DIR`.
@@ -24,7 +25,7 @@ Sub folders are named after the `slug` of the articles.
So the comments to your `foo-bar` article are stored in `comments/foo-bar/` So the comments to your `foo-bar` article are stored in `comments/foo-bar/`
The filenames of the comment files are up to you. But the filename is the Identifier of the comment (without extension). The filenames of the comment files are up to you. But the filename is the Identifier of the comment (**with** extension).
##### Example folder structure ##### Example folder structure
@@ -44,8 +45,8 @@ The filenames of the comment files are up to you. But the filename is the Identi
Tag | Required | Description Tag | Required | Description
--------------|-----------|---------------- --------------|-----------|----------------
`date` | yes | Date when the comment was posted `date` | yes | Date when the comment was posted
`replyto` | no | Identifier of the parent comment. Identifier = Filename (without extension) `author` | yes | Name of the comment author
`locale_date` | forbidden | Will be overwritten with a locale representation of the date `replyto` | no | Identifier of the parent comment. Identifier = Filename (**with** extension)
Every other (custom) tag gets parsed as well and will be available through the theme. Every other (custom) tag gets parsed as well and will be available through the theme.
@@ -62,26 +63,27 @@ Every other (custom) tag gets parsed as well and will be available through the t
## Theme ## Theme
In the `article.html` theme file are now two more variables available. In the `article.html` theme file are now two more variables available.
Variables | Description Variables | Description
----------------------------------|-------------------------- -------------------------|--------------------------
`article.metadata.comments_count` | Amount of total comments for this article (including replies to comments) `article.comments_count` | Amount of total comments for this article (including replies to comments)
`article.metadata.comments` | Array containing the top level comments for this article (no replies to comments) `article.comments` | Array containing the top level comments for this article (no replies to comments)
### Comment object ### Comment object
Variables | Description The comment object is a [content](https://github.com/getpelican/pelican/blob/master/pelican/contents.py#L34) object, so all common attributes are available (like author, content, date, local_date, metadata, ...).
Additional following attributes are added:
Attribute | Description
-----------|-------------------------- -----------|--------------------------
`id` | Identifier of this comment `id` | Identifier of this comment
`content` | Content of this comment
`metadata` | All metadata as in the comment file (or described above)
`replies` | Array containing the top level replies for this comment `replies` | Array containing the top level replies for this comment
`avatar` | Path to the avatar or identicon of the comment author `avatar` | Path to the avatar or identicon of the comment author
##### Example article.html theme ##### Example article.html theme
(only the comment section) (only the comment section)
```html ```html
{% if article.metadata.comments %} {% if article.comments %}
{% for comment in article.metadata.comments recursive %} {% for comment in article.comments recursive %}
{% set metadata = comment.metadata %}
{% if loop.depth0 == 0 %} {% if loop.depth0 == 0 %}
{% set marginLeft = 0 %} {% set marginLeft = 0 %}
{% else %} {% else %}
@@ -89,9 +91,9 @@ Variables | Description
{% endif %} {% endif %}
<article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;"> <article id="comment-{{comment.id}}" style="border: 1px solid #DDDDDD; padding: 5px 0px 0px 5px; margin: 0px -1px 5px {{marginLeft}}px;">
<a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a> <a href="{{ SITEURL }}/{{ article.url }}#comment-{{comment.id}}" rel="bookmark" title="Permalink to this comment">Permalink</a>
<h4>{{ metadata['author'] }}</h4> <h4>{{ comment.author }}</h4>
<p>Posted on <abbr class="published" title="{{ metadata['date'].isoformat() }}">{{ metadata['locale_date'] }}</abbr></p> <p>Posted on <abbr class="published" title="{{ comment.date.isoformat() }}">{{ comment.locale_date }}</abbr></p>
{{ comment.metadata['my_custom_metadata'] }}
{{ comment.content }} {{ comment.content }}
{% if comment.replies %} {% if comment.replies %}
{{ loop(comment.replies) }} {{ loop(comment.replies) }}

View File

@@ -10,51 +10,17 @@ Author: Bernhard Scheirle
import logging import logging
import os import os
import copy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from itertools import chain from itertools import chain
from pelican import signals from pelican import signals
from pelican.utils import strftime
from pelican.readers import MarkdownReader from pelican.readers import MarkdownReader
from pelican.writers import Writer
import avatars import avatars
from comment import Comment
class Comment:
def __init__(self, id, metadata, content, avatar):
self.id = id
self.content = content
self.metadata = metadata
self.replies = []
self.avatar = avatar
def addReply(self, comment):
self.replies.append(comment)
def getReply(self, id):
for reply in self.replies:
if reply.id == id:
return reply
else:
deepReply = reply.getReply(id)
if deepReply != None:
return deepReply
return None
def __lt__(self, other):
return self.metadata['date'] < other.metadata['date']
def sortReplies(self):
for r in self.replies:
r.sortReplies()
self.replies = sorted(self.replies)
def countReplies(self):
amount = 0
for r in self.replies:
amount += r.countReplies()
return amount + len(self.replies)
def pelican_initialized(pelican): def pelican_initialized(pelican):
from pelican.settings import DEFAULT_CONFIG from pelican.settings import DEFAULT_CONFIG
@@ -64,6 +30,8 @@ def pelican_initialized(pelican):
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ()) DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72) DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {}) DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
DEFAULT_CONFIG.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
DEFAULT_CONFIG.setdefault('COMMENT_URL', '#comment-{path}')
if pelican: if pelican:
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False) pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM', False)
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments') pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_DIR', 'comments')
@@ -71,6 +39,8 @@ def pelican_initialized(pelican):
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ()) pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_DATA', ())
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72) pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_IDENTICON_SIZE', 72)
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {}) pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_AUTHORS', {})
pelican.settings.setdefault('PELICAN_COMMENT_SYSTEM_FEED', os.path.join('feeds', 'comment.%s.atom.xml'))
pelican.settings.setdefault('COMMENT_URL', '#comment-{path}')
def initialize(article_generator): def initialize(article_generator):
@@ -82,21 +52,26 @@ def initialize(article_generator):
article_generator.settings['PELICAN_COMMENT_SYSTEM_AUTHORS'], article_generator.settings['PELICAN_COMMENT_SYSTEM_AUTHORS'],
) )
def add_static_comments(gen, metadata): def add_static_comments(gen, content):
if gen.settings['PELICAN_COMMENT_SYSTEM'] != True: if gen.settings['PELICAN_COMMENT_SYSTEM'] != True:
return return
metadata['comments_count'] = 0 content.comments_count = 0
metadata['comments'] = [] content.comments = []
if not 'slug' in metadata: #Modify the local context, so we get proper values for the feed
logger.warning("pelican_comment_system: cant't locate comments files without slug tag in the article") context = copy.copy(gen.context)
return context['SITEURL'] += "/" + content.url
context['SITENAME'] = "Comments for: " + content.title
context['SITESUBTITLE'] = ""
path = gen.settings['PELICAN_COMMENT_SYSTEM_FEED'] % content.slug
writer = Writer(gen.output_path, settings=gen.settings)
folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], metadata['slug']) folder = os.path.join(gen.settings['PELICAN_COMMENT_SYSTEM_DIR'], content.slug)
if not os.path.isdir(folder): if not os.path.isdir(folder):
logger.debug("No comments found for: " + metadata['slug']) logger.debug("No comments found for: " + content.slug)
writer.write_feed( [], context, path)
return return
reader = MarkdownReader(gen.settings) reader = MarkdownReader(gen.settings)
@@ -106,17 +81,19 @@ def add_static_comments(gen, metadata):
for file in os.listdir(folder): for file in os.listdir(folder):
name, extension = os.path.splitext(file) name, extension = os.path.splitext(file)
if extension[1:].lower() in reader.file_extensions: if extension[1:].lower() in reader.file_extensions:
content, meta = reader.read(os.path.join(folder, file)) com_content, meta = reader.read(os.path.join(folder, file))
meta['locale_date'] = strftime(meta['date'], gen.settings['DEFAULT_DATE_FORMAT'])
avatar_path = avatars.getAvatarPath(name, meta) avatar_path = avatars.getAvatarPath(name, meta)
com = Comment(name, meta, content, avatar_path)
com = Comment(file, avatar_path, com_content, meta, gen.settings, file, context)
if 'replyto' in meta: if 'replyto' in meta:
replies.append( com ) replies.append( com )
else: else:
comments.append( com ) comments.append( com )
writer.write_feed( comments + replies, context, path)
#TODO: Fix this O(n²) loop #TODO: Fix this O(n²) loop
for reply in replies: for reply in replies:
for comment in chain(comments, replies): for comment in chain(comments, replies):
@@ -130,8 +107,8 @@ def add_static_comments(gen, metadata):
comments = sorted(comments) comments = sorted(comments)
metadata['comments_count'] = len(comments) + count content.comments_count = len(comments) + count
metadata['comments'] = comments content.comments = comments
def writeIdenticonsToDisk(gen, writer): def writeIdenticonsToDisk(gen, writer):
avatars.generateAndSaveMissingAvatars() avatars.generateAndSaveMissingAvatars()
@@ -139,5 +116,5 @@ def writeIdenticonsToDisk(gen, writer):
def register(): def register():
signals.initialized.connect(pelican_initialized) signals.initialized.connect(pelican_initialized)
signals.article_generator_init.connect(initialize) signals.article_generator_init.connect(initialize)
signals.article_generator_context.connect(add_static_comments) signals.article_generator_write_article.connect(add_static_comments)
signals.article_writer_finalized.connect(writeIdenticonsToDisk) signals.article_writer_finalized.connect(writeIdenticonsToDisk)