Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
#Subcategory Plugin | ||
This plugin adds support for multiple categories per article, and for nested | ||
categories. It requires Pelican 3.8 or newer. | ||
|
||
##Multiple categories | ||
To indicate that an article belongs to multiple categories, use a | ||
comma-separated string: | ||
|
||
Category: foo, bar, bazz | ||
|
||
This will add the article to the categories `foo`, `bar` and `bazz`. | ||
|
||
###Templates | ||
Existing themes that use `article.category` will display only the first of | ||
these categories, `foo`. This plugin adds `article.categories` that you can | ||
loop over instead. To accomodate this plugin in a theme whether it is present | ||
or not, use: | ||
|
||
{% for cat in article.categories or [article.category] %} | ||
<a href="{{ SITEURL }}/{{ cat.url }}">{{ cat }}</a>{{ ', ' if not loop.last }} | ||
{% endfor %} | ||
|
||
##Nested categories | ||
(This is a reimplementation of the `subcategory` plugin.) | ||
|
||
To indicate that a category is a child of another category, use a | ||
slash-separated string: | ||
|
||
Category: foo/bar/bazz | ||
|
||
This will add the article to the categories `foo/bar/bazz`, `foo/bar` and | ||
`foo`. | ||
|
||
###Templates | ||
Existing themes that use `article.category` will display the full path to the | ||
most specific of these categories, `foo/bar/bazz`. For any category `cat`, this | ||
plugin adds `cat.shortname`, which in this case is `bazz`, `cat.parent`, which | ||
in this case is the category `foo/bar`, and `cat.ancestors`, which is a list of | ||
the category's ancestors, ending with the category itself. For instance, to | ||
also include a link to each of the ancestor categories on an article page, in | ||
case this plugin in present, use: | ||
|
||
{% for cat in article.category.ancestors or [article.category] %} | ||
<a href="{{ SITEURL }}/{{ cat.url }}">{{ cat.shortname or cat }}</a>{{ '/' if not loop.last }} | ||
{% endfor %} | ||
|
||
Likewise, `category.shortname`, `category.parent` and `category.ancestors` can | ||
also be used on the category template. | ||
|
||
###Slug | ||
The slug of a category is generated recursively by slugifying the shortname of | ||
the category and its ancestors, and preserving slashes: | ||
|
||
slug-of-(foo/bar/baz) := slug-of-foo/slug-of-bar/slug-of-baz | ||
|
||
###Category folders | ||
To specify categories using the directory structure, you can configure | ||
`PATH_METADATA` to extract the article path into the `category` metadata. The | ||
following settings would use the entire structure: | ||
|
||
PATH_METADATA = '(?P<category>.*)/.*' | ||
|
||
If you store all articles in a single `articles/` folder that you want to | ||
ignore for this purpose, use: | ||
|
||
PATH_METADATA = 'articles/(?P<category>.*)/.*' | ||
|
||
###Categories in templates | ||
The list `categories` of all pairs of categories with their corresponding | ||
articles, which is available in the context and can be used in templates (e.g. | ||
to make a menu of available categories), is ordered lexicographically, so | ||
categories always follow their parent: | ||
|
||
aba | ||
aba/dat | ||
abaala |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .more_categories import * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# -*- coding: utf-8 -*- | ||
""" | ||
Title: More Categories | ||
Description: adds support for multiple categories per article and nested | ||
categories | ||
Requirements: Pelican 3.8 or higher | ||
""" | ||
|
||
from pelican import signals | ||
from pelican.urlwrappers import URLWrapper | ||
from pelican.utils import (slugify, python_2_unicode_compatible) | ||
|
||
from collections import defaultdict | ||
from six import text_type | ||
|
||
class Category(URLWrapper): | ||
@property | ||
def _name(self): | ||
if self.parent: | ||
return self.parent._name + '/' + self.shortname | ||
return self.shortname | ||
|
||
@_name.setter | ||
def _name(self, val): | ||
if '/' in val: | ||
parentname, val = val.rsplit('/', 1) | ||
self.parent = self.__class__(parentname, self.settings) | ||
else: | ||
self.parent = None | ||
self.shortname = val.strip() | ||
|
||
@URLWrapper.name.setter | ||
def name(self, val): | ||
self._name = val | ||
|
||
@property | ||
def slug(self): | ||
if self._slug is None: | ||
substitutions = self.settings.get('SLUG_SUBSTITUTIONS', ()) | ||
substitutions += tuple(self.settings.get('CATEGORY_SUBSTITUTIONS', | ||
())) | ||
self._slug = slugify(self.shortname, substitutions) | ||
if self.parent: | ||
self._slug = self.parent.slug + '/' + self._slug | ||
return self._slug | ||
|
||
@property | ||
def ancestors(self): | ||
if self.parent: | ||
return self.parent.ancestors + [self] | ||
return [self] | ||
|
||
def as_dict(self): | ||
d = super(Category, self).as_dict() | ||
d['shortname'] = self.shortname | ||
return d | ||
|
||
|
||
def get_categories(generator, metadata): | ||
categories = text_type(metadata.get('category')).split(',') | ||
metadata['categories'] = [ | ||
Category(name, generator.settings) for name in categories] | ||
metadata['category'] = metadata['categories'][0] | ||
|
||
|
||
def create_categories(generator): | ||
generator.categories = [] | ||
cat_dct = defaultdict(list) | ||
for article in generator.articles: | ||
for cat in {a for c in article.categories for a in c.ancestors}: | ||
cat_dct[cat].append(article) | ||
|
||
generator.categories = sorted( | ||
list(cat_dct.items()), | ||
reverse=generator.settings.get('REVERSE_CATEGORY_ORDER') or False) | ||
generator._update_context(['categories']) | ||
|
||
|
||
def register(): | ||
signals.article_generator_context.connect(get_categories) | ||
signals.article_generator_finalized.connect(create_categories) |
6 changes: 6 additions & 0 deletions
6
more_categories/test_data/article_with_multiple_categories.rst
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
Article with multiple and nested categories | ||
=========================================== | ||
:date: 2018-11-04 | ||
:category: foo/bar, foo/baz | ||
|
||
This is an article with multiple categories |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
Article with no category | ||
======================== | ||
:date: 2018-11-04 | ||
|
||
This is an article with no category |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"""Unit tests for the more_categories plugin""" | ||
|
||
import os | ||
import unittest | ||
|
||
from . import more_categories | ||
from pelican.generators import ArticlesGenerator | ||
from pelican.tests.support import get_context, get_settings | ||
|
||
|
||
class TestArticlesGenerator(unittest.TestCase): | ||
|
||
@classmethod | ||
def setUpClass(cls): | ||
more_categories.register() | ||
settings = get_settings() | ||
settings['DEFAULT_CATEGORY'] = 'default' | ||
settings['CACHE_CONTENT'] = False | ||
settings['PLUGINS'] = more_categories | ||
context = get_context(settings) | ||
|
||
base_path = os.path.dirname(os.path.abspath(__file__)) | ||
test_data_path = os.path.join(base_path, 'test_data') | ||
cls.generator = ArticlesGenerator( | ||
context=context, settings=settings, | ||
path=test_data_path, theme=settings['THEME'], output_path=None) | ||
cls.generator.generate_context() | ||
|
||
def test_generate_categories(self): | ||
"""Test whether multiple categories are generated correctly, | ||
including ancestor categories""" | ||
|
||
cats_generated = [cat.name for cat, _ in self.generator.categories] | ||
cats_expected = ['default', 'foo', 'foo/bar', 'foo/baz',] | ||
self.assertEqual(sorted(cats_generated), sorted(cats_expected)) | ||
|
||
def test_assign_articles_to_categories(self): | ||
"""Test whether articles are correctly assigned to categories, | ||
including whether articles are not assigned multiple times to the same | ||
ancestor category""" | ||
|
||
for cat, articles in self.generator.categories: | ||
self.assertEqual(len(articles), 1) |