瀏覽代碼

Supports sub-parts, which are articles nested below other articles.

Use sub-parts to break a very long article in parts, without polluting
the timeline with lots of small articles. Sub-parts are removed from
timelines and categories, but remain in tag and author pages.
Joaquim Baptista 9 年之前
父節點
當前提交
c69cf4107d

+ 69 - 0
sub_parts/README.md

@@ -0,0 +1,69 @@
+# Sub-parts
+
+Use sub-parts to break a very long article in parts, without polluting the timeline with lots of small articles. Sub-parts are removed from timelines and categories, but remain in tag and author pages.
+
+## How to use
+
+Article sub-parts have a compound slug with the slug of the parent, two hyphens (`--`), and some identifier. For example, if an article has the slug `karate`, then an article with the slug `karate--medals` would be a sub-part.
+
+This convention is very convenient if the slug is derived from the filename. For example, define the filename metadata as follows:
+
+		FILENAME_METADATA = '(?P<slug>(?P<date>\d{4}-\d{2}-\d{2})-[^.]+)
+
+Then, it is enough to name the files correctly. In the following example, the first Markdown article has two sub-parts:
+
+		./content/blog/2015-03-21-karate.md
+		./content/blog/2015-03-21-karate--attendees.md
+		./content/blog/2015-03-21-karate--medals.md
+
+The plugin provides the following variables to your templates, and modifies the titles of sub-part articles:
+
+`article.subparts`
+:	For a parent article with sub-parts, the list of sub-part articles.
+
+`article.subpart_of`
+:	For a sub-part article, the parent article.
+
+`article.subtitle`
+:	The original title of the sub-part article.
+
+`article.title`
+:	Compound title with the a comma and the title of the parent.
+
+`article.subphotos`
+:	For parent articles with sub-parts that have a gallery generated by the plug-in Photos, the total number of gallery photos in all sub-parts.
+
+For example, add the following to the template `article.html`:
+
+		{% if article.subparts %}
+		<h2>Parts</h2>
+		<ul>
+			{% for part in article.subparts %}
+				<li><a href='{{ SITEURL }}/{{ part.url }}'>{{ part.subtitle }}</a>
+			{% endfor %}
+		</ul>
+		{% endif %}
+		{% if article.subpart_of %}
+		<h2>Parts</h2>
+		<p>This article is part of <a href='{{ SITEURL }}/{{ article.subpart_of.url }}'>{{ article.subpart_of.title }}</a>:</p>
+		<ul>
+			{% for part in article.subpart_of.subparts %}
+				<li><a href='{{ SITEURL }}/{{ part.url }}'>{{ part.subtitle }}</a>
+			{% endfor %}
+		</ul>
+		{% endif %}
+
+
+## Known use cases
+
+<pxquim.pt> uses sub-parts and the plug-in Pictures to publish photo galleries with thousands of photos. Sub-parts break the photo galleries into manageable chunks, possibly with different tags and authors.
+
+<pxquim.com> uses sub-parts to cover conferences, where it makes sense to have a sub-part for each speaker.
+
+## What is the difference between Sub-part and Series?
+
+Series
+:	Connects separate articles, but never removes articles from timelines. Series is adequate to relate articles spread over time. For example, a 10-part article written over several months, or a large festival with several independent performances.
+
+Sub-part
+:	Subordinates articles to each other, hiding sub-articles from timelines. Sub-parts is adequate to relate articles clustered together in time, where the sub-articles need the context of their parent article to be fully understood. For example, a 20-part photo gallery of a Karate competition, or coverage of a conference.

+ 1 - 0
sub_parts/__init__.py

@@ -0,0 +1 @@
+from .sub_parts import *

+ 66 - 0
sub_parts/sub_parts.py

@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from pelican import signals
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def patch_subparts(generator):
+    generator.subparts = []
+    slugs = {}
+    for article in generator.articles:
+        slugs[article.slug] = article
+        if '--' in article.slug:
+            generator.subparts.append(article)
+    for article in generator.subparts:
+        logger.info('sub_part: Detected %s', article.slug)
+        (pslug, _) = article.slug.rsplit('--', 1)
+        if pslug in slugs:
+            parent = slugs[pslug]
+            if not hasattr(parent, 'subparts'):
+                parent.subparts = []
+            parent.subparts.append(article)
+            article.subpart_of = parent
+            article.subtitle = article.title
+            article.title = article.title + ", " + parent.title
+            generator.dates.remove(article)
+            generator.articles.remove(article)
+            if article.category:
+                for cat, arts in generator.categories:
+                    if cat.name == article.category.name:
+                        arts.remove(article)
+                        break
+                else:
+                    logger.error(
+                        'sub_part: Cannot remove sub-part from category %s',
+                        article.category)
+            if (hasattr(article, 'subphotos') or
+                    hasattr(article, 'photos_gallery')):
+                parent.subphotos = (
+                    getattr(parent, 'subphotos',
+                            len(getattr(parent, 'photos_gallery', []))) +
+                    getattr(article, 'subphotos', 0) +
+                    len(getattr(article, 'photos_gallery', [])))
+        else:
+            logger.error('sub_part: No parent for %s', pslug)
+        generator._update_context(('articles', 'dates', 'subparts'))
+
+
+def write_subparts(generator, writer):
+    for article in generator.subparts:
+        signals.article_generator_write_article.send(generator,
+                                                     content=article)
+        writer.write_file(
+            article.save_as, generator.get_template(article.template),
+            generator.context, article=article, category=article.category,
+            override_output=hasattr(article, 'override_save_as'),
+            relative_urls=generator.settings['RELATIVE_URLS'])
+    if len(generator.subparts) > 0:
+        print('sub_part: processed {} sub-parts.'.format(
+            len(generator.subparts)))
+
+
+def register():
+    signals.article_generator_finalized.connect(patch_subparts)
+    signals.article_writer_finalized.connect(write_subparts)

