mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Merge pull request #1247 from paylogic/multiple-authors
Multiple authors implementation for #956
This commit is contained in:
commit
826ff4df50
16 changed files with 122 additions and 59 deletions
|
|
@ -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)
|
||||||
==================
|
==================
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
6
pelican/tests/content/article_with_multiple_authors.rst
Normal file
6
pelican/tests/content/article_with_multiple_authors.rst
Normal 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
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue