[pelican_comment_system] Added comment feeds
This commit is contained in:
@@ -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
|
||||||
|
|||||||
45
pelican_comment_system/comment.py
Normal file
45
pelican_comment_system/comment.py
Normal 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)
|
||||||
@@ -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 }}"
|
||||||
|
|||||||
28
pelican_comment_system/doc/feed.md
Normal file
28
pelican_comment_system/doc/feed.md
Normal 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 %}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -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) }}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user