Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #1711 from getnikola/category_hierarchies
Allowing category hierarchies. (fixes #1520)
  • Loading branch information
felixfontein committed May 12, 2015
2 parents fa27ee5 + 45c0bca commit 85617e3
Show file tree
Hide file tree
Showing 19 changed files with 388 additions and 66 deletions.
2 changes: 2 additions & 0 deletions CHANGES.txt
Expand Up @@ -15,6 +15,8 @@ New in v7.4.1
Features
--------

* Allowing category hierarchies via new option CATEGORY_ALLOW_HIERARCHIES
(Issue #1520)
* Better handling of missing/unconfigured compilers (Issue #1704)
* New -r option for the link checker to check remote links (Issue #1684)
* Use static navbars in bootstrap3 and bootstrap themes
Expand Down
10 changes: 10 additions & 0 deletions nikola/conf.py.in
Expand Up @@ -250,6 +250,16 @@ HIDDEN_TAGS = ['mathjax']
# CATEGORY_PATH = "categories"
# CATEGORY_PREFIX = "cat_"

# If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in
# hierarchies. For a post, the whole path in the hierarchy must be specified,
# using a forward slash ('/') to separate paths. Use a backslash ('\') to escape
# a forward slash or a backslash (i.e. '\//\\' is a path specifying the
# subcategory called '\' of the top-level category called '/').
# CATEGORY_ALLOW_HIERARCHIES = False
# If CATEGORY_OUTPUT_FLAT_HIERARCHY is set to True, the output written to output
# contains only the name of the leaf category and not the whole path.
# CATEGORY_OUTPUT_FLAT_HIERARCHY = False

# If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain
# the posts themselves. If set to False, it will be just a list of links.
# CATEGORY_PAGES_ARE_INDEXES = False
Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/base-jinja/templates/index.tmpl
Expand Up @@ -11,6 +11,7 @@
{% endblock %}

{% block content %}
{% block content_header %}{% endblock %}
<div class="postindex">
{% for post in posts %}
<article class="h-entry post-{{ post.meta('type') }}">
Expand Down
8 changes: 8 additions & 0 deletions nikola/data/themes/base-jinja/templates/tag.tmpl
Expand Up @@ -20,6 +20,14 @@
{% if description %}
<p>{{ description }}</p>
{% endif %}
{% if subcategories %}
{{ messages('Subcategories:') }}
<ul>
{% for name, link in subcategories %}
<li><a href="{{ link }}">{{ name }}</a></li>
{% endfor %}
</ul>
{% endif %}
<div class="metadata">
{% if translations|length > 1 and generate_rss %}
{% for language in translations %}
Expand Down
11 changes: 11 additions & 0 deletions nikola/data/themes/base-jinja/templates/tagindex.tmpl
@@ -1,6 +1,17 @@
{# -*- coding: utf-8 -*- #}
{% extends 'index.tmpl' %}

{% block content_header %}
{% if subcategories %}
{{ messages('Subcategories:') }}
<ul>
{% for name, link in subcategories %}
<li><a href="{{ link }}">{{ name }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}

{% block extra_head %}
{{ super() }}
{% if translations|length > 1 and generate_atom %}
Expand Down
18 changes: 13 additions & 5 deletions nikola/data/themes/base-jinja/templates/tags.tmpl
Expand Up @@ -10,13 +10,21 @@
{% if items %}
<h2>{{ messages("Categories") }}</h2>
{% endif %}
<ul class="postlist">
{% for text, link in cat_items %}
{% if text and text not in hidden_categories %}
<li><a class="reference" href="{{ link }}">{{ text }}</a></li>
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
{% for i in range(indent_change_before) %}
<ul class="postlist">
{% endfor %}
<li><a class="reference" href="{{ link }}">{{ text }}</a>
{% if indent_change_after <= 0 %}
</li>
{% endif %}
{% for i in range(-indent_change_after) %}
</ul>
{% if i + 1 < indent_levels|length %}
</li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
{% if items %}
<h2>{{ messages("Tags") }}</h2>
{% endif %}
Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/base/messages/messages_de.py
Expand Up @@ -28,6 +28,7 @@
"Read more": "Weiterlesen",
"Skip to main content": "Springe zum Hauptinhalt",
"Source": "Source",
"Subcategories:": "Unterkategorien:",
"Tags and Categories": "Tags und Kategorien",
"Tags": "Tags",
"old posts, page %d": "Ältere Einträge, Seite %d",
Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/base/messages/messages_en.py
Expand Up @@ -28,6 +28,7 @@
"Read more": "Read more",
"Skip to main content": "Skip to main content",
"Source": "Source",
"Subcategories:": "Subcategories:",
"Tags and Categories": "Tags and Categories",
"Tags": "Tags",
"old posts, page %d": "old posts, page %d",
Expand Down
1 change: 1 addition & 0 deletions nikola/data/themes/base/templates/index.tmpl
Expand Up @@ -11,6 +11,7 @@
</%block>

<%block name="content">
<%block name="content_header"></%block>
<div class="postindex">
% for post in posts:
<article class="h-entry post-${post.meta('type')}">
Expand Down
8 changes: 8 additions & 0 deletions nikola/data/themes/base/templates/tag.tmpl
Expand Up @@ -20,6 +20,14 @@
%if description:
<p>${description}</p>
%endif
%if subcategories:
${messages('Subcategories:')}
<ul>
%for name, link in subcategories:
<li><a href="${link}">${name}</a></li>
%endfor
</ul>
%endif
<div class="metadata">
%if len(translations) > 1 and generate_rss:
%for language in translations:
Expand Down
11 changes: 11 additions & 0 deletions nikola/data/themes/base/templates/tagindex.tmpl
@@ -1,6 +1,17 @@
## -*- coding: utf-8 -*-
<%inherit file="index.tmpl"/>

<%block name="content_header">
%if subcategories:
${messages('Subcategories:')}
<ul>
%for name, link in subcategories:
<li><a href="${link}">${name}</a></li>
%endfor
</ul>
%endif
</%block>

<%block name="extra_head">
${parent.extra_head()}
%if len(translations) > 1 and generate_atom:
Expand Down
18 changes: 13 additions & 5 deletions nikola/data/themes/base/templates/tags.tmpl
Expand Up @@ -10,13 +10,21 @@
% if items:
<h2>${messages("Categories")}</h2>
% endif
<ul class="postlist">
% for text, link in cat_items:
% if text and text not in hidden_categories:
<li><a class="reference" href="${link}">${text}</a></li>
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
% for i in range(indent_change_before):
<ul class="postlist">
% endfor
<li><a class="reference" href="${link}">${text}</a>
% if indent_change_after <= 0:
</li>
% endif
% for i in range(-indent_change_after):
</ul>
% if i + 1 < len(indent_levels):
</li>
% endif
% endfor
% endfor
</ul>
% if items:
<h2>${messages("Tags")}</h2>
% endif
Expand Down
18 changes: 13 additions & 5 deletions nikola/data/themes/bootstrap-jinja/templates/tags.tmpl
Expand Up @@ -7,13 +7,21 @@
{% if items %}
<h2>{{ messages("Categories") }}</h2>
{% endif %}
<ul class="unstyled">
{% for text, link in cat_items %}
{% if text and text not in hidden_categories %}
<li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
{% for i in range(indent_change_before) %}
<ul class="unstyled">
{% endfor %}
<li><a class="reference badge" href="{{ link }}">{{ text }}</a>
{% if indent_change_after <= 0 %}
</li>
{% endif %}
{% for i in range(-indent_change_after) %}
</ul>
{% if i + 1 < indent_levels|length %}
</li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
{% if items %}
<h2>{{ messages("Tags") }}</h2>
{% endif %}
Expand Down
18 changes: 13 additions & 5 deletions nikola/data/themes/bootstrap/templates/tags.tmpl
Expand Up @@ -7,13 +7,21 @@
% if items:
<h2>${messages("Categories")}</h2>
% endif
<ul class="unstyled">
% for text, link in cat_items:
% if text and text not in hidden_categories:
<li><a class="reference badge" href="${link}">${text}</a></li>
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
% for i in range(indent_change_before):
<ul class="unstyled">
% endfor
<li><a class="reference badge" href="${link}">${text}</a>
% if indent_change_after <= 0:
</li>
% endif
% for i in range(-indent_change_after):
</ul>
% if i + 1 < len(indent_levels):
</li>
% endif
% endfor
% endfor
</ul>
% if items:
<h2>${messages("Tags")}</h2>
% endif
Expand Down
54 changes: 53 additions & 1 deletion nikola/nikola.py
Expand Up @@ -35,6 +35,7 @@
import os
import json
import sys
import natsort
import mimetypes
try:
from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote
Expand Down Expand Up @@ -318,6 +319,8 @@ def __init__(self, **config):
'CATEGORY_PAGES_ARE_INDEXES': None, # None means: same as TAG_PAGES_ARE_INDEXES
'CATEGORY_PAGES_DESCRIPTIONS': {},
'CATEGORY_PREFIX': 'cat_',
'CATEGORY_ALLOW_HIERARCHIES': False,
'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
'CODE_COLOR_SCHEME': 'default',
'COMMENT_SYSTEM': 'disqus',
'COMMENTS_IN_GALLERIES': False,
Expand Down Expand Up @@ -1353,6 +1356,53 @@ def flatten(task):
'task_dep': task_dep
}

def parse_category_name(self, category_name):
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
try:
return utils.parse_escaped_hierarchical_category_name(category_name)
except Exception as e:
utils.LOGGER.error(str(e))
sys.exit(1)
else:
return [category_name] if len(category_name) > 0 else []

def category_path_to_category_name(self, category_path):
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
return utils.join_hierarchical_category_path(category_path)
else:
return ''.join(category_path)

def _add_post_to_category(self, post, category_name):
category_path = self.parse_category_name(category_name)
current_path = []
current_subtree = self.category_hierarchy
for current in category_path:
current_path.append(current)
if current not in current_subtree:
current_subtree[current] = {}
current_subtree = current_subtree[current]
self.posts_per_category[self.category_path_to_category_name(current_path)].append(post)

def _sort_category_hierarchy(self):
# First create a hierarchy of TreeNodes
self.category_hierarchy_lookup = {}

def create_hierarchy(cat_hierarchy, parent=None):
result = []
for name, children in cat_hierarchy.items():
node = utils.TreeNode(name, parent)
node.children = create_hierarchy(children, node)
node.category_path = [pn.name for pn in node.get_path()]
node.category_name = self.category_path_to_category_name(node.category_path)
self.category_hierarchy_lookup[node.category_name] = node
if node.category_name not in self.config.get('HIDDEN_CATEGORIES'):
result.append(node)
return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC)

root_list = create_hierarchy(self.category_hierarchy)
# Next, flatten the hierarchy
self.category_hierarchy = utils.flatten_tree_structure(root_list)

def scan_posts(self, really=False, ignore_quit=False, quiet=False):
"""Scan all the posts."""
if self._scanned and not really:
Expand All @@ -1365,6 +1415,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
self.posts_per_month = defaultdict(list)
self.posts_per_tag = defaultdict(list)
self.posts_per_category = defaultdict(list)
self.category_hierarchy = {}
self.post_per_file = {}
self.timeline = []
self.pages = []
Expand Down Expand Up @@ -1442,7 +1493,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
else:
slugged_tags.add(utils.slugify(tag, force=True))
self.posts_per_tag[tag].append(post)
self.posts_per_category[post.meta('category')].append(post)
self._add_post_to_category(post, post.meta('category'))

if post.is_post:
# unpublished posts
Expand All @@ -1463,6 +1514,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
self.all_posts.reverse()
self.pages.sort(key=lambda p: p.date)
self.pages.reverse()
self._sort_category_hierarchy()

for i, p in enumerate(self.posts[1:]):
p.next_post = self.posts[i]
Expand Down

0 comments on commit 85617e3

Please sign in to comment.