From 85fac3de6d8cb56ebbcd5cbf9d6f58c832f6658f Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 10:18:06 +0200 Subject: [PATCH 01/30] Adding taxonomy plugin type and basic handling. --- nikola/nikola.py | 3 + nikola/plugin_categories.py | 123 ++++++++++ .../plugins/misc/taxonomies_classifier.plugin | 12 + nikola/plugins/misc/taxonomies_classifier.py | 210 ++++++++++++++++ nikola/plugins/task/taxonomies.plugin | 12 + nikola/plugins/task/taxonomies.py | 227 ++++++++++++++++++ 6 files changed, 587 insertions(+) create mode 100644 nikola/plugins/misc/taxonomies_classifier.plugin create mode 100644 nikola/plugins/misc/taxonomies_classifier.py create mode 100644 nikola/plugins/task/taxonomies.plugin create mode 100644 nikola/plugins/task/taxonomies.py diff --git a/nikola/nikola.py b/nikola/nikola.py index 46813c4b7a..e80236adc3 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -73,6 +73,7 @@ SignalHandler, ConfigPlugin, PostScanner, + Taxonomy, ) if DEBUG: @@ -948,6 +949,7 @@ def init_plugins(self, commands_only=False, load_all=False): "SignalHandler": SignalHandler, "ConfigPlugin": ConfigPlugin, "PostScanner": PostScanner, + "Taxonomy": Taxonomy, }) self.plugin_manager.getPluginLocator().setPluginInfoExtension('plugin') extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS'] @@ -1019,6 +1021,7 @@ def plugin_position_in_places(plugin): self.plugin_manager.loadPlugins() + self._activate_plugins_of_category("Taxonomy") self._activate_plugins_of_category("SignalHandler") # Emit signal for SignalHandlers which need to start running immediately. diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index db36c405c0..8c87fda8b6 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -49,6 +49,7 @@ 'SignalHandler', 'ConfigPlugin', 'PostScanner', + 'Taxonomy', ) @@ -451,3 +452,125 @@ def import_file(self): def save_post(self): """Save a post to disk.""" raise NotImplementedError() + + +class Taxonomy(BasePlugin): + """Taxonomy for posts. + + A taxonomy plugin allows to classify posts (see #2107) by + classification strings. + """ + + name = "dummy_taxonomy" + + # Adjust the following values in your plugin! + + # The classification name to be used for path handlers. + classification_name = "taxonomy" + # The classification name to be used when storing the classification + # in the metadata. + metadata_name = "taxonomy" + # If True, there can be more than one classification per post; in that case, + # the classification data in the metadata is stored as a list. If False, + # the classification data in the metadata is stored as a string, or None + # when no classification is given. + more_than_one_classifications_per_post = False + # Whether the classification has a hierarchy. + has_hierarchy = False + # If True, the list for a classification includes all posts with a + # sub-classification (in case has_hierarchy is True). + include_posts_from_subhierarchies = False + # Whether to show the posts for one classification as an index or + # as a post list. + show_list_as_index = False + # The template to use for the post list for one classification. + # Set to none to avoid generating overviews. + template_for_list_of_one_classification = "tagindex.tmpl" + # The template to use for the classification overview page. + template_for_classification_overview = "list.tmpl" + # Whether this classification applies to posts. + apply_to_posts = True + # Whether this classification applies to pages. + apply_to_pages = False + # The minimum number of posts a classification must have to be listed in + # the overview. + minimum_post_count_per_classification_in_overview = 1 + # Whether post lists resp. indexes should be created for empty + # classifications. + omit_empty_classifications = False + # Whether to include all classifications for all languages in every + # language, or only the classifications for one language in its language's + # pages. + also_create_classifications_from_other_languages = True + + def classify(self, post, lang): + """Classifies the given post for the given language. + + Must return a list or tuple of strings.""" + raise NotImplementedError() + + def sort_posts(self, posts): + """Sorts the given list of posts.""" + pass + + def get_list_path(self, lang): + """A path handler for the list of all classifications. + + The last element in the returned path must have no extension, and the + PRETTY_URLS config must be ignored. The return value will be modified + based on the PRETTY_URLS and INDEX_FILE settings.""" + raise NotImplementedError() + + def get_path(self, classification, lang): + """A path handler for the given classification. + + The last element in the returned path must have no extension, and the + PRETTY_URLS config must be ignored. The return value will be modified + based on the PRETTY_URLS and INDEX_FILE settings. + + For hierarchical taxonomies, the result of extract_hierarchy is provided. + For non-hierarchical taxonomies, the classification string itself is provided.""" + raise NotImplementedError() + + def extract_hierarchy(self, classification): + """Given a classification, return a list of parts in the hierarchy. + + For non-hierarchical taxonomies, it usually suffices to return + `[classification]`.""" + return [classification] + + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string. + + For non-hierarchical taxonomies, it usually suffices to return hierarchy[0].""" + return hierarchy[0] + + def provide_list_context_and_uptodate(self): + """Provides data for the context and the uptodate list for the list of all classifiations. + + Must return a tuple of two dicts. The first is merged into the page's context, + the second will be put into the uptodate list of all generated tasks. + + Context must contain `title`.""" + raise NotImplementedError() + + def provide_context_and_uptodate(self, classification): + """Provides data for the context and the uptodate list for the list of the given classifiation. + + Must return a tuple of two dicts. The first is merged into the page's context, + the second will be put into the uptodate list of all generated tasks. + + Context must contain `title`, which should be something like 'Posts about ', + and `classification_title`, which should be related to the classification string.""" + raise NotImplementedError() + + def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """This function can rearrange, modify or otherwise use the list of posts per classification and per language. + + For compatibility reasons, the list could be stored somewhere else as well. + + In case `has_hierarchy` is `True`, `flat_hierarchy_per_lang` is the flat + hierarchy consisting of `TreeNode` elements, and `hierarchy_lookup_per_lang` + is the corresponding hierarchy lookup mapping classification strings to + `TreeNode` objects.""" + pass diff --git a/nikola/plugins/misc/taxonomies_classifier.plugin b/nikola/plugins/misc/taxonomies_classifier.plugin new file mode 100644 index 0000000000..e7546f30a9 --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_taxonomies +module = classify_taxonomies + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Classifies the timeline into taxonomies. + +[Nikola] +plugincategory = SignalHandler diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py new file mode 100644 index 0000000000..1791698589 --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2016 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the taxonomy overviews, classification pages and feeds.""" + +from __future__ import unicode_literals +import blinker +import natsort +import os +import sys + +from collections import defaultdict + +from nikola.plugin_categories import SignalHandler +from nikola import utils + + +class TaxonomiesClassifier(SignalHandler): + """Render the tag/category pages and feeds.""" + + name = "render_taxonomies" + + def _do_classification(self): + taxonomies = self.site.plugin_manager.getPluginsOfCategory('Taxonomy') + self.site.posts_per_classification = {} + for taxonomy in taxonomies: + if taxonomy.classification_name in self.site.posts_per_classification: + raise Exception("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name)) + self.site.posts_per_classification[taxonomy.classification_name] = { + lang: defaultdict(set) for lang in self.config['TRANSLATIONS'].keys() + } + + # Classify posts + for post in self.timeline: + for taxonomy in taxonomies: + if taxonomy.apply_to_posts if post.is_post else taxonomy.apply_to_pages: + classifications = {} + for lang in self.config['TRANSLATIONS'].keys(): + # Extract classifications for this language + classifications[lang] = taxonomy.classify(post, lang) + assert taxonomy.more_than_one_classifications_per_post or len(classifications[lang]) <= 1 + # Store in metadata + if taxonomy.more_than_one_classifications_per_post: + post.meta[lang][taxonomy.metadata_name] = classifications[lang] + else: + post.meta[lang][taxonomy.metadata_name] = classifications[lang][0] if len(classifications[lang]) > 0 else None + # Add post to sets + for classification in classifications[lang]: + while classification: + self.site.posts_per_classification[taxonomy.classification_name][lang][classification].add(post) + if not taxonomy.include_posts_from_subhierarchies or not taxonomy.has_hierarchy: + break + classification = taxonomy.recombine_classification_from_hierarchy(taxonomy.extract_hierarchy(classification)[:-1]) + + # Check for valid paths and for collisions + taxonomy_outputs = {lang: dict() for lang in self.config['TRANSLATIONS'].keys()} + quit = False + for taxonomy in taxonomies: + # Check for collisions (per language) + for lang in self.config['TRANSLATIONS'].keys(): + for tlang in self.config['TRANSLATIONS'].keys(): + if lang != tlang and not taxonomy.also_create_classifications_from_other_languages: + continue + for classification, posts in self.site.posts_per_classification[taxonomy.classification_name][tlang].items(): + # Obtain path as tuple + if taxonomy.has_hierarchy: + path = taxonomy.get_path(taxonomy.extract_hierarchy(classification), lang) + else: + path = taxonomy.get_path(classification, lang) + path = tuple(path) + # Check that path is OK + for path_element in path: + if len(path_element) == 0: + utils.LOGGER.error("{0} {1} yields invalid path '{2}'!".format(taxonomy.classification_name.title(), classification, '/'.join(path))) + quit = True + # Determine collisions + if path in taxonomy_outputs[lang]: + other_classification_name, other_classification, other_posts = taxonomy_outputs[lang][path] + utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {1} "{2}"'.format( + taxonomy.classification_name, classification, other_classification_name, other_classification)) + utils.LOGGER.error('{0} {1} is used in: {1}'.format( + taxonomy.classification_name.title(), classification, ', '.join(sorted([p.source_path for p in posts])))) + utils.LOGGER.error('{0} {1} is used in: {1}'.format( + other_classification_name.title(), other_classification, ', '.join(sorted([p.source_path for p in other_posts])))) + quit = True + else: + taxonomy_outputs[lang][path] = (taxonomy.classification_name, classification, posts) + if quit: + sys.exit(1) + + # Sort everything. + self.site.flat_hierarchy_per_classification = {} + self.site.hierarchy_lookup_per_classification = {} + for taxonomy in taxonomies: + # Sort post lists + for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + # Convert sets to lists and sort them + for classification in list(posts_per_classification.keys()): + posts = list(posts_per_classification[classification]) + posts.sort(key=lambda p: + (int(p.meta('priority')) if p.meta('priority') else 0, + p.date, p.source_path)) + posts.reverse() + posts_per_classification[classification] = posts + # Create hierarchy information + if taxonomy.has_hierarchy: + self.site.flat_hierarchy_per_classification[taxonomy.classification_name] = {} + self.site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {} + for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + # Compose hierarchy + hierarchy = {} + for classification in posts_per_classification.keys(): + hier = taxonomy.extract_hierarchy(classification) + node = hierarchy + for he in hier: + if he not in node: + node[he] = {} + node = node[he] + hierarchy_lookup = {} + + def create_hierarchy(cat_hierarchy, parent=None): + """Create category hierarchy.""" + result = [] + for name, children in cat_hierarchy.items(): + node = utils.TreeNode(name, parent) + node.children = create_hierarchy(children, node) + node.classification_path = [pn.name for pn in node.get_path()] + node.classification_name = taxonomy.recombine_classification_from_hierarchy(node.classification_path) + hierarchy_lookup[node.classification_name] = node + return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC) + + root_list = create_hierarchy(hierarchy) + flat_hierarchy = utils.flatten_tree_structure(root_list) + # Store result + self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy + self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup + taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], + self.site.flat_hierarchy_per_classification[taxonomy.classification_name], + self.site.hierarchy_lookup_per_classification[taxonomy.classification_name]) + else: + taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], flat_hierarchy, hierarchy_lookup) + + def _postprocess_path(self, path, lang, force_extension=None): + if force_extension is not None: + if len(path) == 0: + path = [os.path.splitext(self.site.config['INDEX_FILE'])[0]] + path[-1] += force_extension + elif self.site.config['PRETTY_URLS'] or len(path) == 0: + path = path + [self.site.config['INDEX_FILE']] + else: + path[-1] += '.html' + return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + path if _f] + + def _taxonomy_index_path(self, lang, taxonomy): + """Return path to the classification overview.""" + return self._postprocess_path(taxonomy.get_list_path(lang), lang) + + def _taxonomy_path(self, name, lang, taxonomy, force_extension=None): + """Return path to a classification.""" + if taxonomy.has_hirearchy: + path = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang) + else: + path = taxonomy.get_path(name, lang) + return self._postprocess_path(path, lang, force_extension=force_extension) + + def _taxonomy_atom_path(self, name, lang, taxonomy): + """Return path to a classification Atom feed.""" + return self._taxonomy_path(name, lang, taxonomy, force_extension='.atom') + + def _taxonomy_rss_path(self, name, lang, taxonomy): + """Return path to a classification RSS feed.""" + return self._taxonomy_path(name, lang, taxonomy, force_extension='.xml') + + def _register_path_handlers(self, taxonomy): + self.site.register_path_handler('{0}_index'.format(taxonomy.classification_name), lambda name, lang: self._tag_index_path(lang, taxonomy)) + self.site.register_path_handler('{0}'.format(taxonomy.classification_name), lambda name, lang: self._tag_path(name, lang, taxonomy)) + self.site.register_path_handler('{0}_atom'.format(taxonomy.classification_name), lambda name, lang: self._tag_atom_path(name, lang, taxonomy)) + self.site.register_path_handler('{0}_rss'.format(taxonomy.classification_name), lambda name, lang: self._tag_rss_path(name, lang, taxonomy)) + + def set_site(self, site): + """Set site, which is a Nikola instance.""" + super(TaxonomiesClassifier, self).set_site(site) + # Add hook for after post scanning + blinker.signal("scanned").connect(self._do_classification) + # Register path handlers + for taxonomy in self.plugin_manager.getPluginsOfCategory('Taxonomy'): + self._register_path_handlers(taxonomy) diff --git a/nikola/plugins/task/taxonomies.plugin b/nikola/plugins/task/taxonomies.plugin new file mode 100644 index 0000000000..5dbb4f13de --- /dev/null +++ b/nikola/plugins/task/taxonomies.plugin @@ -0,0 +1,12 @@ +[Core] +name = render_taxonomies +module = taxonomies + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Render the taxonomy overviews, classification pages and feeds. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py new file mode 100644 index 0000000000..5615f987ec --- /dev/null +++ b/nikola/plugins/task/taxonomies.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2016 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the taxonomy overviews, classification pages and feeds.""" + +from __future__ import unicode_literals +import os +import natsort +from copy import copy +try: + from urlparse import urljoin +except ImportError: + from urllib.parse import urljoin # NOQA + +from nikola.plugin_categories import Task +from nikola import utils +from nikola.nikola import _enclosure + + +class RenderTaxonomies(Task): + """Render the tag/category pages and feeds.""" + + name = "render_taxonomies" + + def _generate_classification_overview(self, taxonomy, lang): + """Create a global "all your tags/categories" page for each language.""" + context, kw = taxonomy.provide_list_context_and_uptodate(lang) + + context = copy(context) + kw = copy(kw) + kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview + + # Collect all relevant classifications + if taxonomy.has_hierarchy: + classifications = [cat.classification_name for cat in self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang]] + else: + classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items() + if len(posts) >= kw["minimum_post_count"]], + alg=natsort.ns.F | natsort.ns.IC) + + # Set up classifications in context + context[taxonomy.metadata_name] = classifications + context["items"] = [(classification, self.site.link(taxonomy.classification_name, classification, lang)) for classification in classifications] + context["has_hierarchy"] = taxonomy.has_hierarchy + if taxonomy.has_hierarchy: + context["hierarchy"] = [(node.name, node.classification_name, node.classification_path, + self.site.link(taxonomy.classification_name, node.classification_name, lang), + node.indent_levels, node.indent_change_before, + node.indent_change_after) + for node in self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang]] + + # Prepare rendering + context["permalink"] = self.site.link("{}_index".format(taxonomy.classification_name), None, lang) + context["pagekind"] = ["list", "tags_page"] + output_name = os.path.join(kw['output_folder'], self.site.path('{}_index'.format(taxonomy.classification_name), None, lang)) + task = self.site.generic_post_list_renderer( + lang, + [], + output_name, + taxonomy.template_for_classification_overview, + kw['filters'], + context, + ) + task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:page')] + task['basename'] = str(self.name) + yield task + + def _generate_classification_page_as_rss(self, taxonomy, classification, filtered_posts, title, kw, lang): + """Create a RSS feed for a single classification in a given language.""" + kind = taxonomy.classification_name + # Render RSS + output_name = os.path.normpath(os.path.join(kw['output_folder'], self.site.path(kind + "_rss", classification, lang))) + feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", classification, lang).lstrip('/')) + deps = [] + deps_uptodate = [] + for post in filtered_posts: + deps += post.deps(lang) + deps_uptodate += post.deps_uptodate(lang) + task = { + 'basename': str(self.name), + 'name': output_name, + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, "{0} ({1})".format(kw["blog_title"](lang), title), + kw["site_url"], None, filtered_posts, + output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], + feed_url, _enclosure, kw["feed_link_append_query"]))], + 'clean': True, + 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:rss')] + deps_uptodate, + 'task_dep': ['render_posts'], + } + return utils.apply_filters(task, kw['filters']) + + def _generate_classification_page_as_index(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Render a sort of index page collection using only this classification's posts.""" + kind = taxonomy.classification_name + + def page_link(i, displayed_i, num_pages, force_addition, extension=None): + feed = "{}_atom" if extension == ".atom" else "{}" + return utils.adjust_name_for_index_link(self.site.link(feed.format(kind), classification, lang), i, displayed_i, lang, self.site, force_addition, extension) + + def page_path(i, displayed_i, num_pages, force_addition, extension=None): + feed = "{}_atom" if extension == ".atom" else "{}" + return utils.adjust_name_for_index_path(self.site.path(feed.format(kind), classification, lang), i, displayed_i, lang, self.site, force_addition, extension) + + context = copy(context) + if kw["generate_rss"]: + # On a tag page, the feeds include the tag's feeds + rss_link = ("""""".format( + taxonomy.classification_name, context['classification_title'], lang, self.site.link('{}_rss'.format(kind), classification, lang))) + context['rss_link'] = rss_link + context["pagekind"] = ["index", "tag_page"] + template_name = taxonomy.template_for_list_of_one_classification + + yield self.site.generic_index_renderer(lang, filtered_posts, context['title'], template_name, context, kw, str(self.name), page_link, page_path) + + def _generate_classification_page_as_list_atom(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Generate atom feeds for classification lists.""" + kind = taxonomy.classification_name + context = copy(context) + context['feedlink'] = self.site.abs_link(self.site.path('{}_atom'.format(kind), classification, lang)) + feed_path = os.path.join(kw['output_folder'], self.site.path('{}_atom'.format(kind), classification, lang)) + + task = { + 'basename': str(self.name), + 'name': feed_path, + 'targets': [feed_path], + 'actions': [(self.site.atom_feed_renderer, (lang, filtered_posts, feed_path, kw['filters'], context))], + 'clean': True, + 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:atom')], + 'task_dep': ['render_posts'], + } + return task + + def _generate_classification_page_as_list(self, taxonomy, classification, filtered_posts, context, kw, lang): + """Render a single flat link list with this classification's posts.""" + kind = taxonomy.classification_name + template_name = "tag.tmpl" + output_name = os.path.join(kw['output_folder'], self.site.path(kind, classification, lang)) + context = copy(context) + context["lang"] = lang + context["posts"] = filtered_posts + context["permalink"] = self.site.link(kind, classification, lang) + context["kind"] = kind + context["pagekind"] = ["list", "tag_page"] + task = self.site.generic_post_list_renderer(lang, filtered_posts, output_name, template_name, kw['filters'], context) + task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:list')] + task['basename'] = str(self.name) + yield task + + if self.site.config['GENERATE_ATOM']: + yield self._generate_classification_page_as_list_atom(kind, taxonomy, classification, filtered_posts, context, kw, lang) + + def _generate_classification_page(self, taxonomy, classification, post_list, lang): + """Render index or post list and associated feeds per classification.""" + # Filter list + if self.site.config["SHOW_UNTRANSLATED_POSTS"]: + filtered_posts = post_list + else: + filtered_posts = [x for x in post_list if x.is_translation_available(lang)] + if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications: + return + # Get data + context, kw = taxonomy.provide_context_and_uptodate(classification) + kw = copy(kw) + kw["blog_title"] = self.site.config["BLOG_TITLE"] + kw["feed_teasers"] = self.site.config["FEED_TEASERS"] + kw["feed_plain"] = self.site.config["FEED_PLAIN"] + kw["feed_link_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"] + kw["feed_length"] = self.site.config['FEED_LENGTH'] + # Generate RSS feed + if kw["generate_rss"]: + yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], kw, lang) + # Render HTML + if taxonomy.show_list_as_index: + yield self._generate_classification_page_as_index(taxonomy, classification, filtered_posts, context, kw, lang) + else: + yield self._generate_classification_page_as_list(taxonomy, classification, filtered_posts, context, kw, lang) + + def gen_tasks(self): + """Render the tag pages and feeds.""" + + self.site.scan_posts() + yield self.group_task() + + for taxonomy in self.site.plugin_manager.getPluginsOfCategory('Taxonomy'): + for lang in self.site.config["TRANSLATIONS"]: + # Generate list of classifications (i.e. classification overview) + if taxonomy.template_for_list_of_one_classification is not None: + 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): + yield task From 1bc2a3113d9a7356218c7e78a574be52498de828 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 12:54:04 +0200 Subject: [PATCH 02/30] Making pydocstyle happy. --- nikola/plugin_categories.py | 36 +++++++++++++++++++------------ nikola/plugins/task/taxonomies.py | 1 - 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 8c87fda8b6..1b0bda002f 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -504,13 +504,14 @@ class Taxonomy(BasePlugin): also_create_classifications_from_other_languages = True def classify(self, post, lang): - """Classifies the given post for the given language. + """Classify the given post for the given language. - Must return a list or tuple of strings.""" + Must return a list or tuple of strings. + """ raise NotImplementedError() - def sort_posts(self, posts): - """Sorts the given list of posts.""" + def sort_posts(self, posts, lang): + """Sort the given list of posts.""" pass def get_list_path(self, lang): @@ -518,7 +519,8 @@ def get_list_path(self, lang): The last element in the returned path must have no extension, and the PRETTY_URLS config must be ignored. The return value will be modified - based on the PRETTY_URLS and INDEX_FILE settings.""" + based on the PRETTY_URLS and INDEX_FILE settings. + """ raise NotImplementedError() def get_path(self, classification, lang): @@ -529,48 +531,54 @@ def get_path(self, classification, lang): based on the PRETTY_URLS and INDEX_FILE settings. For hierarchical taxonomies, the result of extract_hierarchy is provided. - For non-hierarchical taxonomies, the classification string itself is provided.""" + For non-hierarchical taxonomies, the classification string itself is provided. + """ raise NotImplementedError() def extract_hierarchy(self, classification): """Given a classification, return a list of parts in the hierarchy. For non-hierarchical taxonomies, it usually suffices to return - `[classification]`.""" + `[classification]`. + """ return [classification] def recombine_classification_from_hierarchy(self, hierarchy): """Given a list of parts in the hierarchy, return the classification string. - For non-hierarchical taxonomies, it usually suffices to return hierarchy[0].""" + For non-hierarchical taxonomies, it usually suffices to return hierarchy[0]. + """ return hierarchy[0] def provide_list_context_and_uptodate(self): - """Provides data for the context and the uptodate list for the list of all classifiations. + """Provide data for the context and the uptodate list for the list of all classifiations. Must return a tuple of two dicts. The first is merged into the page's context, the second will be put into the uptodate list of all generated tasks. - Context must contain `title`.""" + Context must contain `title`. + """ raise NotImplementedError() def provide_context_and_uptodate(self, classification): - """Provides data for the context and the uptodate list for the list of the given classifiation. + """Provide data for the context and the uptodate list for the list of the given classifiation. Must return a tuple of two dicts. The first is merged into the page's context, the second will be put into the uptodate list of all generated tasks. Context must contain `title`, which should be something like 'Posts about ', - and `classification_title`, which should be related to the classification string.""" + and `classification_title`, which should be related to the classification string. + """ raise NotImplementedError() def postprocess_posts_per_classification(self, posts_per_classification_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): - """This function can rearrange, modify or otherwise use the list of posts per classification and per language. + """Rearrange, modify or otherwise use the list of posts per classification and per language. For compatibility reasons, the list could be stored somewhere else as well. In case `has_hierarchy` is `True`, `flat_hierarchy_per_lang` is the flat hierarchy consisting of `TreeNode` elements, and `hierarchy_lookup_per_lang` is the corresponding hierarchy lookup mapping classification strings to - `TreeNode` objects.""" + `TreeNode` objects. + """ pass diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 5615f987ec..0dcfdeb01b 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -203,7 +203,6 @@ def _generate_classification_page(self, taxonomy, classification, post_list, lan def gen_tasks(self): """Render the tag pages and feeds.""" - self.site.scan_posts() yield self.group_task() From fe859aa3cb4219c28ff55d6958946b49c4eed49f Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:00:22 +0200 Subject: [PATCH 03/30] Improved sorting. --- nikola/plugin_categories.py | 10 +++++++++- nikola/plugins/misc/taxonomies_classifier.py | 5 ++++- nikola/plugins/task/taxonomies.py | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 1b0bda002f..5d012e2276 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -510,10 +510,18 @@ def classify(self, post, lang): """ raise NotImplementedError() - def sort_posts(self, posts, lang): + def sort_posts(self, posts, classification, lang): """Sort the given list of posts.""" pass + def sort_classifications(self, classifications, lang): + """Sort the given list of classification strings. + + For hierarchical taxonomies, the elements of the list are a single + path element of the path returned by extract_hierarchy(). + """ + pass + def get_list_path(self, lang): """A path handler for the list of all classifications. diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 1791698589..5f5e4df899 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -124,6 +124,7 @@ def _do_classification(self): (int(p.meta('priority')) if p.meta('priority') else 0, p.date, p.source_path)) posts.reverse() + taxonomy.sort_posts(posts, classification, lang) posts_per_classification[classification] = posts # Create hierarchy information if taxonomy.has_hierarchy: @@ -150,7 +151,9 @@ def create_hierarchy(cat_hierarchy, parent=None): node.classification_path = [pn.name for pn in node.get_path()] node.classification_name = taxonomy.recombine_classification_from_hierarchy(node.classification_path) hierarchy_lookup[node.classification_name] = node - return natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC) + classifications = natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications) + return classifications root_list = create_hierarchy(hierarchy) flat_hierarchy = utils.flatten_tree_structure(root_list) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 0dcfdeb01b..ec5d0d03d0 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -60,6 +60,7 @@ def _generate_classification_overview(self, taxonomy, lang): classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items() if len(posts) >= kw["minimum_post_count"]], alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications) # Set up classifications in context context[taxonomy.metadata_name] = classifications From 8a5d156c250f18c7f80a485443b3ada0d3d1e9d4 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:00:45 +0200 Subject: [PATCH 04/30] Forgot lang argument. --- nikola/plugins/misc/taxonomies_classifier.py | 2 +- nikola/plugins/task/taxonomies.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 5f5e4df899..30b3b1f4cf 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -152,7 +152,7 @@ def create_hierarchy(cat_hierarchy, parent=None): node.classification_name = taxonomy.recombine_classification_from_hierarchy(node.classification_path) hierarchy_lookup[node.classification_name] = node classifications = natsort.natsorted(result, key=lambda e: e.name, alg=natsort.ns.F | natsort.ns.IC) - taxonomy.sort_classifications(classifications) + taxonomy.sort_classifications(classifications, lang) return classifications root_list = create_hierarchy(hierarchy) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index ec5d0d03d0..63e0b5ee66 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -60,7 +60,7 @@ def _generate_classification_overview(self, taxonomy, lang): classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items() if len(posts) >= kw["minimum_post_count"]], alg=natsort.ns.F | natsort.ns.IC) - taxonomy.sort_classifications(classifications) + taxonomy.sort_classifications(classifications, lang) # Set up classifications in context context[taxonomy.metadata_name] = classifications From 5bd161662dde120465b07fd990eef9aaa4626e4c Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:10:14 +0200 Subject: [PATCH 05/30] Fixing minimum post count for hierarchies. --- nikola/plugins/misc/taxonomies_classifier.py | 3 +++ nikola/plugins/task/taxonomies.py | 28 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 30b3b1f4cf..8e04eeb075 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -112,6 +112,7 @@ def _do_classification(self): sys.exit(1) # Sort everything. + self.site.hierarchy_per_classification = {} self.site.flat_hierarchy_per_classification = {} self.site.hierarchy_lookup_per_classification = {} for taxonomy in taxonomies: @@ -128,6 +129,7 @@ def _do_classification(self): posts_per_classification[classification] = posts # Create hierarchy information if taxonomy.has_hierarchy: + self.site.hierarchy_per_classification[taxonomy.classification_name] = {} self.site.flat_hierarchy_per_classification[taxonomy.classification_name] = {} self.site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {} for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): @@ -158,6 +160,7 @@ def create_hierarchy(cat_hierarchy, parent=None): root_list = create_hierarchy(hierarchy) flat_hierarchy = utils.flatten_tree_structure(root_list) # Store result + self.site.hierarchy_per_classification[taxonomy.classification_name][lang] = root_list self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 63e0b5ee66..d31e49f59c 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -40,6 +40,23 @@ from nikola.nikola import _enclosure +def _clone_treenode(treenode, parent=None, acceptor=lambda x: True): + # Standard TreeNode stuff + node_clone = utils.TreeNode(treenode.name, parent) + node_clone.children = [_clone_treenode(node, parent=node_clone, acceptor=acceptor) for node in treenode.children] + node_clone.children = [node for node in node_clone.children if node] + node_clone.indent_levels = treenode.indent_levels + node_clone.indent_change_before = treenode.indent_change_before + node_clone.indent_change_after = treenode.indent_change_after + # Stuff added by extended_tags_preproces plugin + node_clone.tag_path = treenode.tag_path + node_clone.tag_name = treenode.tag_name + # Accept this node if there are no children (left) and acceptor fails + if not node_clone.children and not acceptor(treenode): + return None + return node_clone + + class RenderTaxonomies(Task): """Render the tag/category pages and feeds.""" @@ -55,7 +72,14 @@ def _generate_classification_overview(self, taxonomy, lang): # Collect all relevant classifications if taxonomy.has_hierarchy: - classifications = [cat.classification_name for cat in self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang]] + def acceptor(node): + return len(self.site.posts_per_classification[taxonomy.classification_name][lang][node.classification_name]) >= kw["minimum_post_count"] + + clipped_root_list = [_clone_treenode(node, parent=None, acceptor=acceptor) for node in self.site.hierarchy_per_classification[taxonomy.classification_name][lang]] + clipped_root_list = [node for node in clipped_root_list if node] + clipped_flat_hierarchy = utils.flatten_tree_structure(clipped_root_list) + + classifications = [cat.classification_name for cat in clipped_flat_hierarchy] else: classifications = natsort.natsorted([tag for tag, posts in self.site.posts_per_classification[taxonomy.classification_name][lang].items() if len(posts) >= kw["minimum_post_count"]], @@ -71,7 +95,7 @@ def _generate_classification_overview(self, taxonomy, lang): self.site.link(taxonomy.classification_name, node.classification_name, lang), node.indent_levels, node.indent_change_before, node.indent_change_after) - for node in self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang]] + for node in clipped_flat_hierarchy] # Prepare rendering context["permalink"] = self.site.link("{}_index".format(taxonomy.classification_name), None, lang) From a6c970827fec91bab6ee7a44f58ba120ab082ffa Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:11:01 +0200 Subject: [PATCH 06/30] Fixing output folder. --- nikola/plugins/task/taxonomies.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index d31e49f59c..b166158a3c 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -100,7 +100,7 @@ def acceptor(node): # Prepare rendering context["permalink"] = self.site.link("{}_index".format(taxonomy.classification_name), None, lang) context["pagekind"] = ["list", "tags_page"] - output_name = os.path.join(kw['output_folder'], self.site.path('{}_index'.format(taxonomy.classification_name), None, lang)) + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(taxonomy.classification_name), None, lang)) task = self.site.generic_post_list_renderer( lang, [], @@ -117,7 +117,7 @@ def _generate_classification_page_as_rss(self, taxonomy, classification, filtere """Create a RSS feed for a single classification in a given language.""" kind = taxonomy.classification_name # Render RSS - output_name = os.path.normpath(os.path.join(kw['output_folder'], self.site.path(kind + "_rss", classification, lang))) + output_name = os.path.normpath(os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind + "_rss", classification, lang))) feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", classification, lang).lstrip('/')) deps = [] deps_uptodate = [] @@ -168,7 +168,7 @@ def _generate_classification_page_as_list_atom(self, taxonomy, classification, f kind = taxonomy.classification_name context = copy(context) context['feedlink'] = self.site.abs_link(self.site.path('{}_atom'.format(kind), classification, lang)) - feed_path = os.path.join(kw['output_folder'], self.site.path('{}_atom'.format(kind), classification, lang)) + feed_path = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_atom'.format(kind), classification, lang)) task = { 'basename': str(self.name), @@ -185,7 +185,7 @@ def _generate_classification_page_as_list(self, taxonomy, classification, filter """Render a single flat link list with this classification's posts.""" kind = taxonomy.classification_name template_name = "tag.tmpl" - output_name = os.path.join(kw['output_folder'], self.site.path(kind, classification, lang)) + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang)) context = copy(context) context["lang"] = lang context["posts"] = filtered_posts From 430173e5790379ca4dfbd0da4de9c1889350e4df Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:13:36 +0200 Subject: [PATCH 07/30] Fixing kws. --- nikola/plugins/task/taxonomies.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index b166158a3c..637d81b688 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -68,6 +68,7 @@ def _generate_classification_overview(self, taxonomy, lang): context = copy(context) kw = copy(kw) + kw['filters'] = self.site.config['FILTERS'] kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview # Collect all relevant classifications @@ -212,7 +213,10 @@ def _generate_classification_page(self, taxonomy, classification, post_list, lan # Get data context, kw = taxonomy.provide_context_and_uptodate(classification) kw = copy(kw) - kw["blog_title"] = self.site.config["BLOG_TITLE"] + kw['filters'] = self.site.config['FILTERS'] + kw['site_url'] = self.site.config['SITE_URL'] + kw['blog_title'] = self.site.config['BLOG_TITLE'] + kw['generate_rss'] = self.site.config['GENERATE_RSS'] kw["feed_teasers"] = self.site.config["FEED_TEASERS"] kw["feed_plain"] = self.site.config["FEED_PLAIN"] kw["feed_link_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"] From 7899a3ce6b472151188ba1c132b6de9dd769ee77 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:18:44 +0200 Subject: [PATCH 08/30] Improved documentation. --- nikola/plugin_categories.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 5d012e2276..c6de1d6a40 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -511,7 +511,10 @@ def classify(self, post, lang): raise NotImplementedError() def sort_posts(self, posts, classification, lang): - """Sort the given list of posts.""" + """Sort the given list of posts. + + The sort must happen in-place. + """ pass def sort_classifications(self, classifications, lang): @@ -519,6 +522,8 @@ def sort_classifications(self, classifications, lang): For hierarchical taxonomies, the elements of the list are a single path element of the path returned by extract_hierarchy(). + + The sort must happen in-place. """ pass @@ -585,8 +590,8 @@ def postprocess_posts_per_classification(self, posts_per_classification_per_lang For compatibility reasons, the list could be stored somewhere else as well. In case `has_hierarchy` is `True`, `flat_hierarchy_per_lang` is the flat - hierarchy consisting of `TreeNode` elements, and `hierarchy_lookup_per_lang` + hierarchy consisting of `utils.TreeNode` elements, and `hierarchy_lookup_per_lang` is the corresponding hierarchy lookup mapping classification strings to - `TreeNode` objects. + `utils.TreeNode` objects. """ pass From 2d0d6c6a71724fd3e4828adca08672aafac7673e Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:20:42 +0200 Subject: [PATCH 09/30] Forgot postprocessing... --- nikola/plugins/misc/taxonomies_classifier.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 8e04eeb075..97c604eb4e 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -169,6 +169,15 @@ def create_hierarchy(cat_hierarchy, parent=None): else: taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], flat_hierarchy, hierarchy_lookup) + # Postprocessing + for taxonomy in taxonomies: + for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + taxonomy.postprocess_posts_per_classification( + posts_per_classification, + self.site.flat_hierarchy_per_classification.get(taxonomy.classification_name, {}).get(lang, None), + self.site.hierarchy_lookup_per_classification.get(taxonomy.classification_name, {}).get(lang, None), + ) + def _postprocess_path(self, path, lang, force_extension=None): if force_extension is not None: if len(path) == 0: From 9f3ad9bcaf03bf1a4c7c97b881549cf29a479acd Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 13:30:05 +0200 Subject: [PATCH 10/30] Forgot some changes. --- .../plugins/misc/taxonomies_classifier.plugin | 2 +- nikola/plugins/misc/taxonomies_classifier.py | 62 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.plugin b/nikola/plugins/misc/taxonomies_classifier.plugin index e7546f30a9..aeec68bf8c 100644 --- a/nikola/plugins/misc/taxonomies_classifier.plugin +++ b/nikola/plugins/misc/taxonomies_classifier.plugin @@ -1,6 +1,6 @@ [Core] name = classify_taxonomies -module = classify_taxonomies +module = taxonomies_classifier [Documentation] author = Roberto Alsina diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 97c604eb4e..29eb6c1985 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -43,22 +43,22 @@ class TaxonomiesClassifier(SignalHandler): name = "render_taxonomies" - def _do_classification(self): - taxonomies = self.site.plugin_manager.getPluginsOfCategory('Taxonomy') - self.site.posts_per_classification = {} + def _do_classification(self, site): + taxonomies = site.plugin_manager.getPluginsOfCategory('Taxonomy') + site.posts_per_classification = {} for taxonomy in taxonomies: - if taxonomy.classification_name in self.site.posts_per_classification: + if taxonomy.classification_name in site.posts_per_classification: raise Exception("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name)) - self.site.posts_per_classification[taxonomy.classification_name] = { + site.posts_per_classification[taxonomy.classification_name] = { lang: defaultdict(set) for lang in self.config['TRANSLATIONS'].keys() } # Classify posts - for post in self.timeline: + for post in site.timeline: for taxonomy in taxonomies: if taxonomy.apply_to_posts if post.is_post else taxonomy.apply_to_pages: classifications = {} - for lang in self.config['TRANSLATIONS'].keys(): + for lang in site.config['TRANSLATIONS'].keys(): # Extract classifications for this language classifications[lang] = taxonomy.classify(post, lang) assert taxonomy.more_than_one_classifications_per_post or len(classifications[lang]) <= 1 @@ -70,21 +70,21 @@ def _do_classification(self): # Add post to sets for classification in classifications[lang]: while classification: - self.site.posts_per_classification[taxonomy.classification_name][lang][classification].add(post) + site.posts_per_classification[taxonomy.classification_name][lang][classification].add(post) if not taxonomy.include_posts_from_subhierarchies or not taxonomy.has_hierarchy: break classification = taxonomy.recombine_classification_from_hierarchy(taxonomy.extract_hierarchy(classification)[:-1]) # Check for valid paths and for collisions - taxonomy_outputs = {lang: dict() for lang in self.config['TRANSLATIONS'].keys()} + taxonomy_outputs = {lang: dict() for lang in site.config['TRANSLATIONS'].keys()} quit = False for taxonomy in taxonomies: # Check for collisions (per language) - for lang in self.config['TRANSLATIONS'].keys(): - for tlang in self.config['TRANSLATIONS'].keys(): + for lang in site.config['TRANSLATIONS'].keys(): + for tlang in site.config['TRANSLATIONS'].keys(): if lang != tlang and not taxonomy.also_create_classifications_from_other_languages: continue - for classification, posts in self.site.posts_per_classification[taxonomy.classification_name][tlang].items(): + for classification, posts in site.posts_per_classification[taxonomy.classification_name][tlang].items(): # Obtain path as tuple if taxonomy.has_hierarchy: path = taxonomy.get_path(taxonomy.extract_hierarchy(classification), lang) @@ -112,12 +112,12 @@ def _do_classification(self): sys.exit(1) # Sort everything. - self.site.hierarchy_per_classification = {} - self.site.flat_hierarchy_per_classification = {} - self.site.hierarchy_lookup_per_classification = {} + site.hierarchy_per_classification = {} + site.flat_hierarchy_per_classification = {} + site.hierarchy_lookup_per_classification = {} for taxonomy in taxonomies: # Sort post lists - for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): # Convert sets to lists and sort them for classification in list(posts_per_classification.keys()): posts = list(posts_per_classification[classification]) @@ -129,10 +129,10 @@ def _do_classification(self): posts_per_classification[classification] = posts # Create hierarchy information if taxonomy.has_hierarchy: - self.site.hierarchy_per_classification[taxonomy.classification_name] = {} - self.site.flat_hierarchy_per_classification[taxonomy.classification_name] = {} - self.site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {} - for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + site.hierarchy_per_classification[taxonomy.classification_name] = {} + site.flat_hierarchy_per_classification[taxonomy.classification_name] = {} + site.hierarchy_lookup_per_classification[taxonomy.classification_name] = {} + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): # Compose hierarchy hierarchy = {} for classification in posts_per_classification.keys(): @@ -160,22 +160,22 @@ def create_hierarchy(cat_hierarchy, parent=None): root_list = create_hierarchy(hierarchy) flat_hierarchy = utils.flatten_tree_structure(root_list) # Store result - self.site.hierarchy_per_classification[taxonomy.classification_name][lang] = root_list - self.site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy - self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup - taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], - self.site.flat_hierarchy_per_classification[taxonomy.classification_name], - self.site.hierarchy_lookup_per_classification[taxonomy.classification_name]) + site.hierarchy_per_classification[taxonomy.classification_name][lang] = root_list + site.flat_hierarchy_per_classification[taxonomy.classification_name][lang] = flat_hierarchy + site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang] = hierarchy_lookup + taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name], + site.flat_hierarchy_per_classification[taxonomy.classification_name], + site.hierarchy_lookup_per_classification[taxonomy.classification_name]) else: - taxonomy.postprocess_posts_per_classification(self.site.posts_per_classification[taxonomy.classification_name], flat_hierarchy, hierarchy_lookup) + taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name], flat_hierarchy, hierarchy_lookup) # Postprocessing for taxonomy in taxonomies: - for lang, posts_per_classification in self.site.posts_per_classification[taxonomy.classification_name].items(): + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): taxonomy.postprocess_posts_per_classification( posts_per_classification, - self.site.flat_hierarchy_per_classification.get(taxonomy.classification_name, {}).get(lang, None), - self.site.hierarchy_lookup_per_classification.get(taxonomy.classification_name, {}).get(lang, None), + site.flat_hierarchy_per_classification.get(taxonomy.classification_name, {}).get(lang, None), + site.hierarchy_lookup_per_classification.get(taxonomy.classification_name, {}).get(lang, None), ) def _postprocess_path(self, path, lang, force_extension=None): @@ -221,5 +221,5 @@ def set_site(self, site): # Add hook for after post scanning blinker.signal("scanned").connect(self._do_classification) # Register path handlers - for taxonomy in self.plugin_manager.getPluginsOfCategory('Taxonomy'): + for taxonomy in self.site.plugin_manager.getPluginsOfCategory('Taxonomy'): self._register_path_handlers(taxonomy) From 959160a97237788fe7cc45c2a74d84b9df2e80a7 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 16:55:19 +0200 Subject: [PATCH 11/30] Allowing to disable taxonomies. --- nikola/plugin_categories.py | 8 ++++++++ nikola/plugins/task/taxonomies.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index c6de1d6a40..2a68a7d84a 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -503,6 +503,14 @@ class Taxonomy(BasePlugin): # pages. also_create_classifications_from_other_languages = True + def is_enabled(self): + """Return True if this taxonomy is enabled, or False otherwise. + + Enabled means that the overview page and the classification lists + are created. + """ + return True + def classify(self, post, lang): """Classify the given post for the given language. diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 637d81b688..19b8a16096 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -236,6 +236,9 @@ def gen_tasks(self): yield self.group_task() for taxonomy in self.site.plugin_manager.getPluginsOfCategory('Taxonomy'): + # Should this taxonomy be considered after all? + if not taxonomy.is_enabled(): + continue for lang in self.site.config["TRANSLATIONS"]: # Generate list of classifications (i.e. classification overview) if taxonomy.template_for_list_of_one_classification is not None: From b0a492d6843af82295d535923c45eb7a31e508e6 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 17:07:06 +0200 Subject: [PATCH 12/30] Forgot more lang args. --- nikola/plugin_categories.py | 4 ++-- nikola/plugins/task/taxonomies.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 2a68a7d84a..02a2abd0ac 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -571,7 +571,7 @@ def recombine_classification_from_hierarchy(self, hierarchy): """ return hierarchy[0] - def provide_list_context_and_uptodate(self): + def provide_list_context_and_uptodate(self, lang): """Provide data for the context and the uptodate list for the list of all classifiations. Must return a tuple of two dicts. The first is merged into the page's context, @@ -581,7 +581,7 @@ def provide_list_context_and_uptodate(self): """ raise NotImplementedError() - def provide_context_and_uptodate(self, classification): + def provide_context_and_uptodate(self, classification, lang): """Provide data for the context and the uptodate list for the list of the given classifiation. Must return a tuple of two dicts. The first is merged into the page's context, diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 19b8a16096..7091569bd7 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -211,7 +211,7 @@ def _generate_classification_page(self, taxonomy, classification, post_list, lan if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications: return # Get data - context, kw = taxonomy.provide_context_and_uptodate(classification) + context, kw = taxonomy.provide_context_and_uptodate(classification, lang) kw = copy(kw) kw['filters'] = self.site.config['FILTERS'] kw['site_url'] = self.site.config['SITE_URL'] From 7f6b13e7cf2abeeb6b59a5b2f34919b6e0b86a69 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 17:08:28 +0200 Subject: [PATCH 13/30] Only setting pagekind in context when it wasn't set before. --- nikola/plugins/task/taxonomies.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 7091569bd7..eb09d88c79 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -100,7 +100,8 @@ def acceptor(node): # Prepare rendering context["permalink"] = self.site.link("{}_index".format(taxonomy.classification_name), None, lang) - context["pagekind"] = ["list", "tags_page"] + if "pagekind" not in context: + context["pagekind"] = ["list", "tags_page"] output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path('{}_index'.format(taxonomy.classification_name), None, lang)) task = self.site.generic_post_list_renderer( lang, @@ -159,7 +160,8 @@ def page_path(i, displayed_i, num_pages, force_addition, extension=None): rss_link = ("""""".format( taxonomy.classification_name, context['classification_title'], lang, self.site.link('{}_rss'.format(kind), classification, lang))) context['rss_link'] = rss_link - context["pagekind"] = ["index", "tag_page"] + if "pagekind" not in context: + context["pagekind"] = ["index", "tag_page"] template_name = taxonomy.template_for_list_of_one_classification yield self.site.generic_index_renderer(lang, filtered_posts, context['title'], template_name, context, kw, str(self.name), page_link, page_path) @@ -192,7 +194,8 @@ def _generate_classification_page_as_list(self, taxonomy, classification, filter context["posts"] = filtered_posts context["permalink"] = self.site.link(kind, classification, lang) context["kind"] = kind - context["pagekind"] = ["list", "tag_page"] + if "pagekind" not in context: + context["pagekind"] = ["list", "tag_page"] task = self.site.generic_post_list_renderer(lang, filtered_posts, output_name, template_name, kw['filters'], context) task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.taxonomies:list')] task['basename'] = str(self.name) From 5ff9f4a11a675f45c0b3eb94e6bd04eb60f0ede9 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 17:19:43 +0200 Subject: [PATCH 14/30] Made plugin more flexible. --- nikola/plugin_categories.py | 3 +++ nikola/plugins/task/taxonomies.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 02a2abd0ac..5f2d7fcc58 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -470,6 +470,9 @@ class Taxonomy(BasePlugin): # The classification name to be used when storing the classification # in the metadata. metadata_name = "taxonomy" + # Variable for the overview page template which contains the list of + # classifications. + overview_page_variable_name = "taxonomy" # If True, there can be more than one classification per post; in that case, # the classification data in the metadata is stored as a list. If False, # the classification data in the metadata is stored as a string, or None diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index eb09d88c79..a8f8f1b71d 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -88,7 +88,7 @@ def acceptor(node): taxonomy.sort_classifications(classifications, lang) # Set up classifications in context - context[taxonomy.metadata_name] = classifications + context[taxonomy.overview_page_variable_name] = classifications context["items"] = [(classification, self.site.link(taxonomy.classification_name, classification, lang)) for classification in classifications] context["has_hierarchy"] = taxonomy.has_hierarchy if taxonomy.has_hierarchy: From f6c86a232dc11b285ea450959ce17db79175d105 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 17:37:05 +0200 Subject: [PATCH 15/30] Allowing to force complete path. --- nikola/plugin_categories.py | 25 ++++++++++++------ nikola/plugins/misc/taxonomies_classifier.py | 27 ++++++++++---------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 5f2d7fcc58..59e074c5ec 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -541,21 +541,30 @@ def sort_classifications(self, classifications, lang): def get_list_path(self, lang): """A path handler for the list of all classifications. - The last element in the returned path must have no extension, and the - PRETTY_URLS config must be ignored. The return value will be modified - based on the PRETTY_URLS and INDEX_FILE settings. + Must return a list or tuple, together with a boolean indicating + whether INDEX_FILE should always be added (True) or not (False). + + In case INDEX_FILE should not be added, the last element in the returned + path must have no extension, and the PRETTY_URLS config must be ignored + by this handler. The return value will be modified based on the + PRETTY_URLS and INDEX_FILE settings. """ raise NotImplementedError() def get_path(self, classification, lang): """A path handler for the given classification. - The last element in the returned path must have no extension, and the - PRETTY_URLS config must be ignored. The return value will be modified - based on the PRETTY_URLS and INDEX_FILE settings. + Must return a list or tuple, together with a boolean indicating + whether INDEX_FILE should always be added (True) or not (False). + + In case INDEX_FILE should not be added, the last element in the returned + path must have no extension, and the PRETTY_URLS config must be ignored + by this handler. The return value will be modified based on the + PRETTY_URLS and INDEX_FILE settings. - For hierarchical taxonomies, the result of extract_hierarchy is provided. - For non-hierarchical taxonomies, the classification string itself is provided. + For hierarchical taxonomies, the result of extract_hierarchy is provided + as `classification`. For non-hierarchical taxonomies, the classification + string itself is provided as `classification`. """ raise NotImplementedError() diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 29eb6c1985..794514d4ca 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -86,21 +86,19 @@ def _do_classification(self, site): continue for classification, posts in site.posts_per_classification[taxonomy.classification_name][tlang].items(): # Obtain path as tuple - if taxonomy.has_hierarchy: - path = taxonomy.get_path(taxonomy.extract_hierarchy(classification), lang) - else: - path = taxonomy.get_path(classification, lang) - path = tuple(path) + path = self.site.path_handlers[taxonomy.classification_name](classification, lang) # Check that path is OK for path_element in path: if len(path_element) == 0: utils.LOGGER.error("{0} {1} yields invalid path '{2}'!".format(taxonomy.classification_name.title(), classification, '/'.join(path))) quit = True + # Combine path + path = os.path.join([os.path.normpath(p) for p in path if p != '.']) # Determine collisions if path in taxonomy_outputs[lang]: other_classification_name, other_classification, other_posts = taxonomy_outputs[lang][path] - utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {1} "{2}"'.format( - taxonomy.classification_name, classification, other_classification_name, other_classification)) + utils.LOGGER.error('You have classifications that are too similar: {0} "{1}" and {2} "{3}" both result in output path {4} for langauge {5}.'.format( + taxonomy.classification_name, classification, other_classification_name, other_classification, path, lang)) utils.LOGGER.error('{0} {1} is used in: {1}'.format( taxonomy.classification_name.title(), classification, ', '.join(sorted([p.source_path for p in posts])))) utils.LOGGER.error('{0} {1} is used in: {1}'.format( @@ -178,12 +176,12 @@ def create_hierarchy(cat_hierarchy, parent=None): site.hierarchy_lookup_per_classification.get(taxonomy.classification_name, {}).get(lang, None), ) - def _postprocess_path(self, path, lang, force_extension=None): + def _postprocess_path(self, path, lang, always_append_index=False, force_extension=None): if force_extension is not None: - if len(path) == 0: + if len(path) == 0 or always_append_index: path = [os.path.splitext(self.site.config['INDEX_FILE'])[0]] path[-1] += force_extension - elif self.site.config['PRETTY_URLS'] or len(path) == 0: + elif self.site.config['PRETTY_URLS'] or len(path) == 0 or always_append_index: path = path + [self.site.config['INDEX_FILE']] else: path[-1] += '.html' @@ -191,15 +189,16 @@ def _postprocess_path(self, path, lang, force_extension=None): def _taxonomy_index_path(self, lang, taxonomy): """Return path to the classification overview.""" - return self._postprocess_path(taxonomy.get_list_path(lang), lang) + path, append_index = taxonomy.get_list_path(lang) + return self._postprocess_path(path, lang, always_append_index=append_index) def _taxonomy_path(self, name, lang, taxonomy, force_extension=None): """Return path to a classification.""" if taxonomy.has_hirearchy: - path = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang) + path, append_index = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang) else: - path = taxonomy.get_path(name, lang) - return self._postprocess_path(path, lang, force_extension=force_extension) + path, append_index = taxonomy.get_path(name, lang) + return self._postprocess_path(path, lang, always_append_index=append_index, force_extension=force_extension) def _taxonomy_atom_path(self, name, lang, taxonomy): """Return path to a classification Atom feed.""" From 0c31b3f7e1d2524fa83755c9f492eb3ad16000bb Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 17:42:18 +0200 Subject: [PATCH 16/30] Rewrote authors plugin as a Taxonomy plugin. --- nikola/plugins/task/authors.py | 348 +++++++-------------------------- 1 file changed, 68 insertions(+), 280 deletions(-) diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index ec618002b3..09cc07b07b 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -27,300 +27,88 @@ """Render the author pages and feeds.""" from __future__ import unicode_literals -import os -import natsort -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA -from collections import defaultdict -from blinker import signal - -from nikola.plugin_categories import Task +from nikola.plugin_categories import Taxonomy from nikola import utils -class RenderAuthors(Task): +class ClassifyAuthors(Taxonomy): """Render the author pages and feeds.""" - name = "render_authors" - posts_per_author = None + name = "classify_authors" + + classification_name = "author" + metadata_name = "author" + overview_page_variable_name = "authors" + more_than_one_classifications_per_post = False + has_hierarchy = False + template_for_classification_overview = "authors.tmpl" + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = False + also_create_classifications_from_other_languages = False def set_site(self, site): """Set Nikola site.""" - self.generate_author_pages = False - if site.config["ENABLE_AUTHOR_PAGES"]: - site.register_path_handler('author_index', self.author_index_path) - site.register_path_handler('author', self.author_path) - site.register_path_handler('author_atom', self.author_atom_path) - site.register_path_handler('author_rss', self.author_rss_path) - signal('scanned').connect(self.posts_scanned) - return super(RenderAuthors, self).set_site(site) - - def posts_scanned(self, event): - """Called after posts are scanned via signal.""" - self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and len(self._posts_per_author()) > 1 - self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages - - def gen_tasks(self): - """Render the author pages and feeds.""" - kw = { - "translations": self.site.config["TRANSLATIONS"], - "blog_title": self.site.config["BLOG_TITLE"], - "site_url": self.site.config["SITE_URL"], - "base_url": self.site.config["BASE_URL"], - "messages": self.site.MESSAGES, - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - 'author_path': self.site.config['AUTHOR_PATH'], - "author_pages_are_indexes": self.site.config['AUTHOR_PAGES_ARE_INDEXES'], - "generate_rss": self.site.config['GENERATE_RSS'], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "feed_link_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "feed_length": self.site.config['FEED_LENGTH'], - "tzinfo": self.site.tzinfo, - "pretty_urls": self.site.config['PRETTY_URLS'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "index_file": self.site.config['INDEX_FILE'], - } - - self.site.scan_posts() - yield self.group_task() - - if self.generate_author_pages: - yield self.list_authors_page(kw) - - if not self._posts_per_author(): # this may be self.site.posts_per_author - return - - author_list = list(self._posts_per_author().items()) - - def render_lists(author, posts): - """Render author pages as RSS files and lists/indexes.""" - post_list = sorted(posts, key=lambda a: a.date) - post_list.reverse() - for lang in kw["translations"]: - if kw["show_untranslated_posts"]: - filtered_posts = post_list - else: - filtered_posts = [x for x in post_list if x.is_translation_available(lang)] - if kw["generate_rss"]: - yield self.author_rss(author, lang, filtered_posts, kw) - # Render HTML - if kw['author_pages_are_indexes']: - yield self.author_page_as_index(author, lang, filtered_posts, kw) - else: - yield self.author_page_as_list(author, lang, filtered_posts, kw) - - for author, posts in author_list: - for task in render_lists(author, posts): - yield task - - def _create_authors_page(self, kw): - """Create a global "all authors" page for each language.""" - template_name = "authors.tmpl" - kw = kw.copy() - for lang in kw["translations"]: - authors = natsort.natsorted([author for author in self._posts_per_author().keys()], - alg=natsort.ns.F | natsort.ns.IC) - has_authors = (authors != []) - kw['authors'] = authors - output_name = os.path.join( - kw['output_folder'], self.site.path('author_index', None, lang)) - context = {} - if has_authors: - context["title"] = kw["messages"][lang]["Authors"] - context["items"] = [(author, self.site.link("author", author, lang)) for author - in authors] - context["description"] = context["title"] - else: - context["items"] = None - context["permalink"] = self.site.link("author_index", None, lang) - context["pagekind"] = ["list", "authors_page"] - task = self.site.generic_post_list_renderer( - lang, - [], - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:page')] - task['basename'] = str(self.name) - yield task - - def list_authors_page(self, kw): - """Create a global "all authors" page for each language.""" - yield self._create_authors_page(kw) - - def _get_title(self, author): - return author - - def _get_description(self, author, lang): - descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS'] - return descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None - - def author_page_as_index(self, author, lang, post_list, kw): - """Render a sort of index page collection using only this author's posts.""" - kind = "author" - - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension) - - def page_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path(kind + feed, author, lang), i, displayed_i, lang, self.site, force_addition, extension) - - context_source = {} - title = self._get_title(author) - if kw["generate_rss"]: - # On a author page, the feeds include the author's feeds - rss_link = ("""""".format( - title, lang, self.site.link(kind + "_rss", author, lang))) - context_source['rss_link'] = rss_link - context_source["author"] = title - indexes_title = kw["messages"][lang]["Posts by %s"] % title - context_source["description"] = self._get_description(author, lang) - context_source["pagekind"] = ["index", "author_page"] - template_name = "authorindex.tmpl" + self.show_list_as_index = site.config['AUTHOR_PAGES_ARE_INDEXES'] + self.template_for_list_of_one_classification = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl" + return super(ClassifyAuthors, self).set_site(site) - yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path) + def is_enabled(self): + """Return True if this taxonomy is enabled, or False otherwise.""" + return self.site.config["ENABLE_AUTHOR_PAGES"] - def author_page_as_list(self, author, lang, post_list, kw): - """Render a single flat link list with this author's posts.""" - kind = "author" - template_name = "author.tmpl" - output_name = os.path.join(kw['output_folder'], self.site.path( - kind, author, lang)) - context = {} - context["lang"] = lang - title = self._get_title(author) - context["author"] = title - context["title"] = kw["messages"][lang]["Posts by %s"] % title - context["posts"] = post_list - context["permalink"] = self.site.link(kind, author, lang) - context["kind"] = kind - context["description"] = self._get_description(author, lang) - context["pagekind"] = ["list", "author_page"] - task = self.site.generic_post_list_renderer( - lang, - post_list, - output_name, - template_name, - kw['filters'], - context, - ) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.authors:list')] - task['basename'] = str(self.name) - yield task + def classify(self, post, lang): + """Classify the given post for the given language.""" + return [post.author()] - def author_rss(self, author, lang, posts, kw): - """Create a RSS feed for a single author in a given language.""" - kind = "author" - # Render RSS - output_name = os.path.normpath( - os.path.join(kw['output_folder'], - self.site.path(kind + "_rss", author, lang))) - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link(kind + "_rss", author, lang).lstrip('/')) - deps = [] - deps_uptodate = [] - post_list = sorted(posts, key=lambda a: a.date) - post_list.reverse() - for post in post_list: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) - task = { - 'basename': str(self.name), - 'name': output_name, - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(author)), - kw["site_url"], None, post_list, - output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], - feed_url, None, kw["feed_link_append_query"]))], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.authors:rss')] + deps_uptodate, - 'task_dep': ['render_posts'], - } - return utils.apply_filters(task, kw['filters']) + def get_list_path(self, lang): + """A path handler for the list of all classifications.""" + return [self.site.config['AUTHOR_PATH']], True - def slugify_author_name(self, name, lang=None): - """Slugify an author name.""" - if lang is None: # TODO: remove in v8 - utils.LOGGER.warn("RenderAuthors.slugify_author_name() called without language!") - lang = '' + def get_path(self, author, lang): + """A path handler for the given classification.""" if self.site.config['SLUG_AUTHOR_PATH']: - name = utils.slugify(name, lang) - return name - - def author_index_path(self, name, lang): - """Link to the author's index. - - Example: - - link://authors/ => /authors/index.html - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.site.config['INDEX_FILE']] if _f] - - def author_path(self, name, lang): - """Link to an author's page. - - Example: - - link://author/joe => /authors/joe.html - """ - if self.site.config['PRETTY_URLS']: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.slugify_author_name(name, lang), - self.site.config['INDEX_FILE']] if _f] + slug = utils.slugify(author, lang) else: - return [_f for _f in [ - self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], - self.slugify_author_name(name, lang) + ".html"] if _f] - - def author_atom_path(self, name, lang): - """Link to an author's Atom feed. - - Example: - - link://author_atom/joe => /authors/joe.atom - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".atom"] if - _f] - - def author_rss_path(self, name, lang): - """Link to an author's RSS feed. - - Example: + slug = author + return [self.site.config['AUTHOR_PATH'], slug], False - link://author_rss/joe => /authors/joe.rss - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['AUTHOR_PATH'], self.slugify_author_name(name, lang) + ".xml"] if - _f] - - def _add_extension(self, path, extension): - path[-1] += extension - return path + def provide_list_context_and_uptodate(self, lang): + """Provide data for the context and the uptodate list for the list of all classifiations.""" + kw = { + "messages": self.site.MESSAGES, + } + context = { + "title": kw["messages"][lang]["Authors"], + "description": kw["messages"][lang]["Authors"], + "permalink": self.site.link("author_index", None, lang), + "pagekind": ["list", "authors_page"], + } + kw.update(context) + return context, kw - def _posts_per_author(self): - """Return a dict of posts per author.""" - if self.posts_per_author is None: - self.posts_per_author = defaultdict(list) - for post in self.site.timeline: - if post.is_post: - self.posts_per_author[post.author()].append(post) - return self.posts_per_author + def provide_context_and_uptodate(self, author, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + descriptions = self.site.config['AUTHOR_PAGES_DESCRIPTIONS'] + kw = { + "messages": self.site.MESSAGES, + } + context = { + "title": kw["messages"][lang]["Posts by %s"] % author, + "classification_title": author, + "description": descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None, + "pagekind": ["index" if self.show_list_as_index else "list", "author_page"], + } + kw.update(context) + return context, kw + + def postprocess_posts_per_classification(self, posts_per_author_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + more_than_one = False + for lang, posts_per_author in posts_per_author_per_language.item(): + if len(posts_per_author.keys()) > 1: + more_than_one = True + self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one + self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages From c02fba4c36c2c235f3be3bbbaf35390c7975c529 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:18:24 +0200 Subject: [PATCH 17/30] Improving path creation. --- nikola/plugin_categories.py | 8 ++++++-- nikola/plugins/misc/taxonomies_classifier.py | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 59e074c5ec..536c3d3048 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -538,7 +538,7 @@ def sort_classifications(self, classifications, lang): """ pass - def get_list_path(self, lang): + def get_list_path(self, lang, type='page'): """A path handler for the list of all classifications. Must return a list or tuple, together with a boolean indicating @@ -548,10 +548,12 @@ def get_list_path(self, lang): path must have no extension, and the PRETTY_URLS config must be ignored by this handler. The return value will be modified based on the PRETTY_URLS and INDEX_FILE settings. + + Type can be either 'page', 'feed' (for Atom feed) or 'rss'. """ raise NotImplementedError() - def get_path(self, classification, lang): + def get_path(self, classification, lang, type='page'): """A path handler for the given classification. Must return a list or tuple, together with a boolean indicating @@ -562,6 +564,8 @@ def get_path(self, classification, lang): by this handler. The return value will be modified based on the PRETTY_URLS and INDEX_FILE settings. + Type can be either 'page', 'feed' (for Atom feed) or 'rss'. + For hierarchical taxonomies, the result of extract_hierarchy is provided as `classification`. For non-hierarchical taxonomies, the classification string itself is provided as `classification`. diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 794514d4ca..6e63eccef1 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -177,9 +177,15 @@ def create_hierarchy(cat_hierarchy, parent=None): ) def _postprocess_path(self, path, lang, always_append_index=False, force_extension=None): + if type == 'feed': + force_extension = '.atom' + elif type == 'rss': + force_extension = '.xml' if force_extension is not None: if len(path) == 0 or always_append_index: path = [os.path.splitext(self.site.config['INDEX_FILE'])[0]] + if type == 'rss': + path = ['rss'] path[-1] += force_extension elif self.site.config['PRETTY_URLS'] or len(path) == 0 or always_append_index: path = path + [self.site.config['INDEX_FILE']] @@ -192,21 +198,21 @@ def _taxonomy_index_path(self, lang, taxonomy): path, append_index = taxonomy.get_list_path(lang) return self._postprocess_path(path, lang, always_append_index=append_index) - def _taxonomy_path(self, name, lang, taxonomy, force_extension=None): + def _taxonomy_path(self, name, lang, taxonomy, force_extension=None, type='page'): """Return path to a classification.""" if taxonomy.has_hirearchy: - path, append_index = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang) + path, append_index = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang, type=type) else: - path, append_index = taxonomy.get_path(name, lang) - return self._postprocess_path(path, lang, always_append_index=append_index, force_extension=force_extension) + path, append_index = taxonomy.get_path(name, lang, type=type) + return self._postprocess_path(path, lang, always_append_index=append_index, force_extension=force_extension, type=type) def _taxonomy_atom_path(self, name, lang, taxonomy): """Return path to a classification Atom feed.""" - return self._taxonomy_path(name, lang, taxonomy, force_extension='.atom') + return self._taxonomy_path(name, lang, taxonomy, type='feed') def _taxonomy_rss_path(self, name, lang, taxonomy): """Return path to a classification RSS feed.""" - return self._taxonomy_path(name, lang, taxonomy, force_extension='.xml') + return self._taxonomy_path(name, lang, taxonomy, type='rss') def _register_path_handlers(self, taxonomy): self.site.register_path_handler('{0}_index'.format(taxonomy.classification_name), lambda name, lang: self._tag_index_path(lang, taxonomy)) From 807da56a2a156dd686710928100e057c42545d4e Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:18:30 +0200 Subject: [PATCH 18/30] Fixing metadata. --- nikola/plugins/task/authors.plugin | 2 +- nikola/plugins/task/authors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin index 3fc4ef27e9..32834527a3 100644 --- a/nikola/plugins/task/authors.plugin +++ b/nikola/plugins/task/authors.plugin @@ -1,5 +1,5 @@ [Core] -Name = render_authors +Name = classify_authors Module = authors [Documentation] diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 09cc07b07b..37f4da7769 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -33,7 +33,7 @@ class ClassifyAuthors(Taxonomy): - """Render the author pages and feeds.""" + """Classify the posts by authors.""" name = "classify_authors" From ac24eb915bf3e0522dcf701da951791ef6e77c3e Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:20:07 +0200 Subject: [PATCH 19/30] Moving section taxonomy into own plugin. --- nikola/plugins/task/indexes.py | 140 ---------------------------- nikola/plugins/task/sections.plugin | 13 +++ nikola/plugins/task/sections.py | 102 ++++++++++++++++++++ 3 files changed, 115 insertions(+), 140 deletions(-) create mode 100644 nikola/plugins/task/sections.plugin create mode 100644 nikola/plugins/task/sections.py diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index 8ecd1decd1..e9bd0456a3 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -47,12 +47,8 @@ class Indexes(Task): def set_site(self, site): """Set Nikola site.""" self.number_of_pages = dict() - self.number_of_pages_section = {lang: dict() for lang in site.config['TRANSLATIONS']} site.register_path_handler('index', self.index_path) site.register_path_handler('index_atom', self.index_atom_path) - site.register_path_handler('section_index', self.index_section_path) - site.register_path_handler('section_index_atom', self.index_section_atom_path) - site.register_path_handler('section_index_rss', self.index_section_rss_path) return super(Indexes, self).set_site(site) def _get_filtered_posts(self, lang, show_untranslated_posts): @@ -116,93 +112,6 @@ def page_path(i, displayed_i, num_pages, force_addition, extension=None): yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path) - if self.site.config['POSTS_SECTIONS']: - index_len = len(kw['index_file']) - - groups = defaultdict(list) - for p in filtered_posts: - groups[p.section_slug(lang)].append(p) - - # don't build sections when there is only one, aka. default setups - if not len(groups.items()) > 1: - continue - - for section_slug, post_list in groups.items(): - self.number_of_pages_section[lang][section_slug] = self._compute_number_of_pages(post_list, kw['index_display_post_count']) - - def cat_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_link(self.site.link("section_index" + feed, section_slug, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - def cat_path(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return utils.adjust_name_for_index_path(self.site.path("section_index" + feed, section_slug, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - context = {} - - short_destination = os.path.join(section_slug, kw['index_file']) - link = short_destination.replace('\\', '/') - if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']: - link = link[:-index_len] - context["permalink"] = link - context["pagekind"] = ["section_page"] - context["description"] = self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang)[section_slug] if section_slug in self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang) else "" - - if self.site.config["POSTS_SECTION_ARE_INDEXES"]: - context["pagekind"].append("index") - posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang) - - section_title = None - if type(posts_section_title) is dict: - if section_slug in posts_section_title: - section_title = posts_section_title[section_slug] - elif type(posts_section_title) is str: - section_title = posts_section_title - if not section_title: - section_title = post_list[0].section_name(lang) - section_title = section_title.format(name=post_list[0].section_name(lang)) - - task = self.site.generic_index_renderer(lang, post_list, section_title, "sectionindex.tmpl", context, kw, self.name, cat_link, cat_path) - else: - context["pagekind"].append("list") - output_name = os.path.join(kw['output_folder'], section_slug, kw['index_file']) - task = self.site.generic_post_list_renderer(lang, post_list, output_name, "list.tmpl", kw['filters'], context) - task['uptodate'] = [utils.config_changed(kw, 'nikola.plugins.task.indexes')] - task['basename'] = self.name - yield task - - # RSS feed for section - deps = [] - deps_uptodate = [] - if kw["show_untranslated_posts"]: - posts = post_list[:kw['feed_length']] - else: - posts = [x for x in post_list if x.is_translation_available(lang)][:kw['feed_length']] - for post in posts: - deps += post.deps(lang) - deps_uptodate += post.deps_uptodate(lang) - - feed_url = urljoin(self.site.config['BASE_URL'], self.site.link('section_index_rss', section_slug, lang).lstrip('/')) - output_name = os.path.join(kw['output_folder'], self.site.path('section_index_rss', section_slug, lang).lstrip(os.sep)) - task = { - 'basename': self.name, - 'name': os.path.normpath(output_name), - 'file_dep': deps, - 'targets': [output_name], - 'actions': [(utils.generic_rss_renderer, - (lang, kw["blog_title"](lang), kw["site_url"], - context["description"], posts, output_name, - kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], feed_url, - _enclosure, kw["feed_links_append_query"]))], - - 'task_dep': ['render_posts'], - 'clean': True, - 'uptodate': [utils.config_changed(kw, 'nikola.plugins.indexes')] + deps_uptodate, - } - yield task - if not self.site.config["PAGE_INDEX"]: return kw = { @@ -287,37 +196,6 @@ def index_path(self, name, lang, is_feed=False): self.site, extension=extension) - def index_section_path(self, name, lang, is_feed=False, is_rss=False): - """Link to the index for a section. - - Example: - - link://section_index/cars => /cars/index.html - """ - extension = None - - if is_feed: - extension = ".atom" - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension - elif is_rss: - index_file = 'rss.xml' - else: - index_file = self.site.config['INDEX_FILE'] - if name in self.number_of_pages_section[lang]: - number_of_pages = self.number_of_pages_section[lang][name] - else: - posts = [post for post in self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']) if post.section_slug(lang) == name] - number_of_pages = self._compute_number_of_pages(posts, self.site.config['INDEX_DISPLAY_POST_COUNT']) - self.number_of_pages_section[lang][name] = number_of_pages - return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang], - name, - index_file] if _f], - None, - utils.get_displayed_page_number(None, number_of_pages, self.site), - lang, - self.site, - extension=extension) - def index_atom_path(self, name, lang): """Link to a numbered Atom index. @@ -326,21 +204,3 @@ def index_atom_path(self, name, lang): link://index_atom/3 => /index-3.atom """ return self.index_path(name, lang, is_feed=True) - - def index_section_atom_path(self, name, lang): - """Link to the Atom index for a section. - - Example: - - link://section_index_atom/cars => /cars/index.atom - """ - return self.index_section_path(name, lang, is_feed=True) - - def index_section_rss_path(self, name, lang): - """Link to the RSS feed for a section. - - Example: - - link://section_index_rss/cars => /cars/rss.xml - """ - return self.index_section_path(name, lang, is_rss=True) diff --git a/nikola/plugins/task/sections.plugin b/nikola/plugins/task/sections.plugin new file mode 100644 index 0000000000..57de4ec066 --- /dev/null +++ b/nikola/plugins/task/sections.plugin @@ -0,0 +1,13 @@ +[Core] +name = classify_sections +module = sections + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Generates the blog's index pages. + +[Nikola] +plugincategory = Task + diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py new file mode 100644 index 0000000000..cc507be9af --- /dev/null +++ b/nikola/plugins/task/sections.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2016 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the blog indexes.""" + +from __future__ import unicode_literals + +from nikola.plugin_categories import Taxonomy + + +class Indexes(Taxonomy): + """Classify the posts by sections.""" + + name = "classify_sections" + + classification_name = "section_index" + metadata_name = "section" + overview_page_variable_name = "sections" + more_than_one_classifications_per_post = False + has_hierarchy = False + template_for_list_of_one_classification = None + apply_to_posts = True + apply_to_pages = False + omit_empty_classifications = True + also_create_classifications_from_other_languages = False + + def set_site(self, site): + """Set Nikola site.""" + self.show_list_as_index = self.site.config["POSTS_SECTION_ARE_INDEXES"] + self.template_for_classification_overview = "sectionindex.tmpl" if self.show_list_as_index else "list.tmpl" + return super(Indexes, self).set_site(site) + + def is_enabled(self): + """Return True if this taxonomy is enabled, or False otherwise.""" + return self.site.config['POSTS_SECTIONS'] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + raise [post.section_slug(lang)] + + def get_path(self, section, lang): + """A path handler for the given classification.""" + return [_f for _f in [self.site.config['TRANSLATIONS'][lang], section] if _f], True + + def provide_context_and_uptodate(self, section, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation. + + Must return a tuple of two dicts. The first is merged into the page's context, + the second will be put into the uptodate list of all generated tasks. + + Context must contain `title`, which should be something like 'Posts about ', + and `classification_title`, which should be related to the classification string. + """ + kw = { + "messages": self.site.MESSAGES, + } + # Check whether we have a name for this section + if section in self.config['POSTS_SECTION_NAME'](lang): + section_name = self.config['POSTS_SECTION_NAME'](lang)[section] + else: + section_name = section.replace('-', ' ').title() + # Compose section title + section_title = section_name + posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang) + if type(posts_section_title) is dict: + if section in posts_section_title: + section_title = posts_section_title[section] + elif type(posts_section_title) is str: + section_title = posts_section_title + section_title = section_title.format(name=section_name) + # Compose context + context = { + "title": section_title, + "classification_title": section_name, + "description": self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang)[section] if section in self.site.config['POSTS_SECTION_DESCRIPTIONS'](lang) else "", + "pagekind": ["section_page", "index" if self.show_list_as_index else "list"] + } + kw.update(context) + return context, kw From afc1147e758e11e2d07aec389b47c09d51e2c3f8 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:24:09 +0200 Subject: [PATCH 20/30] Fixing usage of getPluginsOfCategory. --- nikola/plugins/misc/taxonomies_classifier.py | 4 ++-- nikola/plugins/task/taxonomies.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 6e63eccef1..f079de05d0 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -44,7 +44,7 @@ class TaxonomiesClassifier(SignalHandler): name = "render_taxonomies" def _do_classification(self, site): - taxonomies = site.plugin_manager.getPluginsOfCategory('Taxonomy') + taxonomies = [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')] site.posts_per_classification = {} for taxonomy in taxonomies: if taxonomy.classification_name in site.posts_per_classification: @@ -226,5 +226,5 @@ def set_site(self, site): # Add hook for after post scanning blinker.signal("scanned").connect(self._do_classification) # Register path handlers - for taxonomy in self.site.plugin_manager.getPluginsOfCategory('Taxonomy'): + for taxonomy in [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')]: self._register_path_handlers(taxonomy) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index a8f8f1b71d..f4bc95594d 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -238,7 +238,7 @@ def gen_tasks(self): self.site.scan_posts() yield self.group_task() - for taxonomy in self.site.plugin_manager.getPluginsOfCategory('Taxonomy'): + for taxonomy in [p.plugin_object for p in self.site.plugin_manager.getPluginsOfCategory('Taxonomy')]: # Should this taxonomy be considered after all? if not taxonomy.is_enabled(): continue From 5c7bcb9412abcde38f1a59d41e3834008f40b483 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:24:21 +0200 Subject: [PATCH 21/30] Fixing name and reference-before-assign bug. --- nikola/plugins/task/sections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index cc507be9af..c00989169a 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -31,7 +31,7 @@ from nikola.plugin_categories import Taxonomy -class Indexes(Taxonomy): +class ClassifySections(Taxonomy): """Classify the posts by sections.""" name = "classify_sections" @@ -49,9 +49,9 @@ class Indexes(Taxonomy): def set_site(self, site): """Set Nikola site.""" - self.show_list_as_index = self.site.config["POSTS_SECTION_ARE_INDEXES"] + self.show_list_as_index = site.config["POSTS_SECTION_ARE_INDEXES"] self.template_for_classification_overview = "sectionindex.tmpl" if self.show_list_as_index else "list.tmpl" - return super(Indexes, self).set_site(site) + return super(ClassifySections, self).set_site(site) def is_enabled(self): """Return True if this taxonomy is enabled, or False otherwise.""" From 94845e28db94e014540cdd9beec389087f216cfb Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:25:02 +0200 Subject: [PATCH 22/30] Fixed two stupid typos. --- nikola/plugins/misc/taxonomies_classifier.py | 2 +- nikola/plugins/task/sections.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index f079de05d0..292d6617c3 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -50,7 +50,7 @@ def _do_classification(self, site): if taxonomy.classification_name in site.posts_per_classification: raise Exception("Found more than one taxonomy with classification name '{}'!".format(taxonomy.classification_name)) site.posts_per_classification[taxonomy.classification_name] = { - lang: defaultdict(set) for lang in self.config['TRANSLATIONS'].keys() + lang: defaultdict(set) for lang in site.config['TRANSLATIONS'].keys() } # Classify posts diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index c00989169a..9fddbf5311 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -59,7 +59,7 @@ def is_enabled(self): def classify(self, post, lang): """Classify the given post for the given language.""" - raise [post.section_slug(lang)] + return [post.section_slug(lang)] def get_path(self, section, lang): """A path handler for the given classification.""" From e1e474a87d15550aafffadbfde6b912e440be690 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 16 Oct 2016 18:35:38 +0200 Subject: [PATCH 23/30] Fixing some more bugs. --- nikola/plugins/misc/taxonomies_classifier.py | 25 +++++++------------- nikola/plugins/task/authors.py | 6 ++--- nikola/plugins/task/sections.py | 10 ++++---- nikola/plugins/task/taxonomies.py | 4 +++- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 292d6617c3..4c1183c87a 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -93,7 +93,7 @@ def _do_classification(self, site): utils.LOGGER.error("{0} {1} yields invalid path '{2}'!".format(taxonomy.classification_name.title(), classification, '/'.join(path))) quit = True # Combine path - path = os.path.join([os.path.normpath(p) for p in path if p != '.']) + path = os.path.join(*[os.path.normpath(p) for p in path if p != '.']) # Determine collisions if path in taxonomy_outputs[lang]: other_classification_name, other_classification, other_posts = taxonomy_outputs[lang][path] @@ -165,18 +165,9 @@ def create_hierarchy(cat_hierarchy, parent=None): site.flat_hierarchy_per_classification[taxonomy.classification_name], site.hierarchy_lookup_per_classification[taxonomy.classification_name]) else: - taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name], flat_hierarchy, hierarchy_lookup) + taxonomy.postprocess_posts_per_classification(site.posts_per_classification[taxonomy.classification_name]) - # Postprocessing - for taxonomy in taxonomies: - for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): - taxonomy.postprocess_posts_per_classification( - posts_per_classification, - site.flat_hierarchy_per_classification.get(taxonomy.classification_name, {}).get(lang, None), - site.hierarchy_lookup_per_classification.get(taxonomy.classification_name, {}).get(lang, None), - ) - - def _postprocess_path(self, path, lang, always_append_index=False, force_extension=None): + def _postprocess_path(self, path, lang, always_append_index=False, force_extension=None, type='page'): if type == 'feed': force_extension = '.atom' elif type == 'rss': @@ -200,7 +191,7 @@ def _taxonomy_index_path(self, lang, taxonomy): def _taxonomy_path(self, name, lang, taxonomy, force_extension=None, type='page'): """Return path to a classification.""" - if taxonomy.has_hirearchy: + if taxonomy.has_hierarchy: path, append_index = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang, type=type) else: path, append_index = taxonomy.get_path(name, lang, type=type) @@ -215,10 +206,10 @@ def _taxonomy_rss_path(self, name, lang, taxonomy): return self._taxonomy_path(name, lang, taxonomy, type='rss') def _register_path_handlers(self, taxonomy): - self.site.register_path_handler('{0}_index'.format(taxonomy.classification_name), lambda name, lang: self._tag_index_path(lang, taxonomy)) - self.site.register_path_handler('{0}'.format(taxonomy.classification_name), lambda name, lang: self._tag_path(name, lang, taxonomy)) - self.site.register_path_handler('{0}_atom'.format(taxonomy.classification_name), lambda name, lang: self._tag_atom_path(name, lang, taxonomy)) - self.site.register_path_handler('{0}_rss'.format(taxonomy.classification_name), lambda name, lang: self._tag_rss_path(name, lang, taxonomy)) + self.site.register_path_handler('{0}_index'.format(taxonomy.classification_name), lambda name, lang: self._taxonomy_index_path(lang, taxonomy)) + self.site.register_path_handler('{0}'.format(taxonomy.classification_name), lambda name, lang: self._taxonomy_path(name, lang, taxonomy)) + self.site.register_path_handler('{0}_atom'.format(taxonomy.classification_name), lambda name, lang: self._taxonomy_atom_path(name, lang, taxonomy)) + self.site.register_path_handler('{0}_rss'.format(taxonomy.classification_name), lambda name, lang: self._taxonomy_rss_path(name, lang, taxonomy)) def set_site(self, site): """Set site, which is a Nikola instance.""" diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 37f4da7769..e7dc676515 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -63,11 +63,11 @@ def classify(self, post, lang): """Classify the given post for the given language.""" return [post.author()] - def get_list_path(self, lang): + def get_list_path(self, lang, type='page'): """A path handler for the list of all classifications.""" return [self.site.config['AUTHOR_PATH']], True - def get_path(self, author, lang): + def get_path(self, author, lang, type='page'): """A path handler for the given classification.""" if self.site.config['SLUG_AUTHOR_PATH']: slug = utils.slugify(author, lang) @@ -107,7 +107,7 @@ def provide_context_and_uptodate(self, author, lang): def postprocess_posts_per_classification(self, posts_per_author_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): """Rearrange, modify or otherwise use the list of posts per classification and per language.""" more_than_one = False - for lang, posts_per_author in posts_per_author_per_language.item(): + for lang, posts_per_author in posts_per_author_per_language.items(): if len(posts_per_author.keys()) > 1: more_than_one = True self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index 9fddbf5311..1e39723710 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -41,7 +41,7 @@ class ClassifySections(Taxonomy): overview_page_variable_name = "sections" more_than_one_classifications_per_post = False has_hierarchy = False - template_for_list_of_one_classification = None + template_for_classification_overview = None apply_to_posts = True apply_to_pages = False omit_empty_classifications = True @@ -50,7 +50,7 @@ class ClassifySections(Taxonomy): def set_site(self, site): """Set Nikola site.""" self.show_list_as_index = site.config["POSTS_SECTION_ARE_INDEXES"] - self.template_for_classification_overview = "sectionindex.tmpl" if self.show_list_as_index else "list.tmpl" + self.template_for_list_of_one_classification = "sectionindex.tmpl" if self.show_list_as_index else "list.tmpl" return super(ClassifySections, self).set_site(site) def is_enabled(self): @@ -61,7 +61,7 @@ def classify(self, post, lang): """Classify the given post for the given language.""" return [post.section_slug(lang)] - def get_path(self, section, lang): + def get_path(self, section, lang, type='page'): """A path handler for the given classification.""" return [_f for _f in [self.site.config['TRANSLATIONS'][lang], section] if _f], True @@ -78,8 +78,8 @@ def provide_context_and_uptodate(self, section, lang): "messages": self.site.MESSAGES, } # Check whether we have a name for this section - if section in self.config['POSTS_SECTION_NAME'](lang): - section_name = self.config['POSTS_SECTION_NAME'](lang)[section] + if section in self.site.config['POSTS_SECTION_NAME'](lang): + section_name = self.site.config['POSTS_SECTION_NAME'](lang)[section] else: section_name = section.replace('-', ' ').title() # Compose section title diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index f4bc95594d..2f1bb280f1 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -70,6 +70,7 @@ def _generate_classification_overview(self, taxonomy, lang): kw = copy(kw) kw['filters'] = self.site.config['FILTERS'] kw["minimum_post_count"] = taxonomy.minimum_post_count_per_classification_in_overview + kw["output_folder"] = self.site.config['OUTPUT_FOLDER'] # Collect all relevant classifications if taxonomy.has_hierarchy: @@ -224,6 +225,7 @@ def _generate_classification_page(self, taxonomy, classification, post_list, lan kw["feed_plain"] = self.site.config["FEED_PLAIN"] kw["feed_link_append_query"] = self.site.config["FEED_LINKS_APPEND_QUERY"] kw["feed_length"] = self.site.config['FEED_LENGTH'] + kw["output_folder"] = self.site.config['OUTPUT_FOLDER'] # Generate RSS feed if kw["generate_rss"]: yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], kw, lang) @@ -244,7 +246,7 @@ def gen_tasks(self): continue for lang in self.site.config["TRANSLATIONS"]: # Generate list of classifications (i.e. classification overview) - if taxonomy.template_for_list_of_one_classification is not None: + if taxonomy.template_for_classification_overview is not None: for task in self._generate_classification_overview(taxonomy, lang): yield task From 7f86e79bce0e38a0bcac31f8397b8952b8b2d1b1 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 07:37:15 +0200 Subject: [PATCH 24/30] Allow to not store classification into metadata. --- nikola/plugin_categories.py | 1 + nikola/plugins/misc/taxonomies_classifier.py | 9 +++++---- nikola/plugins/task/authors.py | 2 +- nikola/plugins/task/sections.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 536c3d3048..f733d0203d 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -468,6 +468,7 @@ class Taxonomy(BasePlugin): # The classification name to be used for path handlers. classification_name = "taxonomy" # The classification name to be used when storing the classification + # in the metadata. If set to None, the classification won't be stored # in the metadata. metadata_name = "taxonomy" # Variable for the overview page template which contains the list of diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 4c1183c87a..33f2564a51 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -63,10 +63,11 @@ def _do_classification(self, site): classifications[lang] = taxonomy.classify(post, lang) assert taxonomy.more_than_one_classifications_per_post or len(classifications[lang]) <= 1 # Store in metadata - if taxonomy.more_than_one_classifications_per_post: - post.meta[lang][taxonomy.metadata_name] = classifications[lang] - else: - post.meta[lang][taxonomy.metadata_name] = classifications[lang][0] if len(classifications[lang]) > 0 else None + if taxonomy.metadata_name is not None: + if taxonomy.more_than_one_classifications_per_post: + post.meta[lang][taxonomy.metadata_name] = classifications[lang] + else: + post.meta[lang][taxonomy.metadata_name] = classifications[lang][0] if len(classifications[lang]) > 0 else None # Add post to sets for classification in classifications[lang]: while classification: diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index e7dc676515..1118dde7bd 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -38,7 +38,7 @@ class ClassifyAuthors(Taxonomy): name = "classify_authors" classification_name = "author" - metadata_name = "author" + metadata_name = None overview_page_variable_name = "authors" more_than_one_classifications_per_post = False has_hierarchy = False diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index 1e39723710..be6ebf0a22 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -37,7 +37,7 @@ class ClassifySections(Taxonomy): name = "classify_sections" classification_name = "section_index" - metadata_name = "section" + metadata_name = None overview_page_variable_name = "sections" more_than_one_classifications_per_post = False has_hierarchy = False From f95aaa9442980276529516ae267b70dcafbc0719 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 07:47:54 +0200 Subject: [PATCH 25/30] Allowing to enable/disable taxonomies based on stage and language. --- nikola/plugin_categories.py | 8 +++++--- nikola/plugins/misc/taxonomies_classifier.py | 4 ++-- nikola/plugins/task/authors.py | 2 +- nikola/plugins/task/sections.py | 15 +++++++++++++-- nikola/plugins/task/taxonomies.py | 2 ++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index f733d0203d..d625d49c93 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -507,11 +507,13 @@ class Taxonomy(BasePlugin): # pages. also_create_classifications_from_other_languages = True - def is_enabled(self): + def is_enabled(self, lang=None): """Return True if this taxonomy is enabled, or False otherwise. - Enabled means that the overview page and the classification lists - are created. + If lang is None, this determins whether the classification is + made at all. If lang is not None, this determines whether the + overview page and the classification lists are created for this + language. """ return True diff --git a/nikola/plugins/misc/taxonomies_classifier.py b/nikola/plugins/misc/taxonomies_classifier.py index 33f2564a51..9f08a7fca5 100644 --- a/nikola/plugins/misc/taxonomies_classifier.py +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -44,7 +44,7 @@ class TaxonomiesClassifier(SignalHandler): name = "render_taxonomies" def _do_classification(self, site): - taxonomies = [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')] + taxonomies = [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy') if p.plugin_object.is_enabled()] site.posts_per_classification = {} for taxonomy in taxonomies: if taxonomy.classification_name in site.posts_per_classification: @@ -218,5 +218,5 @@ def set_site(self, site): # Add hook for after post scanning blinker.signal("scanned").connect(self._do_classification) # Register path handlers - for taxonomy in [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')]: + for taxonomy in [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy') if p.plugin_object.is_enabled()]: self._register_path_handlers(taxonomy) diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 1118dde7bd..071636edcb 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -55,7 +55,7 @@ def set_site(self, site): self.template_for_list_of_one_classification = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl" return super(ClassifyAuthors, self).set_site(site) - def is_enabled(self): + def is_enabled(self, lang=None): """Return True if this taxonomy is enabled, or False otherwise.""" return self.site.config["ENABLE_AUTHOR_PAGES"] diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index be6ebf0a22..64b372d897 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -51,11 +51,16 @@ def set_site(self, site): """Set Nikola site.""" self.show_list_as_index = site.config["POSTS_SECTION_ARE_INDEXES"] self.template_for_list_of_one_classification = "sectionindex.tmpl" if self.show_list_as_index else "list.tmpl" + self.enable_for_lang = {} return super(ClassifySections, self).set_site(site) - def is_enabled(self): + def is_enabled(self, lang=None): """Return True if this taxonomy is enabled, or False otherwise.""" - return self.site.config['POSTS_SECTIONS'] + if not self.site.config['POSTS_SECTIONS']: + return False + if lang is not None: + return self.enable_for_lang.get(lang, False) + return True def classify(self, post, lang): """Classify the given post for the given language.""" @@ -100,3 +105,9 @@ def provide_context_and_uptodate(self, section, lang): } kw.update(context) return context, kw + + def postprocess_posts_per_classification(self, posts_per_section_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): + """Rearrange, modify or otherwise use the list of posts per classification and per language.""" + for lang, posts_per_section in posts_per_section_per_language.items(): + # don't build sections when there is only one, aka. default setups + self.enable_for_lang[lang] = (len(posts_per_section.keys()) > 1) diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 2f1bb280f1..2b413511ef 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -245,6 +245,8 @@ def gen_tasks(self): if not taxonomy.is_enabled(): continue for lang in self.site.config["TRANSLATIONS"]: + if not taxonomy.is_enabled(lang): + continue # Generate list of classifications (i.e. classification overview) if taxonomy.template_for_classification_overview is not None: for task in self._generate_classification_overview(taxonomy, lang): From 3e95d10166a0dac9052439d065ad0ae0f02faa2d Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 07:55:36 +0200 Subject: [PATCH 26/30] Fixed bugs. --- nikola/plugins/task/authors.py | 1 + nikola/plugins/task/taxonomies.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 071636edcb..520b687776 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -96,6 +96,7 @@ def provide_context_and_uptodate(self, author, lang): "messages": self.site.MESSAGES, } context = { + "author": author, "title": kw["messages"][lang]["Posts by %s"] % author, "classification_title": author, "description": descriptions[lang][author] if lang in descriptions and author in descriptions[lang] else None, diff --git a/nikola/plugins/task/taxonomies.py b/nikola/plugins/task/taxonomies.py index 2b413511ef..32de0e100e 100644 --- a/nikola/plugins/task/taxonomies.py +++ b/nikola/plugins/task/taxonomies.py @@ -188,7 +188,7 @@ def _generate_classification_page_as_list_atom(self, taxonomy, classification, f def _generate_classification_page_as_list(self, taxonomy, classification, filtered_posts, context, kw, lang): """Render a single flat link list with this classification's posts.""" kind = taxonomy.classification_name - template_name = "tag.tmpl" + template_name = taxonomy.template_for_list_of_one_classification output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang)) context = copy(context) context["lang"] = lang From 78167512c0b287e211a625cf872c3dcd3c072260 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 08:00:14 +0200 Subject: [PATCH 27/30] Considering only relevant posts. --- nikola/plugins/task/authors.py | 8 +++++++- nikola/plugins/task/sections.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 520b687776..3186a4528a 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -109,7 +109,13 @@ def postprocess_posts_per_classification(self, posts_per_author_per_language, fl """Rearrange, modify or otherwise use the list of posts per classification and per language.""" more_than_one = False for lang, posts_per_author in posts_per_author_per_language.items(): - if len(posts_per_author.keys()) > 1: + authors = set() + for author, posts in posts_per_author.items(): + for post in posts: + if not self.site.config["SHOW_UNTRANSLATED_POSTS"] and not post.is_translation_available(lang): + continue + authors.add(author) + if len(authors) > 1: more_than_one = True self.generate_author_pages = self.site.config["ENABLE_AUTHOR_PAGES"] and more_than_one self.site.GLOBAL_CONTEXT["author_pages_generated"] = self.generate_author_pages diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py index 64b372d897..4ecb5ffbdc 100644 --- a/nikola/plugins/task/sections.py +++ b/nikola/plugins/task/sections.py @@ -109,5 +109,11 @@ def provide_context_and_uptodate(self, section, lang): def postprocess_posts_per_classification(self, posts_per_section_per_language, flat_hierarchy_per_lang=None, hierarchy_lookup_per_lang=None): """Rearrange, modify or otherwise use the list of posts per classification and per language.""" for lang, posts_per_section in posts_per_section_per_language.items(): - # don't build sections when there is only one, aka. default setups - self.enable_for_lang[lang] = (len(posts_per_section.keys()) > 1) + # Don't build sections when there is only one, a.k.a. default setups + sections = set() + for section, posts in posts_per_section.items(): + for post in posts: + if not self.site.config["SHOW_UNTRANSLATED_POSTS"] and not post.is_translation_available(lang): + continue + sections.add(section) + self.enable_for_lang[lang] = (len(sections) > 1) From 99270eeddcc1cc1dd200e1a4f5dccf27f6465048 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 08:05:36 +0200 Subject: [PATCH 28/30] Splitting PAGE_INDEX capabilities into own plugin. --- nikola/plugins/task/indexes.py | 63 --------------- nikola/plugins/task/page_index.plugin | 12 +++ nikola/plugins/task/page_index.py | 106 ++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 nikola/plugins/task/page_index.plugin create mode 100644 nikola/plugins/task/page_index.py diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index e9bd0456a3..a39b03ec9f 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -27,16 +27,10 @@ """Render the blog indexes.""" from __future__ import unicode_literals -from collections import defaultdict import os -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA from nikola.plugin_categories import Task from nikola import utils -from nikola.nikola import _enclosure class Indexes(Task): @@ -112,63 +106,6 @@ def page_path(i, displayed_i, num_pages, force_addition, extension=None): yield self.site.generic_index_renderer(lang, filtered_posts, indexes_title, template_name, context, kw, 'render_indexes', page_link, page_path) - if not self.site.config["PAGE_INDEX"]: - return - kw = { - "translations": self.site.config['TRANSLATIONS'], - "post_pages": self.site.config["post_pages"], - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - "index_file": self.site.config['INDEX_FILE'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - } - template_name = "list.tmpl" - index_len = len(kw['index_file']) - for lang in kw["translations"]: - # Need to group by folder to avoid duplicated tasks (Issue #758) - # Group all pages by path prefix - groups = defaultdict(list) - for p in self.site.timeline: - if not p.is_post: - destpath = p.destination_path(lang) - if destpath[-(1 + index_len):] == '/' + kw['index_file']: - destpath = destpath[:-(1 + index_len)] - dirname = os.path.dirname(destpath) - groups[dirname].append(p) - for dirname, post_list in groups.items(): - context = {} - context["items"] = [] - should_render = True - output_name = os.path.join(kw['output_folder'], dirname, kw['index_file']) - short_destination = os.path.join(dirname, kw['index_file']) - link = short_destination.replace('\\', '/') - if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']: - link = link[:-index_len] - context["permalink"] = link - context["pagekind"] = ["list"] - if dirname == "/": - context["pagekind"].append("front_page") - - for post in post_list: - # If there is an index.html pending to be created from - # a page, do not generate the PAGE_INDEX - if post.destination_path(lang) == short_destination: - should_render = False - else: - context["items"].append((post.title(lang), - post.permalink(lang), - None)) - - if should_render: - task = self.site.generic_post_list_renderer(lang, post_list, - output_name, - template_name, - kw['filters'], - context) - task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')] - task['basename'] = self.name - yield task - def index_path(self, name, lang, is_feed=False): """Link to a numbered index. diff --git a/nikola/plugins/task/page_index.plugin b/nikola/plugins/task/page_index.plugin new file mode 100644 index 0000000000..1e728291c9 --- /dev/null +++ b/nikola/plugins/task/page_index.plugin @@ -0,0 +1,12 @@ +[Core] +name = render_page_index +module = page_index + +[Documentation] +author = Roberto Alsina +version = 1.0 +website = https://getnikola.com/ +description = Generates the blog's index pages. + +[Nikola] +plugincategory = Task diff --git a/nikola/plugins/task/page_index.py b/nikola/plugins/task/page_index.py new file mode 100644 index 0000000000..f3d6a1684b --- /dev/null +++ b/nikola/plugins/task/page_index.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Copyright © 2012-2016 Roberto Alsina and others. + +# Permission is hereby granted, free of charge, to any +# person obtaining a copy of this software and associated +# documentation files (the "Software"), to deal in the +# Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the +# Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice +# shall be included in all copies or substantial portions of +# the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Render the blog indexes.""" + +from __future__ import unicode_literals +from collections import defaultdict +import os + +from nikola.plugin_categories import Task +from nikola import utils + + +class PageIndex(Task): + """Render the page index.""" + + name = "render_page_index" + + def set_site(self, site): + """Set Nikola site.""" + return super(PageIndex, self).set_site(site) + + def gen_tasks(self): + """Render the blog indexes.""" + self.site.scan_posts() + yield self.group_task() + + if not self.site.config["PAGE_INDEX"]: + return + kw = { + "translations": self.site.config['TRANSLATIONS'], + "post_pages": self.site.config["post_pages"], + "output_folder": self.site.config['OUTPUT_FOLDER'], + "filters": self.site.config['FILTERS'], + "index_file": self.site.config['INDEX_FILE'], + "strip_indexes": self.site.config['STRIP_INDEXES'], + } + template_name = "list.tmpl" + index_len = len(kw['index_file']) + for lang in kw["translations"]: + # Need to group by folder to avoid duplicated tasks (Issue #758) + # Group all pages by path prefix + groups = defaultdict(list) + for p in self.site.timeline: + if not p.is_post: + destpath = p.destination_path(lang) + if destpath[-(1 + index_len):] == '/' + kw['index_file']: + destpath = destpath[:-(1 + index_len)] + dirname = os.path.dirname(destpath) + groups[dirname].append(p) + for dirname, post_list in groups.items(): + context = {} + context["items"] = [] + should_render = True + output_name = os.path.join(kw['output_folder'], dirname, kw['index_file']) + short_destination = os.path.join(dirname, kw['index_file']) + link = short_destination.replace('\\', '/') + if kw['strip_indexes'] and link[-(1 + index_len):] == '/' + kw['index_file']: + link = link[:-index_len] + context["permalink"] = link + context["pagekind"] = ["list"] + if dirname == "/": + context["pagekind"].append("front_page") + + for post in post_list: + # If there is an index.html pending to be created from + # a page, do not generate the PAGE_INDEX + if post.destination_path(lang) == short_destination: + should_render = False + else: + context["items"].append((post.title(lang), + post.permalink(lang), + None)) + + if should_render: + task = self.site.generic_post_list_renderer(lang, post_list, + output_name, + template_name, + kw['filters'], + context) + task['uptodate'] = task['uptodate'] + [utils.config_changed(kw, 'nikola.plugins.task.indexes')] + task['basename'] = self.name + yield task From 03830993b7f20db35819bc3e69f743ca1c75c63d Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 08:29:32 +0200 Subject: [PATCH 29/30] Forgot some logic. --- nikola/plugins/task/authors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nikola/plugins/task/authors.py b/nikola/plugins/task/authors.py index 3186a4528a..57c2594ec8 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -57,7 +57,11 @@ def set_site(self, site): def is_enabled(self, lang=None): """Return True if this taxonomy is enabled, or False otherwise.""" - return self.site.config["ENABLE_AUTHOR_PAGES"] + if not self.site.config["ENABLE_AUTHOR_PAGES"]: + return False + if lang is not None: + return self.generate_author_pages + return True def classify(self, post, lang): """Classify the given post for the given language.""" From 70e030224591cf843d5d0cb1b13ccb570fba37bd Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 17 Oct 2016 08:31:01 +0200 Subject: [PATCH 30/30] Updated metadata. --- nikola/plugins/task/authors.plugin | 2 ++ nikola/plugins/task/sections.plugin | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin index 32834527a3..1e10cb1964 100644 --- a/nikola/plugins/task/authors.plugin +++ b/nikola/plugins/task/authors.plugin @@ -8,3 +8,5 @@ Version = 0.1 Website = http://getnikola.com Description = Render the author pages and feeds. +[Nikola] +plugincategory = Taxonomy diff --git a/nikola/plugins/task/sections.plugin b/nikola/plugins/task/sections.plugin index 57de4ec066..146f2f82df 100644 --- a/nikola/plugins/task/sections.plugin +++ b/nikola/plugins/task/sections.plugin @@ -9,5 +9,4 @@ website = https://getnikola.com/ description = Generates the blog's index pages. [Nikola] -plugincategory = Task - +plugincategory = Taxonomy