Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
First shot on #993 for taxonomies, in particular archives.
  • Loading branch information
felixfontein committed May 19, 2017
1 parent fe2c5cd commit 0d07b7b
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 16 deletions.
10 changes: 10 additions & 0 deletions nikola/data/themes/base/templates/archive_navigation_helper.tmpl
Expand Up @@ -23,5 +23,15 @@
</ul>
</nav>
%endif
%if other_archive_languages:
<nav class="archivelang">
<h3 class="posttranslations-intro">${messages("Also available in:")}</h3>
<ul>
%for language, archive_path, title in other_archive_languages:
<li><a href="${_link('archive', archive_path, language)}" rel="alternate">${messages("LANGUAGE", language)} (${title|h})</a></li>
%endfor
</ul>
</nav>
%endif
% endif
</%def>
3 changes: 3 additions & 0 deletions nikola/data/themes/base/templates/archiveindex.tmpl
Expand Up @@ -11,6 +11,9 @@
%elif generate_atom:
<link rel="alternate" type="application/atom+xml" title="Atom for the ${archive_name} archive" href="${_link("archive_atom", archive_name)}">
%endif
%for language, archive_path, _ in other_archive_languages:
<link rel="alternate" hreflang="${language}" href="${_link('archive', archive_path, language)}">
%endfor
</%block>

<%block name="content_header">
Expand Down
8 changes: 8 additions & 0 deletions nikola/data/themes/base/templates/list.tmpl
Expand Up @@ -2,6 +2,14 @@
<%inherit file="base.tmpl"/>
<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/>

<%block name="extra_head">
%if 'archive_page' in pagekind:
%for language, archive_path, _ in other_archive_languages:
<link rel="alternate" hreflang="${language}" href="${_link('archive', archive_path, language)}">
%endfor
%endif
</%block>

<%block name="content">
<article class="listpage">
<header>
Expand Down
8 changes: 8 additions & 0 deletions nikola/data/themes/base/templates/list_post.tmpl
Expand Up @@ -2,6 +2,14 @@
<%inherit file="base.tmpl"/>
<%namespace name="archive_nav" file="archive_navigation_helper.tmpl" import="*"/>

<%block name="extra_head">
%if 'archive_page' in pagekind:
%for language, archive_path, _ in other_archive_languages:
<link rel="alternate" hreflang="${language}" href="${_link('archive', archive_path, language)}">
%endfor
%endif
</%block>

