Merge pull request #1247 from paylogic/multiple-authors

Multiple authors implementation for #956
This commit is contained in:
Justin Mayer 2014-02-13 19:08:34 -08:00
commit 826ff4df50
16 changed files with 122 additions and 59 deletions

View file

@ -7,6 +7,7 @@ Next release
* Added the `:modified:` metadata field to complement `:date:`. * Added the `:modified:` metadata field to complement `:date:`.
Used to specify the last date and time an article was updated independently from the date and time it was published. Used to specify the last date and time an article was updated independently from the date and time it was published.
* Produce inline links instead of reference-style links when importing content. * Produce inline links instead of reference-style links when importing content.
* Multiple authors support added via new `:authors:` metadata field.
3.3.0 (2013-09-24) 3.3.0 (2013-09-24)
================== ==================

View file

@ -311,7 +311,7 @@ this metadata in text files via the following syntax (give your file the
:tags: thats, awesome :tags: thats, awesome
:category: yeah :category: yeah
:slug: my-super-post :slug: my-super-post
:author: Alexis Metaireau :authors: Alexis Metaireau, Conan Doyle
:summary: Short version for index and feeds :summary: Short version for index and feeds
Pelican implements an extension to reStructuredText to enable support for the Pelican implements an extension to reStructuredText to enable support for the
@ -331,7 +331,7 @@ pattern::
Category: Python Category: Python
Tags: pelican, publishing Tags: pelican, publishing
Slug: my-super-post Slug: my-super-post
Author: Alexis Metaireau Authors: Alexis Metaireau, Conan Doyle
Summary: Short version for index and feeds Summary: Short version for index and feeds
This is the content of my super blog post. This is the content of my super blog post.
@ -351,7 +351,7 @@ interprets the HTML in a very straightforward manner, reading metadata from
<meta name="date" content="2012-07-09 22:28" /> <meta name="date" content="2012-07-09 22:28" />
<meta name="modified" content="2012-07-10 20:14" /> <meta name="modified" content="2012-07-10 20:14" />
<meta name="category" content="yeah" /> <meta name="category" content="yeah" />
<meta name="author" content="Alexis Métaireau" /> <meta name="authors" content="Alexis Métaireau, Conan Doyle" />
<meta name="summary" content="Short version for index and feeds" /> <meta name="summary" content="Short version for index and feeds" />
</head> </head>
<body> <body>
@ -380,6 +380,9 @@ __ `W3C ISO 8601`_
Besides you can show ``modified`` in the templates, feed entries in feed readers will be updated automatically Besides you can show ``modified`` in the templates, feed entries in feed readers will be updated automatically
when you set ``modified`` to the current date after you modified your article. when you set ``modified`` to the current date after you modified your article.
``authors`` is a comma-separated list of article authors. If there's only one author you
can use ``author`` field.
If you do not explicitly specify summary metadata for a given post, the If you do not explicitly specify summary metadata for a given post, the
``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the ``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the
beginning of an article are used as the summary. beginning of an article are used as the summary.

View file

@ -74,11 +74,17 @@ class Content(object):
#default template if it's not defined in page #default template if it's not defined in page
self.template = self._get_template() self.template = self._get_template()
# default author to the one in settings if not defined # First, read the authors from "authors", if not, fallback to "author"
# and if not use the settings defined one, if any.
if not hasattr(self, 'author'): if not hasattr(self, 'author'):
if 'AUTHOR' in settings: if hasattr(self, 'authors'):
self.author = self.authors[0]
elif 'AUTHOR' in settings:
self.author = Author(settings['AUTHOR'], settings) self.author = Author(settings['AUTHOR'], settings)
if not hasattr(self, 'authors') and hasattr(self, 'author'):
self.authors = [self.author]
# XXX Split all the following code into pieces, there is too much here. # XXX Split all the following code into pieces, there is too much here.
# manage languages # manage languages

View file

@ -444,9 +444,9 @@ class ArticlesGenerator(Generator):
for tag in article.tags: for tag in article.tags:
self.tags[tag].append(article) self.tags[tag].append(article)
# ignore blank authors as well as undefined # ignore blank authors as well as undefined
if hasattr(article, 'author') and article.author.name != '': for author in getattr(article, 'authors', []):
self.authors[article.author].append(article) if author.name != '':
self.authors[author].append(article)
# sort the articles by date # sort the articles by date
self.articles.sort(key=attrgetter('date'), reverse=True) self.articles.sort(key=attrgetter('date'), reverse=True)
self.dates = list(self.articles) self.dates = list(self.articles)

View file

@ -46,6 +46,7 @@ METADATA_PROCESSORS = {
'status': lambda x, y: x.strip(), 'status': lambda x, y: x.strip(),
'category': Category, 'category': Category,
'author': Author, 'author': Author,
'authors': lambda x, y: [Author(author, y) for author in x],
} }
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -144,6 +145,9 @@ class RstReader(BaseReader):
value = render_node_to_html(document, body_elem) value = render_node_to_html(document, body_elem)
else: else:
value = body_elem.astext() value = body_elem.astext()
elif element.tagname == 'authors': # author list
name = element.tagname
value = [element.astext() for element in element.children]
else: # standard fields (e.g. address) else: # standard fields (e.g. address)
name = element.tagname name = element.tagname
value = element.astext() value = element.astext()