+ 4 - 0
sub_parts/test_data/noparent.md

@@ -0,0 +1,4 @@
+title: No parent
+tags: atag
+
+Normal article.

+ 4 - 0
sub_parts/test_data/parent--implicit.md

@@ -0,0 +1,4 @@
+title: Implicit sub-article
+tags: atag
+
+Sub-article based on filename as implicit slug.

+ 5 - 0
sub_parts/test_data/parent-explicit.md

@@ -0,0 +1,5 @@
+title: Explicit sub-article
+tags: atag
+slug: parent--explicit
+
+Explicit sub-article, based on explicit slug.

+ 4 - 0
sub_parts/test_data/parent.md

@@ -0,0 +1,4 @@
+title: Parent
+tags: atag
+
+Parent article with two sub-articles.

+ 120 - 0
sub_parts/test_sub_parts.py

@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import os
+
+from pelican.generators import ArticlesGenerator
+from pelican.tests.support import unittest, get_settings
+import sub_parts
+
+CUR_DIR = os.path.dirname(__file__)
+
+
+class TestSubParts(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        settings = get_settings(filenames={})
+        settings['PATH'] = os.path.join(CUR_DIR, 'test_data')
+        settings['AUTHOR'] = 'Me'
+        settings['DEFAULT_DATE'] = (1970, 1, 1)
+        settings['DEFAULT_CATEGORY'] = 'Default'
+        settings['FILENAME_METADATA'] = '(?P<slug>[^.]+)'
+        settings['PLUGINS'] = [sub_parts]
+        settings['CACHE_CONTENT'] = False
+        cls.generator = ArticlesGenerator(
+            context=settings.copy(), settings=settings,
+            path=settings['PATH'], theme=settings['THEME'], output_path=None)
+        cls.generator.generate_context()
+        cls.all_articles = list(cls.generator.articles)
+        sub_parts.patch_subparts(cls.generator)
+
+    def test_all_articles(self):
+        self.assertEqual(
+            sorted(['noparent', 'parent',
+                    'parent--explicit', 'parent--implicit']),
+            sorted([a.slug for a in self.all_articles]))
+
+    def test_articles(self):
+        self.assertEqual(
+            sorted(['noparent', 'parent']),
+            sorted([a.slug for a in self.generator.articles]))
+
+    def test_dates(self):
+        self.assertEqual(
+            sorted(['noparent', 'parent']),
+            sorted([a.slug for a in self.generator.dates]))
+
+    def test_categories(self):
+        self.assertEqual(
+            sorted(['noparent', 'parent']),
+            sorted([a.slug for a in self.generator.categories[0][1]]))
+
+    def test_tags(self):
+        self.assertEqual(
+            sorted([a.slug for a in self.all_articles]),
+            sorted([a.slug for a in self.generator.tags['atag']]))
+
+    def test_authors(self):
+        self.assertEqual(
+            sorted([a.slug for a in self.all_articles]),
+            sorted([a.slug for a in self.generator.authors[0][1]]))
+
+    def test_subparts(self):
+        for a in self.all_articles:
+            if a.slug == 'parent':
+                self.assertTrue(hasattr(a, 'subparts'))
+                self.assertEqual(
+                    sorted(['parent--explicit', 'parent--implicit']),
+                    sorted([a.slug for a in a.subparts]))
+            else:
+                self.assertFalse(hasattr(a, 'subparts'))
+
+    def test_subpart_of(self):
+        for a in self.all_articles:
+            if '--' in a.slug:
+                self.assertTrue(hasattr(a, 'subpart_of'))
+                self.assertEqual('parent', a.subpart_of.slug)
+            else:
+                self.assertFalse(hasattr(a, 'subpart_of'))
+
+    def test_subtitle(self):
+        for a in self.all_articles:
+            if '--' in a.slug:
+                self.assertTrue(hasattr(a, 'subtitle'))
+                self.assertEqual(a.title,
+                                 a.subtitle + ', ' + a.subpart_of.title)
+
+
+class TestSubPartsPhotos(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        settings = get_settings(filenames={})
+        settings['PATH'] = os.path.join(CUR_DIR, 'test_data')
+        settings['AUTHOR'] = 'Me'
+        settings['DEFAULT_DATE'] = (1970, 1, 1)
+        settings['DEFAULT_CATEGORY'] = 'Default'
+        settings['FILENAME_METADATA'] = '(?P<slug>[^.]+)'
+        settings['PLUGINS'] = [sub_parts]
+        settings['CACHE_CONTENT'] = False
+        cls.generator = ArticlesGenerator(
+            context=settings.copy(), settings=settings,
+            path=settings['PATH'], theme=settings['THEME'], output_path=None)
+        cls.generator.generate_context()
+        cls.all_articles = list(cls.generator.articles)
+        for a in cls.all_articles:
+            a.photos_gallery = [('i.jpg', 'i.jpg', 'it.jpg', '', '')]
+        sub_parts.patch_subparts(cls.generator)
+
+    def test_subphotos(self):
+        for a in self.all_articles:
+            if a.slug == 'parent':
+                self.assertTrue(hasattr(a, 'subphotos'))
+                self.assertEqual(3, a.subphotos)
+            else:
+                self.assertFalse(hasattr(a, 'subphotos'))
+
+
+if __name__ == '__main__':
+    unittest.main()