<%block name="content">
<article class="listpage">
<header>
Expand Down
21 changes: 21 additions & 0 deletions nikola/plugin_categories.py
Expand Up @@ -602,6 +602,12 @@ class Taxonomy(BasePlugin):
language, or only the classifications for one language in its language's
pages.
other_language_variable_name = None:
In case this is not `None`, each classification page will get a list
of triples `(other_lang, other_classification, title)` of classifications
in other languages which should be linked. The list will be stored in a
variable by the name `other_language_variable_name`.
path_handler_docstrings:
A dictionary of docstrings for path handlers. See eg. nikola.py for
examples. Must be overridden, keys are "taxonomy_index", "taxonomy",
Expand Down Expand Up @@ -633,6 +639,7 @@ class Taxonomy(BasePlugin):
minimum_post_count_per_classification_in_overview = 1
omit_empty_classifications = False
also_create_classifications_from_other_languages = True
other_language_variable_name = None
path_handler_docstrings = {
'taxonomy_index': '',
'taxonomy': '',
Expand Down Expand Up @@ -801,3 +808,17 @@ def postprocess_posts_per_classification(self, posts_per_classification_per_lang
`utils.TreeNode` objects.
"""
pass

def get_other_language_variants(self, classification, lang, classifications_per_language):
"""Return a list of variants of the same classification in other languages.
Given a `classification` in a language `lang`, return a list of pairs
`(other_classification, other_lang)` with `lang != other_lang` such that
`classification` should be linked to `other_classification`.
Classifications where links to other language versions makes no sense
should simply return an empty list.
Provided is a set of classifications per language (`classifications_per_language`).
"""
return []
5 changes: 5 additions & 0 deletions nikola/plugins/task/archive.py
Expand Up @@ -53,6 +53,7 @@ class Archive(Taxonomy):
minimum_post_count_per_classification_in_overview = 1
omit_empty_classifications = False
also_create_classifications_from_other_languages = False
other_language_variable_name = 'other_archive_languages'
path_handler_docstrings = {
'archive_index': False,
'archive': """Link to archive path, name is the year.
Expand Down Expand Up @@ -243,3 +244,7 @@ def postprocess_posts_per_classification(self, posts_per_archive_per_language, f
def should_generate_classification_page(self, classification, post_list, lang):
"""Only generates list of posts for classification if this function returns True."""
return classification == '' or len(post_list) > 0

def get_other_language_variants(self, classification, lang, classifications_per_language):
"""Return a list of variants of the same classification in other languages."""
return [(other_lang, classification) for other_lang, lookup in classifications_per_language.items() if classification in lookup and other_lang != lang]
128 changes: 112 additions & 16 deletions nikola/plugins/task/taxonomies.py
Expand Up @@ -30,6 +30,7 @@
import blinker
import os
import natsort
from collections import defaultdict
from copy import copy
try:
from urlparse import urljoin
Expand Down Expand Up @@ -302,15 +303,60 @@ def get_subnode_data(subnode):
task['basename'] = self.name
return task

def _generate_classification_page(self, taxonomy, classification, post_list, lang):
@staticmethod
def _sort_classifications(taxonomy, classifications, lang):
"""Sort the given list of classifications of the given taxonomy and language."""
if taxonomy.has_hierarchy:
# To sort a hierarchy of classifications correctly, we first
# build a tree out of them (and mark for each node whether it
# appears in the list), then sort the tree node-wise, and finally
# collapse the tree into a list of recombined classifications.

# Step 1: build hierarchy. Here, each node consists of a boolean
# flag (node appears in list) and a dictionary mapping path elements
# to nodes.
root = [False, {}]
for classification in classifications:
node = root
for elt in taxonomy.extract_hierarchy(classification):
if elt not in node[1]:
node[1][elt] = [False, {}]
node = node[1][elt]
node[0] = True
# Step 2: sort hierarchy. The result for a node is a pair
# (flag, subnodes), where subnodes is a list of pairs (name, subnode).

def sort_node(node, level=0):
keys = natsort.natsorted(node[1].keys(), alg=natsort.ns.F | natsort.ns.IC)
taxonomy.sort_classifications(keys, lang, level)
subnodes = []
for key in keys:
subnodes.append((key, sort_node(node[1][key], level + 1)))
return (node[0], subnodes)

root = sort_node(root)
# Step 3: collapse the tree structure into a linear sorted list,
# with a node coming before its children.

def append_node(classifications, node, path=[]):
if node[0]:
classifications.append(taxonomy.recombine_classification_from_hierarchy(path))
for key, subnode in node[1]:
append_node(classifications, subnode, path + [key])

classifications = []
append_node(classifications, root)
return classifications
else:
# Sorting a flat hierarchy is simpler. We pre-sort with
# natsorted and call taxonomy.sort_classifications.
classifications = natsort.natsorted(classifications, alg=natsort.ns.F | natsort.ns.IC)
taxonomy.sort_classifications(classifications, lang)
return classifications

def _generate_classification_page(self, taxonomy, classification, filtered_posts, generate_list, generate_rss, lang, post_lists_per_lang, classification_set_per_lang=None):
"""Render index or post list and associated feeds per classification."""
# Filter list
filtered_posts = self._filter_list(post_list, lang)
if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications:
return
# Should we create this list?
generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang)
generate_rss = taxonomy.should_generate_rss_for_classification_page(classification, filtered_posts, lang)
if not generate_list and not generate_rss:
return
# Get data
Expand All @@ -335,6 +381,27 @@ def _generate_classification_page(self, taxonomy, classification, post_list, lan
kw["index_file"] = self.site.config['INDEX_FILE']
context = copy(context)
context["permalink"] = self.site.link(taxonomy.classification_name, classification, lang)
# Get links to other language versions of this classification
if classification_set_per_lang is not None:
other_lang_links = taxonomy.get_other_language_variants(classification, lang, classification_set_per_lang)
# Collect by language
links_per_lang = defaultdict(list)
for other_lang, link in other_lang_links:
# Make sure we ignore the current language (in case the
# plugin accidentally returns links for it as well)
if other_lang != lang:
links_per_lang[other_lang].append(link)
# Sort first by language, then by classification
sorted_links = []
for other_lang in sorted(links_per_lang.keys()):
links = self._sort_classifications(taxonomy, links_per_lang[other_lang], other_lang)
sorted_links.extend([(other_lang, classification,
taxonomy.get_classification_friendly_name(classification, other_lang))
for classification in links if post_lists_per_lang[other_lang][classification][1]])
# Store result in context and kw
context[taxonomy.other_language_variable_name] = sorted_links
kw[taxonomy.other_language_variable_name] = sorted_links
# Allow other plugins to modify the result
blinker.signal('generate_classification_page').send({
'site': self.site,
'taxonomy': taxonomy,
Expand Down Expand Up @@ -368,6 +435,42 @@ def gen_tasks(self):
self.site.scan_posts()
yield self.group_task()

# Cache classification sets per language for taxonomies where
# other_language_variable_name is set.
classification_set_per_lang = {}
for taxonomy in self.site.taxonomy_plugins.values():
if taxonomy.other_language_variable_name is not None:
lookup = self.site.posts_per_classification[taxonomy.classification_name]
cspl = {lang: set(lookup[lang].keys()) for lang in lookup}
classification_set_per_lang[taxonomy.classification_name] = cspl

# Collect post lists for classification pages and determine whether
# they should be generated.
post_lists_per_lang = {}
for taxonomy in self.site.taxonomy_plugins.values():
plpl = {}
for lang in self.site.config["TRANSLATIONS"]:
classifications = {}
for tlang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items():
if lang != tlang and not taxonomy.also_create_classifications_from_other_languages:
continue
classifications.update(posts_per_classification)
result = {}
for classification, posts in classifications.items():
# Filter list
filtered_posts = self._filter_list(posts, lang)
if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications:
generate_list = False
generate_rss = False
else:
# Should we create this list?
generate_list = taxonomy.should_generate_classification_page(classification, filtered_posts, lang)
generate_rss = taxonomy.should_generate_rss_for_classification_page(classification, filtered_posts, lang)
result[classification] = (filtered_posts, generate_list, generate_rss)
plpl[lang] = result
post_lists_per_lang[taxonomy.classification_name] = plpl

# Now generate pages
for lang in self.site.config["TRANSLATIONS"]:
# To support that tag and category classifications share the same overview,
# we explicitly detect this case:
Expand All @@ -385,16 +488,9 @@ def gen_tasks(self):
for task in self._generate_classification_overview(taxonomy, lang):
yield task

# Generate classification lists
classifications = {}
for tlang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items():
if lang != tlang and not taxonomy.also_create_classifications_from_other_languages:
continue
classifications.update(posts_per_classification)

# Process classifications
for classification, posts in classifications.items():
for task in self._generate_classification_page(taxonomy, classification, posts, lang):
for classification, (filtered_posts, generate_list, generate_rss) in post_lists_per_lang[taxonomy.classification_name][lang].items():
for task in self._generate_classification_page(taxonomy, classification, filtered_posts, generate_list, generate_rss, lang, post_lists_per_lang[taxonomy.classification_name], classification_set_per_lang.get(taxonomy.classification_name)):
yield task
# In case we are ignoring plugins for overview, we must have a collision for
# tags and categories. Handle this special case with extra code.
Expand Down

0 comments on commit 0d07b7b

Please sign in to comment.