Skip to content

Commit 85617e3

Browse files
committedMay 12, 2015
Merge pull request #1711 from getnikola/category_hierarchies
Allowing category hierarchies. (fixes #1520)
2 parents fa27ee5 + 45c0bca commit 85617e3

File tree

19 files changed

+388
-66
lines changed

19 files changed

+388
-66
lines changed
 

Diff for: ‎CHANGES.txt

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ New in v7.4.1
1515
Features
1616
--------
1717

18+
* Allowing category hierarchies via new option CATEGORY_ALLOW_HIERARCHIES
19+
(Issue #1520)
1820
* Better handling of missing/unconfigured compilers (Issue #1704)
1921
* New -r option for the link checker to check remote links (Issue #1684)
2022
* Use static navbars in bootstrap3 and bootstrap themes

Diff for: ‎nikola/conf.py.in

+10
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ HIDDEN_TAGS = ['mathjax']
250250
# CATEGORY_PATH = "categories"
251251
# CATEGORY_PREFIX = "cat_"
252252

253+
# If CATEGORY_ALLOW_HIERARCHIES is set to True, categories can be organized in
254+
# hierarchies. For a post, the whole path in the hierarchy must be specified,
255+
# using a forward slash ('/') to separate paths. Use a backslash ('\') to escape
256+
# a forward slash or a backslash (i.e. '\//\\' is a path specifying the
257+
# subcategory called '\' of the top-level category called '/').
258+
# CATEGORY_ALLOW_HIERARCHIES = False
259+
# If CATEGORY_OUTPUT_FLAT_HIERARCHY is set to True, the output written to output
260+
# contains only the name of the leaf category and not the whole path.
261+
# CATEGORY_OUTPUT_FLAT_HIERARCHY = False
262+
253263
# If CATEGORY_PAGES_ARE_INDEXES is set to True, each category's page will contain
254264
# the posts themselves. If set to False, it will be just a list of links.
255265
# CATEGORY_PAGES_ARE_INDEXES = False

Diff for: ‎nikola/data/themes/base-jinja/templates/index.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{% endblock %}
1212

1313
{% block content %}
14+
{% block content_header %}{% endblock %}
1415
<div class="postindex">
1516
{% for post in posts %}
1617
<article class="h-entry post-{{ post.meta('type') }}">

Diff for: ‎nikola/data/themes/base-jinja/templates/tag.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
{% if description %}
2121
<p>{{ description }}</p>
2222
{% endif %}
23+
{% if subcategories %}
24+
{{ messages('Subcategories:') }}
25+
<ul>
26+
{% for name, link in subcategories %}
27+
<li><a href="{{ link }}">{{ name }}</a></li>
28+
{% endfor %}
29+
</ul>
30+
{% endif %}
2331
<div class="metadata">
2432
{% if translations|length > 1 and generate_rss %}
2533
{% for language in translations %}

Diff for: ‎nikola/data/themes/base-jinja/templates/tagindex.tmpl

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
{# -*- coding: utf-8 -*- #}
22
{% extends 'index.tmpl' %}
33

4+
{% block content_header %}
5+
{% if subcategories %}
6+
{{ messages('Subcategories:') }}
7+
<ul>
8+
{% for name, link in subcategories %}
9+
<li><a href="{{ link }}">{{ name }}</a></li>
10+
{% endfor %}
11+
</ul>
12+
{% endif %}
13+
{% endblock %}
14+
415
{% block extra_head %}
516
{{ super() }}
617
{% if translations|length > 1 and generate_atom %}

Diff for: ‎nikola/data/themes/base-jinja/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010
{% if items %}
1111
<h2>{{ messages("Categories") }}</h2>
1212
{% endif %}
13-
<ul class="postlist">
14-
{% for text, link in cat_items %}
15-
{% if text and text not in hidden_categories %}
16-
<li><a class="reference" href="{{ link }}">{{ text }}</a></li>
13+
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
14+
{% for i in range(indent_change_before) %}
15+
<ul class="postlist">
16+
{% endfor %}
17+
<li><a class="reference" href="{{ link }}">{{ text }}</a>
18+
{% if indent_change_after <= 0 %}
19+
</li>
1720
{% endif %}
21+
{% for i in range(-indent_change_after) %}
22+
</ul>
23+
{% if i + 1 < indent_levels|length %}
24+
</li>
25+
{% endif %}
26+
{% endfor %}
1827
{% endfor %}
19-
</ul>
2028
{% if items %}
2129
<h2>{{ messages("Tags") }}</h2>
2230
{% endif %}

Diff for: ‎nikola/data/themes/base/messages/messages_de.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"Read more": "Weiterlesen",
2929
"Skip to main content": "Springe zum Hauptinhalt",
3030
"Source": "Source",
31+
"Subcategories:": "Unterkategorien:",
3132
"Tags and Categories": "Tags und Kategorien",
3233
"Tags": "Tags",
3334
"old posts, page %d": "Ältere Einträge, Seite %d",

Diff for: ‎nikola/data/themes/base/messages/messages_en.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"Read more": "Read more",
2929
"Skip to main content": "Skip to main content",
3030
"Source": "Source",
31+
"Subcategories:": "Subcategories:",
3132
"Tags and Categories": "Tags and Categories",
3233
"Tags": "Tags",
3334
"old posts, page %d": "old posts, page %d",

Diff for: ‎nikola/data/themes/base/templates/index.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
</%block>
1212

1313
<%block name="content">
14+
<%block name="content_header"></%block>
1415
<div class="postindex">
1516
% for post in posts:
1617
<article class="h-entry post-${post.meta('type')}">

Diff for: ‎nikola/data/themes/base/templates/tag.tmpl

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
%if description:
2121
<p>${description}</p>
2222
%endif
23+
%if subcategories:
24+
${messages('Subcategories:')}
25+
<ul>
26+
%for name, link in subcategories:
27+
<li><a href="${link}">${name}</a></li>
28+
%endfor
29+
</ul>
30+
%endif
2331
<div class="metadata">
2432
%if len(translations) > 1 and generate_rss:
2533
%for language in translations:

Diff for: ‎nikola/data/themes/base/templates/tagindex.tmpl

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
## -*- coding: utf-8 -*-
22
<%inherit file="index.tmpl"/>
33

4+
<%block name="content_header">
5+
%if subcategories:
6+
${messages('Subcategories:')}
7+
<ul>
8+
%for name, link in subcategories:
9+
<li><a href="${link}">${name}</a></li>
10+
%endfor
11+
</ul>
12+
%endif
13+
</%block>
14+
415
<%block name="extra_head">
516
${parent.extra_head()}
617
%if len(translations) > 1 and generate_atom:

Diff for: ‎nikola/data/themes/base/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010
% if items:
1111
<h2>${messages("Categories")}</h2>
1212
% endif
13-
<ul class="postlist">
14-
% for text, link in cat_items:
15-
% if text and text not in hidden_categories:
16-
<li><a class="reference" href="${link}">${text}</a></li>
13+
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
14+
% for i in range(indent_change_before):
15+
<ul class="postlist">
16+
% endfor
17+
<li><a class="reference" href="${link}">${text}</a>
18+
% if indent_change_after <= 0:
19+
</li>
1720
% endif
21+
% for i in range(-indent_change_after):
22+
</ul>
23+
% if i + 1 < len(indent_levels):
24+
</li>
25+
% endif
26+
% endfor
1827
% endfor
19-
</ul>
2028
% if items:
2129
<h2>${messages("Tags")}</h2>
2230
% endif

Diff for: ‎nikola/data/themes/bootstrap-jinja/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
{% if items %}
88
<h2>{{ messages("Categories") }}</h2>
99
{% endif %}
10-
<ul class="unstyled">
11-
{% for text, link in cat_items %}
12-
{% if text and text not in hidden_categories %}
13-
<li><a class="reference badge" href="{{ link }}">{{ text }}</a></li>
10+
{% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy %}
11+
{% for i in range(indent_change_before) %}
12+
<ul class="unstyled">
13+
{% endfor %}
14+
<li><a class="reference badge" href="{{ link }}">{{ text }}</a>
15+
{% if indent_change_after <= 0 %}
16+
</li>
1417
{% endif %}
18+
{% for i in range(-indent_change_after) %}
19+
</ul>
20+
{% if i + 1 < indent_levels|length %}
21+
</li>
22+
{% endif %}
23+
{% endfor %}
1524
{% endfor %}
16-
</ul>
1725
{% if items %}
1826
<h2>{{ messages("Tags") }}</h2>
1927
{% endif %}

Diff for: ‎nikola/data/themes/bootstrap/templates/tags.tmpl

+13-5
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,21 @@
77
% if items:
88
<h2>${messages("Categories")}</h2>
99
% endif
10-
<ul class="unstyled">
11-
% for text, link in cat_items:
12-
% if text and text not in hidden_categories:
13-
<li><a class="reference badge" href="${link}">${text}</a></li>
10+
% for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
11+
% for i in range(indent_change_before):
12+
<ul class="unstyled">
13+
% endfor
14+
<li><a class="reference badge" href="${link}">${text}</a>
15+
% if indent_change_after <= 0:
16+
</li>
1417
% endif
18+
% for i in range(-indent_change_after):
19+
</ul>
20+
% if i + 1 < len(indent_levels):
21+
</li>
22+
% endif
23+
% endfor
1524
% endfor
16-
</ul>
1725
% if items:
1826
<h2>${messages("Tags")}</h2>
1927
% endif

Diff for: ‎nikola/nikola.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import os
3636
import json
3737
import sys
38+
import natsort
3839
import mimetypes
3940
try:
4041
from urlparse import urlparse, urlsplit, urlunsplit, urljoin, unquote
@@ -318,6 +319,8 @@ def __init__(self, **config):
318319
'CATEGORY_PAGES_ARE_INDEXES': None, # None means: same as TAG_PAGES_ARE_INDEXES
319320
'CATEGORY_PAGES_DESCRIPTIONS': {},
320321
'CATEGORY_PREFIX': 'cat_',
322+
'CATEGORY_ALLOW_HIERARCHIES': False,
323+
'CATEGORY_OUTPUT_FLAT_HIERARCHY': False,
321324
'CODE_COLOR_SCHEME': 'default',
322325
'COMMENT_SYSTEM': 'disqus',
323326
'COMMENTS_IN_GALLERIES': False,
@@ -1353,6 +1356,53 @@ def flatten(task):
13531356
'task_dep': task_dep
13541357
}
13551358

1359+
def parse_category_name(self, category_name):
1360+
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
1361+
try:
1362+
return utils.parse_escaped_hierarchical_category_name(category_name)
1363+
except Exception as e:
1364+
utils.LOGGER.error(str(e))
1365+
sys.exit(1)
1366+
else:
1367+
return [category_name] if len(category_name) > 0 else []
1368+
1369+
def category_path_to_category_name(self, category_path):
1370+
if self.config['CATEGORY_ALLOW_HIERARCHIES']:
1371+
return utils.join_hierarchical_category_path(category_path)
1372+
else:
1373+
return ''.join(category_path)
1374+
1375+
def _add_post_to_category(self, post, category_name):
1376+
category_path = self.parse_category_name(category_name)
1377+
current_path = []
1378+
current_subtree = self.category_hierarchy
1379+
for current in category_path:
1380+
current_path.append(current)
1381+
if current not in current_subtree:
1382+
current_subtree[current] = {}
1383+
current_subtree = current_subtree[current]
1384+
self.posts_per_category[self.category_path_to_category_name(current_path)].append(post)
1385+
1386+
def _sort_category_hierarchy(self):
1387+
# First create a hierarchy of TreeNodes
1388+
self.category_hierarchy_lookup = {}
1389+
1390+
def create_hierarchy(cat_hierarchy, parent=None):
1391+
result = []
1392+
for name, children in cat_hierarchy.items():
1393+
node = utils.TreeNode(name, parent)
1394+
node.children = create_hierarchy(children, node)
1395+
node.category_path = [pn.name for pn in node.get_path()]
1396+
node.category_name = self.category_path_to_category_name(node.category_path)
1397+
self.category_hierarchy_lookup[node.category_name] = node
1398+
if node.category_name not in self.config.get('HIDDEN_CATEGORIES'):
1399+
result.append(node)
1400+
return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC)
1401+
1402+
root_list = create_hierarchy(self.category_hierarchy)
1403+
# Next, flatten the hierarchy
1404+
self.category_hierarchy = utils.flatten_tree_structure(root_list)
1405+
13561406
def scan_posts(self, really=False, ignore_quit=False, quiet=False):
13571407
"""Scan all the posts."""
13581408
if self._scanned and not really:
@@ -1365,6 +1415,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
13651415
self.posts_per_month = defaultdict(list)
13661416
self.posts_per_tag = defaultdict(list)
13671417
self.posts_per_category = defaultdict(list)
1418+
self.category_hierarchy = {}
13681419
self.post_per_file = {}
13691420
self.timeline = []
13701421
self.pages = []
@@ -1442,7 +1493,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
14421493
else:
14431494
slugged_tags.add(utils.slugify(tag, force=True))
14441495
self.posts_per_tag[tag].append(post)
1445-
self.posts_per_category[post.meta('category')].append(post)
1496+
self._add_post_to_category(post, post.meta('category'))
14461497

14471498
if post.is_post:
14481499
# unpublished posts
@@ -1463,6 +1514,7 @@ def scan_posts(self, really=False, ignore_quit=False, quiet=False):
14631514
self.all_posts.reverse()
14641515
self.pages.sort(key=lambda p: p.date)
14651516
self.pages.reverse()
1517+
self._sort_category_hierarchy()
14661518

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

0 commit comments

Comments
 (0)
Please sign in to comment.