From 2c25e488c4bc1bd72c592354967497b93ce57e49 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 5 Jul 2013 01:08:45 +0200 Subject: [PATCH] multiple authors implemented --- docs/changelog.rst | 1 + docs/getting_started.rst | 15 ++++--- pelican/contents.py | 10 ++++- pelican/generators.py | 8 ++-- pelican/readers.py | 4 ++ .../content/article_with_multiple_authors.rst | 6 +++ pelican/tests/test_contents.py | 11 +++++ pelican/tests/test_generators.py | 11 +++++ pelican/tests/test_importer.py | 28 ++++++------ pelican/tests/test_pelican.py | 15 ++++--- pelican/tests/test_readers.py | 9 ++++ .../notmyidea/templates/article_infos.html | 6 ++- .../themes/notmyidea/templates/authors.html | 1 - pelican/themes/simple/templates/article.html | 6 ++- pelican/themes/simple/templates/index.html | 6 ++- pelican/tools/pelican_import.py | 44 +++++++++---------- 16 files changed, 122 insertions(+), 59 deletions(-) create mode 100644 pelican/tests/content/article_with_multiple_authors.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c8db17f..68348da5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ Next release * 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. * 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) ================== diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 53b466a0..6655d8d6 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -311,7 +311,7 @@ this metadata in text files via the following syntax (give your file the :tags: thats, awesome :category: yeah :slug: my-super-post - :author: Alexis Metaireau + :authors: Alexis Metaireau, Conan Doyle :summary: Short version for index and feeds Pelican implements an extension to reStructuredText to enable support for the @@ -331,7 +331,7 @@ pattern:: Category: Python Tags: pelican, publishing Slug: my-super-post - Author: Alexis Metaireau + Authors: Alexis Metaireau, Conan Doyle Summary: Short version for index and feeds This is the content of my super blog post. @@ -351,7 +351,7 @@ interprets the HTML in a very straightforward manner, reading metadata from - + @@ -380,6 +380,9 @@ __ `W3C ISO 8601`_ 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. +``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 ``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the beginning of an article are used as the summary. @@ -587,12 +590,12 @@ classprefix string String to prepend to token class names hl_lines numbers List of lines to be highlighted. lineanchors string Wrap each line in an anchor using this string and -linenumber. -linenos string If present or set to "table" output line +linenos string If present or set to "table" output line numbers in a table, if set to "inline" output them inline. "none" means - do not output the line numbers for this + do not output the line numbers for this table. -linenospecial number If set every nth line will be given the +linenospecial number If set every nth line will be given the 'special' css class. linenostart number Line number for the first line. linenostep number Print every nth line number. diff --git a/pelican/contents.py b/pelican/contents.py index 51ef8a22..69b7fa69 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -74,11 +74,17 @@ class Content(object): #default template if it's not defined in page 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 'AUTHOR' in settings: + if hasattr(self, 'authors'): + self.author = self.authors[0] + elif 'AUTHOR' in 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. # manage languages diff --git a/pelican/generators.py b/pelican/generators.py index 8c5f446b..d1034eb0 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -434,7 +434,7 @@ class ArticlesGenerator(Generator): self.articles, self.translations = process_translations(all_articles) - signals.article_generator_pretaxonomy.send(self) + signals.article_generator_pretaxonomy.send(self) for article in self.articles: # only main articles are listed in categories and tags @@ -444,9 +444,9 @@ class ArticlesGenerator(Generator): for tag in article.tags: self.tags[tag].append(article) # ignore blank authors as well as undefined - if hasattr(article, 'author') and article.author.name != '': - self.authors[article.author].append(article) - + for author in getattr(article, 'authors', []): + if author.name != '': + self.authors[author].append(article) # sort the articles by date self.articles.sort(key=attrgetter('date'), reverse=True) self.dates = list(self.articles) diff --git a/pelican/readers.py b/pelican/readers.py index 7cb59f5a..1e00aefa 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -46,6 +46,7 @@ METADATA_PROCESSORS = { 'status': lambda x, y: x.strip(), 'category': Category, 'author': Author, + 'authors': lambda x, y: [Author(author, y) for author in x], } logger = logging.getLogger(__name__) @@ -144,6 +145,9 @@ class RstReader(BaseReader): value = render_node_to_html(document, body_elem) else: 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) name = element.tagname value = element.astext() diff --git a/pelican/tests/content/article_with_multiple_authors.rst b/pelican/tests/content/article_with_multiple_authors.rst new file mode 100644 index 00000000..04af017c --- /dev/null +++ b/pelican/tests/content/article_with_multiple_authors.rst @@ -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 diff --git a/pelican/tests/test_contents.py b/pelican/tests/test_contents.py index 448f73eb..f831f061 100644 --- a/pelican/tests/test_contents.py +++ b/pelican/tests/test_contents.py @@ -346,6 +346,17 @@ class TestPage(unittest.TestCase): 'link' ) + 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): def test_template(self): diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index eddaa504..6f13aeb6 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -93,6 +93,7 @@ class TestArticlesGenerator(unittest.TestCase): ['This is a super article !', 'published', 'Default', 'article'], ['This is an article with category !', 'published', 'yeah', 'article'], + ['This is an article with multiple authors!', 'published', 'Default', 'article'], ['This is an article without category !', 'published', 'Default', 'article'], ['This is an article without category !', 'published', @@ -257,6 +258,16 @@ class TestArticlesGenerator(unittest.TestCase): settings, 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): # Note: Every time you want to test for a new field; Make sure the test diff --git a/pelican/tests/test_importer.py b/pelican/tests/test_importer.py index 9d6f911d..8412c75b 100644 --- a/pelican/tests/test_importer.py +++ b/pelican/tests/test_importer.py @@ -10,7 +10,7 @@ from pelican.tests.support import (unittest, temporary_folder, mute, 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_ENCODED_CONTENT_SAMPLE = os.path.join(CUR_DIR, 'content', @@ -75,7 +75,7 @@ class TestWordpressXmlImporter(unittest.TestCase): out_name = fnames[index] self.assertTrue(out_name.endswith(filename)) index += 1 - + def test_unless_custom_post_all_items_should_be_pages_or_posts(self): self.assertTrue(self.posts) pages_data = [] @@ -85,7 +85,7 @@ class TestWordpressXmlImporter(unittest.TestCase): else: pages_data.append((title, fname)) self.assertEqual(0, len(pages_data)) - + def test_recognise_custom_post_type(self): self.assertTrue(self.custposts) cust_data = [] @@ -98,7 +98,7 @@ class TestWordpressXmlImporter(unittest.TestCase): self.assertEqual(('A custom post in category 4', 'custom1'), cust_data[0]) self.assertEqual(('A custom post in category 5', 'custom1'), cust_data[1]) self.assertEqual(('A 2nd custom post type also in category 5', 'custom2'), cust_data[2]) - + def test_custom_posts_put_in_own_dir(self): silent_f2p = mute(True)(fields2pelican) test_posts = [] @@ -130,7 +130,7 @@ class TestWordpressXmlImporter(unittest.TestCase): else: test_posts.append(post) with temporary_folder() as temp: - fnames = list(silent_f2p(test_posts, 'markdown', temp, + fnames = list(silent_f2p(test_posts, 'markdown', temp, wp_custpost=True, dircat=True)) index = 0 for post in test_posts: @@ -152,7 +152,7 @@ class TestWordpressXmlImporter(unittest.TestCase): if post[7] == 'page': test_posts.append(post) with temporary_folder() as temp: - fnames = list(silent_f2p(test_posts, 'markdown', temp, + fnames = list(silent_f2p(test_posts, 'markdown', temp, wp_custpost=True, dirpage=False)) index = 0 for post in test_posts: @@ -161,8 +161,8 @@ class TestWordpressXmlImporter(unittest.TestCase): filename = os.path.join('pages', name) out_name = fnames[index] self.assertFalse(out_name.endswith(filename)) - - + + def test_can_toggle_raw_html_code_parsing(self): def r(f): with open(f) as infile: @@ -247,9 +247,9 @@ class TestBuildHeader(unittest.TestCase): '##############################################\n\n') def test_galleries_added_to_header(self): - header = build_header('test', None, None, None, None, + header = build_header('test', None, None, None, None, None, ['output/test1', 'output/test2']) - self.assertEqual(header, 'test\n####\n' + ':attachments: output/test1, ' + self.assertEqual(header, 'test\n####\n' + ':attachments: output/test1, ' + 'output/test2\n\n') def test_galleries_added_to_markdown_header(self): @@ -258,11 +258,11 @@ class TestBuildHeader(unittest.TestCase): self.assertEqual(header, 'Title: test\n' + 'Attachments: output/test1, ' + 'output/test2\n\n') -@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') -class TestWordpressXMLAttachements(unittest.TestCase): +@unittest.skipUnless(BeautifulSoup, 'Needs BeautifulSoup module') +class TestWordpressXMLAttachements(unittest.TestCase): def setUp(self): self.attachments = get_attachments(WORDPRESS_XML_SAMPLE) - + def test_recognise_attachments(self): self.assertTrue(self.attachments) self.assertTrue(len(self.attachments.keys()) == 3) @@ -283,7 +283,7 @@ class TestWordpressXMLAttachements(unittest.TestCase): def test_download_attachments(self): real_file = os.path.join(CUR_DIR, 'content/article.rst') 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) with temporary_folder() as temp: #locations = download_attachments(temp, [good_url, bad_url]) diff --git a/pelican/tests/test_pelican.py b/pelican/tests/test_pelican.py index 8cccc918..21a77e6b 100644 --- a/pelican/tests/test_pelican.py +++ b/pelican/tests/test_pelican.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals, print_function import os -from filecmp import dircmp from tempfile import mkdtemp from shutil import rmtree import locale import logging +import subprocess from pelican import Pelican from pelican.settings import read_settings @@ -64,6 +64,13 @@ class TestPelican(LoggedTestCase): self.assertEqual(diff['right_only'], [], 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): # when running pelican without settings, it should pick up the default # ones and generate correct output without raising any exception @@ -74,8 +81,7 @@ class TestPelican(LoggedTestCase): }) pelican = Pelican(settings=settings) mute(True)(pelican.run)() - dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'basic')) - self.assertFilesEqual(recursiveDiff(dcmp)) + self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, 'basic')) self.assertLogCountEqual( count=4, msg="Unable to find.*skipping url replacement", @@ -90,8 +96,7 @@ class TestPelican(LoggedTestCase): }) pelican = Pelican(settings=settings) mute(True)(pelican.run)() - dcmp = dircmp(self.temp_path, os.path.join(OUTPUT_PATH, 'custom')) - self.assertFilesEqual(recursiveDiff(dcmp)) + self.assertDirsEqual(self.temp_path, os.path.join(OUTPUT_PATH, 'custom')) def test_theme_static_paths_copy(self): # the same thing with a specified set of settings should work diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py index e827ed3b..acb268fb 100644 --- a/pelican/tests/test_readers.py +++ b/pelican/tests/test_readers.py @@ -360,6 +360,15 @@ class HTMLReaderTest(ReaderTest): for key, value in expected.items(): 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): page = self.read_file(path='article_with_metadata_and_contents.html') expected = { diff --git a/pelican/themes/notmyidea/templates/article_infos.html b/pelican/themes/notmyidea/templates/article_infos.html index 54c88004..718967e0 100644 --- a/pelican/themes/notmyidea/templates/article_infos.html +++ b/pelican/themes/notmyidea/templates/article_infos.html @@ -9,9 +9,11 @@ {% endif %} - {% if article.author %} + {% if article.authors %}
- By {{ article.author }} + By {% for author in article.authors %} + {{ author }} + {% endfor %}
{% endif %}

In {{ article.category }}. {% if PDF_PROCESSOR %}get the pdf{% endif %}

diff --git a/pelican/themes/notmyidea/templates/authors.html b/pelican/themes/notmyidea/templates/authors.html index a203422b..b145902e 100644 --- a/pelican/themes/notmyidea/templates/authors.html +++ b/pelican/themes/notmyidea/templates/authors.html @@ -6,7 +6,6 @@

Authors on {{ SITENAME }}

- {%- for author, articles in authors|sort %}
  • {{ author }} ({{ articles|count }})
  • {% endfor %} diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index b5368d9f..e81261c5 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -33,9 +33,11 @@ {{ article.locale_modified }} {% endif %} - {% if article.author %} + {% if article.authors %}
    - By {{ article.author }} + By {% for author in article.authors %} + {{ author }} + {% endfor %}
    {% endif %} diff --git a/pelican/themes/simple/templates/index.html b/pelican/themes/simple/templates/index.html index 5bb94dfc..20104db9 100644 --- a/pelican/themes/simple/templates/index.html +++ b/pelican/themes/simple/templates/index.html @@ -11,7 +11,11 @@

    {{ article.title }}

    {{ article.locale_date }} - {% if article.author %}
    By {{ article.author }}
    {% endif %} +
    By + {% for author in article.authors %} + {{ author }} + {% endfor %} +
    {{ article.summary }}
    diff --git a/pelican/tools/pelican_import.py b/pelican/tools/pelican_import.py index b04906c0..d6b57c47 100755 --- a/pelican/tools/pelican_import.py +++ b/pelican/tools/pelican_import.py @@ -124,7 +124,7 @@ def get_filename(filename, post_id): def wp2fields(xml, wp_custpost=False): """Opens a wordpress XML file, and yield Pelican fields""" - + items = get_items(xml) for item in items: @@ -140,7 +140,7 @@ def wp2fields(xml, wp_custpost=False): filename = item.find('post_name').string post_id = item.find('post_id').string filename = get_filename(filename, post_id) - + content = item.find('encoded').string raw_date = item.find('post_date').string date_object = time.strptime(raw_date, "%Y-%m-%d %H:%M:%S") @@ -161,7 +161,7 @@ def wp2fields(xml, wp_custpost=False): pass # Old behaviour was to name everything not a page as an article. # Theoretically all attachments have status == inherit so - # no attachments should be here. But this statement is to + # no attachments should be here. But this statement is to # maintain existing behaviour in case that doesn't hold true. elif post_type == 'attachment': pass @@ -469,7 +469,7 @@ def build_header(title, date, author, categories, tags, slug, attachments=None): header += '\n' return header -def build_markdown_header(title, date, author, categories, tags, slug, +def build_markdown_header(title, date, author, categories, tags, slug, attachments=None): """Build a header from a list of fields""" header = 'Title: %s\n' % title @@ -494,8 +494,8 @@ def get_ext(out_markup, in_markup='html'): else: ext = '.rst' return ext - -def get_out_filename(output_path, filename, ext, kind, + +def get_out_filename(output_path, filename, ext, kind, dirpage, dircat, categories, wp_custpost): filename = os.path.basename(filename) @@ -516,7 +516,7 @@ def get_out_filename(output_path, filename, ext, kind, os.mkdir(pages_dir) out_filename = os.path.join(pages_dir, filename+ext) elif not dirpage and kind == 'page': - pass + pass # option to put wp custom post types in directories with post type # names. Custom post types can also have categories so option to # create subdirectories with category names @@ -530,7 +530,7 @@ def get_out_filename(output_path, filename, ext, kind, catname = slugify(categories[0]) else: catname = '' - out_filename = os.path.join(output_path, typename, + out_filename = os.path.join(output_path, typename, catname, filename+ext) if not os.path.isdir(os.path.join(output_path, typename, catname)): os.makedirs(os.path.join(output_path, typename, catname)) @@ -544,20 +544,20 @@ def get_out_filename(output_path, filename, ext, kind, return out_filename def get_attachments(xml): - """returns a dictionary of posts that have attachments with a list + """returns a dictionary of posts that have attachments with a list of the attachment_urls """ items = get_items(xml) names = {} attachments = [] - + for item in items: kind = item.find('post_type').string filename = item.find('post_name').string post_id = item.find('post_id').string - + if kind == 'attachment': - attachments.append((item.find('post_parent').string, + attachments.append((item.find('post_parent').string, item.find('attachment_url').string)) else: filename = get_filename(filename, post_id) @@ -569,7 +569,7 @@ def get_attachments(xml): except KeyError: #attachment's parent is not a valid post parent_name = None - + try: attachedposts[parent_name].append(url) except KeyError: @@ -578,13 +578,13 @@ def get_attachments(xml): return attachedposts def download_attachments(output_path, urls): - """Downloads wordpress attachments and returns a list of paths to - attachments that can be associated with a post (relative path to output + """Downloads wordpress attachments and returns a list of paths to + attachments that can be associated with a post (relative path to output directory). Files that fail to download, will not be added to posts""" locations = [] for url in urls: path = urlparse(url).path - #teardown path and rebuild to negate any errors with + #teardown path and rebuild to negate any errors with #os.path.join and leading /'s path = path.split('/') filename = path.pop(-1) @@ -625,16 +625,16 @@ def fields2pelican(fields, out_markup, output_path, attached_files = download_attachments(output_path, urls) except KeyError: attached_files = None - else: + else: attached_files = None ext = get_ext(out_markup, in_markup) if ext == '.md': - header = build_markdown_header(title, date, author, categories, + header = build_markdown_header(title, date, author, categories, tags, slug, attached_files) else: out_markup = "rst" - header = build_header(title, date, author, categories, + header = build_header(title, date, author, categories, tags, slug, attached_files) out_filename = get_out_filename(output_path, filename, ext, @@ -690,7 +690,7 @@ def fields2pelican(fields, out_markup, output_path, print("downloading attachments that don't have a parent post") urls = attachments[None] orphan_galleries = download_attachments(output_path, urls) - + def main(): parser = argparse.ArgumentParser( description="Transform feed, WordPress, Tumblr, Dotclear, or Posterous " @@ -723,7 +723,7 @@ def main(): parser.add_argument('--strip-raw', action='store_true', dest='strip_raw', help="Strip raw HTML code that can't be converted to " "markup such as flash embeds or iframes (wordpress import only)") - parser.add_argument('--wp-custpost', action='store_true', + parser.add_argument('--wp-custpost', action='store_true', dest='wp_custpost', help='Put wordpress custom post types in directories. If used with ' '--dir-cat option directories will be created as ' @@ -775,7 +775,7 @@ def main(): if args.wp_attach and input_type != 'wordpress': error = "You must be importing a wordpress xml to use the --wp-attach option" exit(error) - + if input_type == 'wordpress': fields = wp2fields(args.input, args.wp_custpost or False) elif input_type == 'dotclear':