View file

@ -0,0 +1,6 @@
This is an article with multiple authors!
#########################################
:date: 2014-02-09 02:20
:modified: 2014-02-09 02:20
:authors: First Author, Second Author

View file

@ -346,6 +346,17 @@ class TestPage(unittest.TestCase):
'<a href="http://notmyidea.org/article-spaces.html">link</a>' '<a href="http://notmyidea.org/article-spaces.html">link</a>'
) )
def test_multiple_authors(self):
"""Test article with multiple authors."""
args = self.page_kwargs.copy()
content = Page(**args)
assert content.authors == [content.author]
args['metadata'].pop('author')
args['metadata']['authors'] = ['First Author', 'Second Author']
content = Page(**args)
assert content.authors
assert content.author == content.authors[0]
class TestArticle(TestPage): class TestArticle(TestPage):
def test_template(self): def test_template(self):

View file

@ -93,6 +93,7 @@ class TestArticlesGenerator(unittest.TestCase):
['This is a super article !', 'published', 'Default', 'article'], ['This is a super article !', 'published', 'Default', 'article'],
['This is an article with category !', 'published', 'yeah', ['This is an article with category !', 'published', 'yeah',
'article'], 'article'],
['This is an article with multiple authors!', 'published', 'Default', 'article'],
['This is an article without category !', 'published', 'Default', ['This is an article without category !', 'published', 'Default',
'article'], 'article'],
['This is an article without category !', 'published', ['This is an article without category !', 'published',
@ -257,6 +258,16 @@ class TestArticlesGenerator(unittest.TestCase):
settings, settings,
blog=True, dates=dates) blog=True, dates=dates)
def test_generate_authors(self):
"""Check authors generation."""
authors = [author.name for author, _ in self.generator.authors]
authors_expected = sorted(['Alexis Métaireau', 'First Author', 'Second Author'])
self.assertEqual(sorted(authors), authors_expected)
# test for slug
authors = [author.slug for author, _ in self.generator.authors]
authors_expected = ['alexis-metaireau', 'first-author', 'second-author']
self.assertEqual(sorted(authors), sorted(authors_expected))
class TestPageGenerator(unittest.TestCase): class TestPageGenerator(unittest.TestCase):
# Note: Every time you want to test for a new field; Make sure the test # Note: Every time you want to test for a new field; Make sure the test

View file

@ -10,7 +10,7 @@ from pelican.tests.support import (unittest, temporary_folder, mute,
from pelican.utils import slugify from pelican.utils import slugify
CUR_DIR = os.path.dirname(__file__) CUR_DIR = os.path.abspath(os.path.dirname(__file__))
WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml') WORDPRESS_XML_SAMPLE = os.path.join(CUR_DIR, 'content', 'wordpressexport.xml')
WORDPRESS_ENCODED_CONTENT_SAMPLE = os.path.join(CUR_DIR, WORDPRESS_ENCODED_CONTENT_SAMPLE = os.path.join(CUR_DIR,
'content', 'content',
@ -283,7 +283,7 @@ class TestWordpressXMLAttachements(unittest.TestCase):
def test_download_attachments(self): def test_download_attachments(self):
real_file = os.path.join(CUR_DIR, 'content/article.rst') real_file = os.path.join(CUR_DIR, 'content/article.rst')
good_url = 'file://' + real_file good_url = 'file://' + real_file
bad_url = 'http://www.notarealsite.notarealdomain/not_a_file.txt' bad_url = 'http://localhost:1/not_a_file.txt'
silent_da = mute()(download_attachments) silent_da = mute()(download_attachments)
with temporary_folder() as temp: with temporary_folder() as temp:
#locations = download_attachments(temp, [good_url, bad_url]) #locations = download_attachments(temp, [good_url, bad_url])

View file

@ -2,11 +2,11 @@
from __future__ import unicode_literals, print_function from __future__ import unicode_literals, print_function
import os import os
from filecmp import dircmp
from tempfile import mkdtemp from tempfile import mkdtemp
from shutil import rmtree from shutil import rmtree
import locale import locale
import logging import logging
import subprocess
from pelican import Pelican from pelican import Pelican
from pelican.settings import read_settings from pelican.settings import read_settings
@ -64,6 +64,13 @@ class TestPelican(LoggedTestCase):
self.assertEqual(diff['right_only'], [], msg=msg) self.assertEqual(diff['right_only'], [], msg=msg)
self.assertEqual(diff['diff_files'], [], msg=msg) self.assertEqual(diff['diff_files'], [], msg=msg)
def assertDirsEqual(self, left_path, right_path):
out, err = subprocess.Popen(
['git', 'diff', '--no-ext-diff', '--exit-code', '-w', left_path, right_path], env={'PAGER': ''},
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
assert not out, out
assert not err, err
def test_basic_generation_works(self): def test_basic_generation_works(self):
# when running pelican without settings, it should pick up the default # when running pelican without settings, it should pick up the default
# ones and generate correct output without raising any exception # ones and generate correct output without raising any exception
@ -74,8 +81,7 @@ class TestPelican(LoggedTestCase):
}) })
pelican = Pelican(settings=settings) pelican = Pelican(settings=settings)
mute(True)(pelican.run)() mute(True)(pelican.run)()
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'basic')) self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, 'basic'))
self.assertFilesEqual(recursiveDiff(dcmp))
self.assertLogCountEqual( self.assertLogCountEqual(
count=4, count=4,
msg="Unable to find.*skipping url replacement", msg="Unable to find.*skipping url replacement",
@ -90,8 +96,7 @@ class TestPelican(LoggedTestCase):
}) })
pelican = Pelican(settings=settings) pelican = Pelican(settings=settings)
mute(True)(pelican.run)() mute(True)(pelican.run)()
dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'custom')) self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, 'custom'))
self.assertFilesEqual(recursiveDiff(dcmp))
def test_theme_static_paths_copy(self): def test_theme_static_paths_copy(self):
# the same thing with a specified set of settings should work # the same thing with a specified set of settings should work

View file

@ -360,6 +360,15 @@ class HTMLReaderTest(ReaderTest):
for key, value in expected.items(): for key, value in expected.items():
self.assertEqual(value, page.metadata[key], key) self.assertEqual(value, page.metadata[key], key)
def test_article_with_multiple_authors(self):
page = self.read_file(path='article_with_multiple_authors.rst')
expected = {
'authors': ['First Author', 'Second Author']
}
for key, value in expected.items():
self.assertEqual(value, page.metadata[key], key)
def test_article_with_metadata_and_contents_attrib(self): def test_article_with_metadata_and_contents_attrib(self):
page = self.read_file(path='article_with_metadata_and_contents.html') page = self.read_file(path='article_with_metadata_and_contents.html')
expected = { expected = {

View file

@ -9,9 +9,11 @@
</abbr> </abbr>
{% endif %} {% endif %}
{% if article.author %} {% if article.authors %}
<address class="vcard author"> <address class="vcard author">
By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a> By {% for author in article.authors %}
<a class="url fn" href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
{% endfor %}
</address> </address>
{% endif %} {% endif %}
<p>In <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>. {% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a>{% endif %}</p> <p>In <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>. {% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a>{% endif %}</p>

View file

@ -6,7 +6,6 @@
<section id="content" class="body"> <section id="content" class="body">
<h1>Authors on {{ SITENAME }}</h1> <h1>Authors on {{ SITENAME }}</h1>
{%- for author, articles in authors|sort %} {%- for author, articles in authors|sort %}
<li><a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a> ({{ articles|count }})</li> <li><a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a> ({{ articles|count }})</li>
{% endfor %} {% endfor %}

View file

@ -33,9 +33,11 @@
{{ article.locale_modified }} {{ article.locale_modified }}
</abbr> </abbr>
{% endif %} {% endif %}
{% if article.author %} {% if article.authors %}
<address class="vcard author"> <address class="vcard author">
By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a> By {% for author in article.authors %}
<a class="url fn" href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
{% endfor %}
</address> </address>
{% endif %} {% endif %}
</footer><!-- /.post-info --> </footer><!-- /.post-info -->

View file

@ -11,7 +11,11 @@
<header> <h2 class="entry-title"><a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title|striptags }}">{{ article.title }}</a></h2> </header> <header> <h2 class="entry-title"><a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title|striptags }}">{{ article.title }}</a></h2> </header>
<footer class="post-info"> <footer class="post-info">
<abbr class="published" title="{{ article.date.isoformat() }}"> {{ article.locale_date }} </abbr> <abbr class="published" title="{{ article.date.isoformat() }}"> {{ article.locale_date }} </abbr>
{% if article.author %}<address class="vcard author">By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a></address>{% endif %} <address class="vcard author">By
{% for author in article.authors %}
<a class="url fn" href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
{% endfor %}
</address>
</footer><!-- /.post-info --> </footer><!-- /.post-info -->
<div class="entry-content"> {{ article.summary }} </div><!-- /.entry-content --> <div class="entry-content"> {{ article.summary }} </div><!-- /.entry-content -->
</article></li> </article></li>