Skip to content

Commit

Permalink
add more_categories plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
oulenz committed Nov 6, 2018
1 parent 78b27f2 commit e0a2f31
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Readme.rst
Expand Up @@ -154,6 +154,8 @@ Math Render Gives pelican the ability to render mathematics

Mbox Reader Generate articles automatically via email, given a path to a Unix mbox

More Categories Multiple categories per article; nested categories (`foo/bar, foo/baz`)

Multi Neighbors Adds a list of newer articles and a list of older articles to every article's context.

Multi parts posts Allows you to write multi-part posts
Expand Down
76 changes: 76 additions & 0 deletions more_categories/README.md
@@ -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
1 change: 1 addition & 0 deletions more_categories/__init__.py
@@ -0,0 +1 @@
from .more_categories import *
81 changes: 81 additions & 0 deletions more_categories/more_categories.py
@@ -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)
@@ -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
5 changes: 5 additions & 0 deletions more_categories/test_data/article_with_no_category.rst
@@ -0,0 +1,5 @@
Article with no category
========================
:date: 2018-11-04

This is an article with no category
43 changes: 43 additions & 0 deletions more_categories/test_more_categories.py
@@ -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)

0 comments on commit e0a2f31

Please sign in to comment.