diff --git a/nikola/nikola.py b/nikola/nikola.py index 4a3cc9f3df..c109219a1d 100644 --- a/nikola/nikola.py +++ b/nikola/nikola.py @@ -73,6 +73,7 @@ SignalHandler, ConfigPlugin, PostScanner, + Taxonomy, ) if DEBUG: @@ -949,6 +950,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'] @@ -1020,6 +1022,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. @@ -2344,7 +2347,7 @@ def generic_index_renderer(self, lang, posts, indexes_title, template_name, cont kw['indexes_prety_page_url'] = self.config["INDEXES_PRETTY_PAGE_URL"] kw['demote_headers'] = self.config['DEMOTE_HEADERS'] kw['generate_atom'] = self.config["GENERATE_ATOM"] - kw['feed_link_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"] + kw['feed_links_append_query'] = self.config["FEED_LINKS_APPEND_QUERY"] kw['currentfeed'] = None # Split in smaller lists diff --git a/nikola/plugin_categories.py b/nikola/plugin_categories.py index 335034fb12..1d0090776f 100644 --- a/nikola/plugin_categories.py +++ b/nikola/plugin_categories.py @@ -49,6 +49,7 @@ 'SignalHandler', 'ConfigPlugin', 'PostScanner', + 'Taxonomy', ) @@ -471,3 +472,268 @@ 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. Classification plugins must adjust + a set of options to determine certain aspects. + + The following options are class attributes with their default + values. These variables should be set in the class definition, + in the constructor or latest in the `set_site` function. + + classification_name = "taxonomy": + The classification name to be used for path handlers. + + metadata_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. + + overview_page_variable_name = "taxonomy": + When rendering the overview page, its template will have a list + of classifications available in a variable by this name. + + more_than_one_classifications_per_post = False: + 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. + + has_hierarchy = False: + Whether the classification has a hierarchy. + + include_posts_from_subhierarchies = False: + If True, the list for a classification includes all posts with a + sub-classification (in case has_hierarchy is True). + + include_posts_into_hierarchy_root = False: + If True, include_posts_from_subhierarchies == True will also insert + posts into the list for the empty hierarchy []. + + show_list_as_subcategories_list = False: + If not False, for every classification which has at least one + subclassification, create a list of subcategories instead of a list/index + of posts. This is only used when has_hierarchy = True. If not False, this + must be the template name for the list; usually "list.tmpl". + If this is set to a string, it is recommended to set + include_posts_from_subhierarchies to True to get correct post counts. + + show_list_as_index = False: + Whether to show the posts for one classification as an index or + as a post list. + + generate_atom_feeds_for_post_lists = False: + Whether to generate Atom feeds for post lists in case GENERATE_ATOM is set. + + template_for_single_list = "tagindex.tmpl": + The template to use for the post list for one classification. + + template_for_classification_overview = "list.tmpl": + The template to use for the classification overview page. + Set to None to avoid generating overviews. + + always_disable_rss = False: + Whether to always disable RSS feed generation + + apply_to_posts = True: + Whether this classification applies to posts. + + apply_to_pages = False: + Whether this classification applies to pages. + + minimum_post_count_per_classification_in_overview = 1: + The minimum number of posts a classification must have to be listed in + the overview. + + omit_empty_classifications = False: + Whether post lists resp. indexes should be created for empty + classifications. + + also_create_classifications_from_other_languages = True: + Whether to include all classifications for all languages in every + language, or only the classifications for one language in its language's + pages. + """ + + name = "dummy_taxonomy" + + # Adjust the following values in your plugin! + classification_name = "taxonomy" + metadata_name = "taxonomy" + overview_page_variable_name = "taxonomy" + more_than_one_classifications_per_post = False + has_hierarchy = False + include_posts_from_subhierarchies = False + include_posts_into_hierarchy_root = False + show_list_as_subcategories_list = False + show_list_as_index = False + generate_atom_feeds_for_post_lists = False + template_for_single_list = "tagindex.tmpl" + template_for_classification_overview = "list.tmpl" + always_disable_rss = False + 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 = True + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise. + + 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 + + def get_implicit_classifications(self, lang): + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [] + + def classify(self, post, lang): + """Classify the given post for the given language. + + Must return a list or tuple of strings. + """ + raise NotImplementedError() + + def sort_posts(self, posts, classification, lang): + """Sort the given list of posts. + + Allows the plugin to order the posts per classification as it wants. + The posts will be ordered by date (latest first) before calling + this function. This function must sort in-place. + """ + pass + + def sort_classifications(self, classifications, lang, level=None): + """Sort the given list of classification strings. + + Allows the plugin to order the classifications as it wants. The + classifications will be ordered by `natsort` before calling this + function. This function must sort in-place. + + For hierarchical taxonomies, the elements of the list are a single + path element of the path returned by `extract_hierarchy()`. The index + of the path element in the path will be provided in `level`. + """ + pass + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification. + + The result of this function is usually displayed to the user, instead + of using the classification string. + + For hierarchical taxonomies, the result of extract_hierarchy is provided + as `classification`. For non-hierarchical taxonomies, the classification + string itself is provided as `classification`. + + The argument `only_last_component` is only relevant to hierarchical + taxonomies. If it is set, the printable name should only describe the + last component of `classification` if possible. + """ + raise NotImplementedError() + + def get_list_path(self, lang, type='page'): + """A path handler for the list of all classifications. + + Must return one or two values (in this order): + * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY; + * a string with values 'auto', 'always' or 'never', indicating whether + INDEX_FILE should be added or not. + + Note that this function must always return a list or tuple of strings; + the other return value is optional with default value `'auto'`. + + In case INDEX_FILE should potentially 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. + + Type can be either 'page', 'feed' (for Atom feed) or 'rss'. + """ + raise NotImplementedError() + + def get_path(self, classification, lang, type='page'): + """A path handler for the given classification. + + Must return one to three values (in this order): + * a list or tuple of strings: the path relative to OUTPUT_DIRECTORY; + * a string with values 'auto', 'always' or 'never', indicating whether + INDEX_FILE should be added or not; + * an integer if a specific page of the index is to be targeted (will be + ignored for post lists), or `None` if the most current page is targeted. + + Note that this function must always return a list or tuple of strings; + the other two return values are optional with default values `'auto'` and + `None`. + + In case INDEX_FILE should potentially 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. + + 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`. + """ + 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, 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, + 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, 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 '. + """ + raise NotImplementedError() + + def should_generate_classification_list(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + return True + + def postprocess_posts_per_classification(self, posts_per_classification_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 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 `utils.TreeNode` elements, and `hierarchy_lookup_per_lang` + is the corresponding hierarchy lookup mapping classification strings to + `utils.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..aeec68bf8c --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_taxonomies +module = taxonomies_classifier + +[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..d33cb66e2b --- /dev/null +++ b/nikola/plugins/misc/taxonomies_classifier.py @@ -0,0 +1,311 @@ +# -*- 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, site): + # Get list of enabled taxonomy plugins + taxonomies = [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')] + taxonomies = [taxonomy for taxonomy in taxonomies if taxonomy.is_enabled()] + # Prepare classification and check for collisions + site.posts_per_classification = {} + for taxonomy in taxonomies: + 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 site.config['TRANSLATIONS'].keys() + } + + # Classify posts + for post in site.timeline: + if not post.use_in_feeds: + continue + for taxonomy in taxonomies: + if taxonomy.apply_to_posts if post.is_post else taxonomy.apply_to_pages: + classifications = {} + 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 + # Store in metadata + 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 True: + 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_path = taxonomy.extract_hierarchy(classification) + if len(classification_path) <= 1: + if len(classification_path) == 0 or not taxonomy.include_posts_into_hierarchy_root: + break + classification = taxonomy.recombine_classification_from_hierarchy(classification_path[:-1]) + + # Check for valid paths and for collisions + 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 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 site.posts_per_classification[taxonomy.classification_name][tlang].items(): + # Obtain path as tuple + 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 {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( + 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. + site.page_count_per_classification = {} + site.hierarchy_per_classification = {} + site.flat_hierarchy_per_classification = {} + site.hierarchy_lookup_per_classification = {} + for taxonomy in taxonomies: + site.page_count_per_classification[taxonomy.classification_name] = {} + # Sort post lists + for lang, posts_per_classification in site.posts_per_classification[taxonomy.classification_name].items(): + # Ensure implicit classifications are inserted + for classification in taxonomy.get_implicit_classifications(lang): + if classification not in posts_per_classification: + posts_per_classification[classification] = [] + site.page_count_per_classification[taxonomy.classification_name][lang] = {} + # 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() + taxonomy.sort_posts(posts, classification, lang) + posts_per_classification[classification] = posts + # Create hierarchy information + if taxonomy.has_hierarchy: + 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(): + 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(hierarchy, parent=None, level=0): + """Create hierarchy.""" + result = {} + for name, children in hierarchy.items(): + node = utils.TreeNode(name, parent) + node.children = create_hierarchy(children, node, level + 1) + 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 + result[node.name] = node + classifications = natsort.natsorted(result.keys(), alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications, lang, level=level) + return [result[classification] for classification in classifications] + + root_list = create_hierarchy(hierarchy) + if '' in posts_per_classification: + node = utils.TreeNode('', parent=None) + node.children = root_list + node.classification_path = [] + node.classification_name = '' + hierarchy_lookup[node.name] = node + root_list = [node] + flat_hierarchy = utils.flatten_tree_structure(root_list) + # Store result + 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(site.posts_per_classification[taxonomy.classification_name]) + + def _get_filtered_list(self, taxonomy, classification, lang): + """Return the filtered list of posts for this classification and language.""" + post_list = self.site.posts_per_classification[taxonomy.classification_name][lang].get(classification, []) + if self.site.config["SHOW_UNTRANSLATED_POSTS"]: + return post_list + else: + return [x for x in post_list if x.is_translation_available(lang)] + + @staticmethod + def _compute_number_of_pages(filtered_posts, posts_count): + """Given a list of posts and the maximal number of posts per page, computes the number of pages needed.""" + return min(1, (len(filtered_posts) + posts_count - 1) // posts_count) + + def _postprocess_path(self, path, lang, append_index='auto', type='page', page_info=None): + """Postprocess a generated path. + + Takes the path `path` for language `lang`, and postprocesses it. + + It appends `site.config['INDEX_FILE']` depending on `append_index` + (which can have the values `'always'`, `'never'` and `'auto'`) and + `site.config['PRETTY_URLS']`. + + It also modifies/adds the extension of the last path element resp. + `site.config['INDEX_FILE']` depending on `type`, which can be + `'feed'`, `'rss'` or `'page'`. + + Finally, if `type` is `'page'`, `page_info` can be `None` or a tuple + of two integers: the page number and the number of pages. This will + be used to append the correct page number by calling + `utils.adjust_name_for_index_path_list` and + `utils.get_displayed_page_number`. + """ + # Forcing extension for Atom feeds and RSS feeds + force_extension = None + if type == 'feed': + force_extension = '.atom' + elif type == 'rss': + force_extension = '.xml' + # Determine how to extend path + path = [_f for _f in path if _f] + if force_extension is not None: + if len(path) == 0 and type == 'rss': + path = ['rss'] + elif len(path) == 0 or append_index == 'always': + path = path + [os.path.splitext(self.site.config['INDEX_FILE'])[0]] + path[-1] += force_extension + elif (self.site.config['PRETTY_URLS'] and append_index != 'never') or len(path) == 0 or append_index == 'always': + path = path + [self.site.config['INDEX_FILE']] + else: + path[-1] += '.html' + # Create path + result = [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + path if _f] + if page_info is not None and type == 'page': + result = utils.adjust_name_for_index_path_list(result, + page_info[0], + utils.get_displayed_page_number(page_info[0], page_info[1], self.site), + lang, + self.site) + return result + + @staticmethod + def _parse_path_result(result): + """Interpret the return values of taxonomy.get_path() and taxonomy.get_list_path() as if all three return values were given.""" + if not isinstance(result[0], (list, tuple)): + # The result must be a list or tuple of strings. Wrap into a tuple + result = (result, ) + path = result[0] + append_index = result[1] if len(result) > 1 else 'auto' + page_info = result[2] if len(result) > 2 else None + return path, append_index, page_info + + def _taxonomy_index_path(self, lang, taxonomy): + """Return path to the classification overview.""" + result = taxonomy.get_list_path(lang) + path, append_index, _ = self._parse_path_result(result) + return self._postprocess_path(path, lang, append_index=append_index, type='list') + + def _taxonomy_path(self, name, lang, taxonomy, type='page'): + """Return path to a classification.""" + if taxonomy.has_hierarchy: + result = taxonomy.get_path(taxonomy.extract_hierarchy(name), lang, type=type) + else: + result = taxonomy.get_path(name, lang, type=type) + path, append_index, page = self._parse_path_result(result) + page_info = None + if not taxonomy.show_list_as_index and page is not None: + number_of_pages = self.site.page_count_per_classification[taxonomy.classification_name][lang].get(name) + if number_of_pages is None: + number_of_pages = self._compute_number_of_pages(self._get_filtered_list(name, lang), self.site.config['INDEX_DISPLAY_POST_COUNT']) + self.site.page_count_per_classification[taxonomy.classification_name][lang][name] = number_of_pages + page_info = (page, number_of_pages) + return self._postprocess_path(path, lang, append_index=append_index, type=type, page_info=page_info) + + def _taxonomy_atom_path(self, name, lang, taxonomy): + """Return path to a classification Atom feed.""" + 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, type='rss') + + def _register_path_handlers(self, 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.""" + 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 [p.plugin_object for p in site.plugin_manager.getPluginsOfCategory('Taxonomy')]: + if not taxonomy.is_enabled(): + continue + self._register_path_handlers(taxonomy) diff --git a/nikola/plugins/task/archive.plugin b/nikola/plugins/task/archive.plugin index eb079da4eb..c64dcee40f 100644 --- a/nikola/plugins/task/archive.plugin +++ b/nikola/plugins/task/archive.plugin @@ -1,5 +1,5 @@ [Core] -name = render_archive +name = classify_archive module = archive [Documentation] @@ -9,5 +9,5 @@ website = https://getnikola.com/ description = Generates the blog's archive pages. [Nikola] -plugincategory = Task +plugincategory = Taxonomy diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index 303d349cb0..fb3b5e51b2 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -24,231 +24,141 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Render the post archives.""" +"""Classify the posts in archives.""" -import copy import os - -# for tearDown with _reload we cannot use 'import from' to access LocaleBorg import nikola.utils import datetime -from nikola.plugin_categories import Task -from nikola.utils import config_changed, adjust_name_for_index_path, adjust_name_for_index_link - - -class Archive(Task): - """Render the post archives.""" - - name = "render_archive" +from nikola.plugin_categories import Taxonomy + + +class Archive(Taxonomy): + """Classify the post archives.""" + + name = "classify_archive" + + classification_name = "archive" + metadata_name = None + overview_page_variable_name = "archive" + more_than_one_classifications_per_post = False + has_hierarchy = True + include_posts_from_subhierarchies = True + include_posts_into_hierarchy_root = True + generate_atom_feeds_for_post_lists = False + template_for_classification_overview = None + always_disable_rss = True + 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.""" - site.register_path_handler('archive', self.archive_path) - site.register_path_handler('archive_atom', self.archive_atom_path) + # Sanity checks + if (site.config['CREATE_MONTHLY_ARCHIVE'] and site.config['CREATE_SINGLE_ARCHIVE']) and not site.config['CREATE_FULL_ARCHIVES']: + raise Exception('Cannot create monthly and single archives at the same time.') + # Finish setup + self.show_list_as_subcategories_list = False if site.config['CREATE_FULL_ARCHIVES'] else "list.tmpl" + self.show_list_as_index = site.config['ARCHIVES_ARE_INDEXES'] + self.template_for_single_list = "archiveindex.tmpl" if site.config['ARCHIVES_ARE_INDEXES'] else "list_post.tmpl" + # Determine maximum hierarchy height + if site.config['CREATE_DAILY_ARCHIVE'] or site.config['CREATE_FULL_ARCHIVES']: + self.max_levels = 3 + elif site.config['CREATE_MONTHLY_ARCHIVE']: + self.max_levels = 2 + elif site.config['CREATE_SINGLE_ARCHIVE']: + self.max_levels = 0 + else: + self.max_levels = 1 return super(Archive, self).set_site(site) - def _prepare_task(self, kw, name, lang, posts, items, template_name, - title, deps_translatable=None): - """Prepare an archive task.""" - # name: used to build permalink and destination - # posts, items: posts or items; only one of them should be used, - # the other should be None - # template_name: name of the template to use - # title: the (translated) title for the generated page - # deps_translatable: dependencies (None if not added) - assert posts is not None or items is not None - task_cfg = [copy.copy(kw)] - context = {} - context["lang"] = lang - context["title"] = title - context["permalink"] = self.site.link("archive", name, lang) - context["pagekind"] = ["list", "archive_page"] - if posts is not None: - context["posts"] = posts - # Depend on all post metadata because it can be used in templates (Issue #1931) - task_cfg.append([repr(p) for p in posts]) + def get_implicit_classifications(self, lang): + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [''] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + levels = [str(post.date.year).zfill(4), str(post.date.month).zfill(2), str(post.date.day).zfill(2)] + return ['/'.join(levels[:self.max_levels])] + + def sort_classifications(self, classifications, lang, level=None): + """Sort the given list of classification strings.""" + if level in (0, 1): + # Years or months: sort descending + classifications.sort() + classifications.reverse() + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + if len(classification) == 0: + return "" + elif len(classification) == 1: + return classification[0] + elif len(classification) == 2: + nikola.utils.LocaleBorg().get_month_name(int(classification[1]), lang) else: - # Depend on the content of items, to rebuild if links change (Issue #1931) - context["items"] = items - task_cfg.append(items) - task = self.site.generic_post_list_renderer( - lang, - [], - os.path.join(kw['output_folder'], self.site.path("archive", name, lang)), - template_name, - kw['filters'], - context, - ) - - task_cfg = {i: x for i, x in enumerate(task_cfg)} - if deps_translatable is not None: - task_cfg[3] = deps_translatable - task['uptodate'] = task['uptodate'] + [config_changed(task_cfg, 'nikola.plugins.task.archive')] - task['basename'] = self.name - return task - - def _generate_posts_task(self, kw, name, lang, posts, title, deps_translatable=None): - """Genereate a task for an archive with posts.""" - posts = sorted(posts, key=lambda a: a.date) - posts.reverse() - if kw['archives_are_indexes']: - def page_link(i, displayed_i, num_pages, force_addition, extension=None): - feed = "_atom" if extension == ".atom" else "" - return adjust_name_for_index_link(self.site.link("archive" + feed, name, 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 adjust_name_for_index_path(self.site.path("archive" + feed, name, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - uptodate = [] - if deps_translatable is not None: - uptodate += [config_changed(deps_translatable, 'nikola.plugins.task.archive')] - context = {"archive_name": name, - "is_feed_stale": kw["is_feed_stale"], - "pagekind": ["index", "archive_page"]} - yield self.site.generic_index_renderer( - lang, - posts, - title, - "archiveindex.tmpl", - context, - kw, - str(self.name), - page_link, - page_path, - uptodate) + # Fallback + return '/'.join(classification) + + def get_path(self, classification, lang, type='page'): + """A path handler for the given classification.""" + components = [self.site.config['ARCHIVE_PATH']] + if classification: + components.extend(classification) + add_index = 'always' else: - yield self._prepare_task(kw, name, lang, posts, None, "list_post.tmpl", title, deps_translatable) + components.append(os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0]) + add_index = 'never' + return [_f for _f in components if _f], add_index + + def extract_hierarchy(self, classification): + """Given a classification, return a list of parts in the hierarchy.""" + return classification.split('/') if classification else [] - def gen_tasks(self): - """Generate archive tasks.""" + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string.""" + return '/'.join(hierarchy) + + def provide_context_and_uptodate(self, classification, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + hierarchy = self.extract_hierarchy(classification) kw = { "messages": self.site.MESSAGES, - "translations": self.site.config['TRANSLATIONS'], - "output_folder": self.site.config['OUTPUT_FOLDER'], - "filters": self.site.config['FILTERS'], - "archives_are_indexes": self.site.config['ARCHIVES_ARE_INDEXES'], - "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'], - "create_single_archive": self.site.config['CREATE_SINGLE_ARCHIVE'], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "create_full_archives": self.site.config['CREATE_FULL_ARCHIVES'], - "create_daily_archive": self.site.config['CREATE_DAILY_ARCHIVE'], - "pretty_urls": self.site.config['PRETTY_URLS'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "index_file": self.site.config['INDEX_FILE'], - "generate_atom": self.site.config["GENERATE_ATOM"], } - self.site.scan_posts() - yield self.group_task() - # TODO add next/prev links for years - if (kw['create_monthly_archive'] and kw['create_single_archive']) and not kw['create_full_archives']: - raise Exception('Cannot create monthly and single archives at the same time.') - for lang in kw["translations"]: - if kw['create_single_archive'] and not kw['create_full_archives']: - # if we are creating one single archive - archdata = {} - else: - # if we are not creating one single archive, start with all years - archdata = self.site.posts_per_year.copy() - if kw['create_single_archive'] or kw['create_full_archives']: - # if we are creating one single archive, or full archives - archdata[None] = self.site.posts # for create_single_archive - - for year, posts in archdata.items(): - # Filter untranslated posts (Issue #1360) - if not kw["show_untranslated_posts"]: - posts = [p for p in posts if lang in p.translated_to] - - # Add archive per year or total archive - if year: - title = kw["messages"][lang]["Posts for year %s"] % year - kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != year) - else: - title = kw["messages"][lang]["Archive"] - kw["is_feed_stale"] = False - deps_translatable = {} - for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: - deps_translatable[k] = self.site.GLOBAL_CONTEXT[k](lang) - if not kw["create_monthly_archive"] or kw["create_full_archives"]: - yield self._generate_posts_task(kw, year, lang, posts, title, deps_translatable) - else: - months = set([(m.split('/')[1], self.site.link("archive", m, lang), len(self.site.posts_per_month[m])) for m in self.site.posts_per_month.keys() if m.startswith(str(year))]) - months = sorted(list(months)) - months.reverse() - items = [[nikola.utils.LocaleBorg().get_month_name(int(month), lang), link, count] for month, link, count in months] - yield self._prepare_task(kw, year, lang, None, items, "list.tmpl", title, deps_translatable) - - if not kw["create_monthly_archive"] and not kw["create_full_archives"] and not kw["create_daily_archive"]: - continue # Just to avoid nesting the other loop in this if - for yearmonth, posts in self.site.posts_per_month.items(): - # Add archive per month - year, month = yearmonth.split('/') - - kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m") != yearmonth) - - # Filter untranslated posts (via Issue #1360) - if not kw["show_untranslated_posts"]: - posts = [p for p in posts if lang in p.translated_to] - - if kw["create_monthly_archive"] or kw["create_full_archives"]: - title = kw["messages"][lang]["Posts for {month} {year}"].format( - year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang)) - yield self._generate_posts_task(kw, yearmonth, lang, posts, title) - - if not kw["create_full_archives"] and not kw["create_daily_archive"]: - continue # Just to avoid nesting the other loop in this if - # Add archive per day - days = dict() - for p in posts: - if p.date.day not in days: - days[p.date.day] = list() - days[p.date.day].append(p) - for day, posts in days.items(): - title = kw["messages"][lang]["Posts for {month} {day}, {year}"].format( - year=year, month=nikola.utils.LocaleBorg().get_month_name(int(month), lang), day=day) - yield self._generate_posts_task(kw, yearmonth + '/{0:02d}'.format(day), lang, posts, title) - - if not kw['create_single_archive'] and not kw['create_full_archives']: - # And an "all your years" page for yearly and monthly archives - if "is_feed_stale" in kw: - del kw["is_feed_stale"] - years = list(self.site.posts_per_year.keys()) - years.sort(reverse=True) - kw['years'] = years - for lang in kw["translations"]: - items = [(y, self.site.link("archive", y, lang), len(self.site.posts_per_year[y])) for y in years] - yield self._prepare_task(kw, None, lang, None, items, "list.tmpl", kw["messages"][lang]["Archive"]) - - def archive_path(self, name, lang, is_feed=False): - """Link to archive path, name is the year. - - Example: - - link://archive/2013 => /archives/2013/index.html - """ - if is_feed: - extension = ".atom" - archive_file = os.path.splitext(self.site.config['ARCHIVE_FILENAME'])[0] + extension - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension + page_kind = "list" + if self.show_list_as_index: + if not self.show_list_as_subcategories_list or len(hierarchy) == self.max_levels: + page_kind = "index" + if len(hierarchy) == 0: + title = kw["messages"][lang]["Archive"] + kw["is_feed_stale"] = False + elif len(hierarchy) == 1: + title = kw["messages"][lang]["Posts for year %s"] % hierarchy[0] + kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y") != hierarchy[0]) + elif len(hierarchy) == 2: + title = kw["messages"][lang]["Posts for {month} {year}"].format( + year=hierarchy[0], + month=nikola.utils.LocaleBorg().get_month_name(int(hierarchy[1]), lang)) + kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m") != classification) + elif len(hierarchy) == 3: + title = kw["messages"][lang]["Posts for {month} {day}, {year}"].format( + year=hierarchy[0], + month=nikola.utils.LocaleBorg().get_month_name(int(hierarchy[1]), lang), + day=int(hierarchy[2])) + kw["is_feed_stale"] = (datetime.datetime.utcnow().strftime("%Y/%m/%d") != classification) else: - archive_file = self.site.config['ARCHIVE_FILENAME'] - index_file = self.site.config['INDEX_FILE'] - if name: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['ARCHIVE_PATH'], name, - index_file] if _f] - else: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['ARCHIVE_PATH'], - archive_file] if _f] - - def archive_atom_path(self, name, lang): - """Link to atom archive path, name is the year. - - Example: - - link://archive_atom/2013 => /archives/2013/index.atom - """ - return self.archive_path(name, lang, is_feed=True) + raise Exception("Cannot interpret classification {}!".format(repr(classification))) + context = { + "title": title, + "pagekind": [page_kind, "archive_page"], + } + if page_kind == 'index': + context["archive_name"] = classification if classification else None + context["is_feed_stale"] = kw["is_feed_stale"] + kw.update(context) + return context, kw + + def should_generate_classification_list(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + return len(classification.split('/')) < 3 or len(post_list) > 0 diff --git a/nikola/plugins/task/authors.plugin b/nikola/plugins/task/authors.plugin index 3fc4ef27e9..1e10cb1964 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] @@ -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/authors.py b/nikola/plugins/task/authors.py index ec618002b3..214a06e050 100644 --- a/nikola/plugins/task/authors.py +++ b/nikola/plugins/task/authors.py @@ -27,300 +27,107 @@ """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): - """Render the author pages and feeds.""" +class ClassifyAuthors(Taxonomy): + """Classify the posts by authors.""" + + name = "classify_authors" - name = "render_authors" - posts_per_author = None + classification_name = "author" + metadata_name = None + overview_page_variable_name = "authors" + more_than_one_classifications_per_post = False + has_hierarchy = False + generate_atom_feeds_for_post_lists = 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) + self.show_list_as_index = site.config['AUTHOR_PAGES_ARE_INDEXES'] + self.template_for_single_list = "authorindex.tmpl" if self.show_list_as_index else "author.tmpl" + return super(ClassifyAuthors, self).set_site(site) + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + 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.""" + return [post.author()] + + def get_classification_friendly_name(self, author, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return author - 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 get_list_path(self, lang, type='page'): + """A path handler for the list of all classifications.""" + return [self.site.config['AUTHOR_PATH']], 'always' + + 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) + else: + slug = author + return [self.site.config['AUTHOR_PATH'], slug], 'auto' - def gen_tasks(self): - """Render the author pages and feeds.""" + def provide_list_context_and_uptodate(self, lang): + """Provide data for the context and the uptodate list for the list of all classifiations.""" 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'], } + 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 - 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): + 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'] - 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" - - yield self.site.generic_index_renderer(lang, post_list, indexes_title, template_name, context_source, kw, str(self.name), page_link, page_path) - - 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 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'], + kw = { + "messages": self.site.MESSAGES, } - return utils.apply_filters(task, kw['filters']) - - 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 = '' - 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] - 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: - - 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 _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 + context = { + "author": author, + "title": kw["messages"][lang]["Posts by %s"] % 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"], + } + if self.site.config["GENERATE_RSS"]: + rss_link = ("""""".format( + author, lang, self.site.link('author_rss', author, lang))) + context['rss_link'] = rss_link + 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.items(): + 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/indexes.plugin b/nikola/plugins/task/indexes.plugin index 553b5ad16d..897577094d 100644 --- a/nikola/plugins/task/indexes.plugin +++ b/nikola/plugins/task/indexes.plugin @@ -1,5 +1,5 @@ [Core] -name = render_indexes +name = classify_indexes module = indexes [Documentation] @@ -9,5 +9,4 @@ website = https://getnikola.com/ description = Generates the blog's index pages. [Nikola] -plugincategory = Task - +plugincategory = Taxonomy diff --git a/nikola/plugins/task/indexes.py b/nikola/plugins/task/indexes.py index 8ecd1decd1..50aeb70017 100644 --- a/nikola/plugins/task/indexes.py +++ b/nikola/plugins/task/indexes.py @@ -24,323 +24,71 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Render the blog indexes.""" +"""Render the blog's main index.""" 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 +from nikola.plugin_categories import Taxonomy -class Indexes(Task): - """Render the blog indexes.""" +class Indexes(Taxonomy): + """Classify for the blog's main index.""" - name = "render_indexes" + name = "classify_indexes" + + classification_name = "index" + metadata_name = None + overview_page_variable_name = None + more_than_one_classifications_per_post = False + has_hierarchy = False + show_list_as_index = True + template_for_single_list = "index.tmpl" + template_for_classification_overview = None + apply_to_posts = True + apply_to_pages = False + omit_empty_classifications = False + also_create_classifications_from_other_languages = False 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) + # Redirect automatically generated 'index_rss' path handler to 'rss' for compatibility with old rss plugin + site.register_path_handler('rss', lambda name, lang: site.path_handlers['index_rss'](name, lang)) return super(Indexes, self).set_site(site) - def _get_filtered_posts(self, lang, show_untranslated_posts): - """Return a filtered list of all posts for the given language. - - If show_untranslated_posts is True, will only include posts which - are translated to the given language. Otherwise, returns all posts. - """ - if show_untranslated_posts: - return self.site.posts - else: - return [x for x in self.site.posts if x.is_translation_available(lang)] - - def _compute_number_of_pages(self, filtered_posts, posts_count): - """Given a list of posts and the maximal number of posts per page, computes the number of pages needed.""" - return min(1, (len(filtered_posts) + posts_count - 1) // posts_count) - - def gen_tasks(self): - """Render the blog indexes.""" - self.site.scan_posts() - yield self.group_task() - + def get_implicit_classifications(self, lang): + """Return a list of classification strings which should always appear in posts_per_classification.""" + return [""] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + return [""] + + def get_classification_friendly_name(self, classification, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return self.site.config["BLOG_TITLE"](lang) + + def get_path(self, classification, lang, type='page'): + """A path handler for the given classification.""" + if type == 'rss': + return [self.site.config['RSS_PATH']], True + # 'page' (index) or 'feed' (Atom) + page_number = None + if type == 'page': + # Interpret argument as page number + try: + page_number = int(classification) + except: + pass + return [self.site.config['INDEX_PATH']], 'always', page_number + + def provide_context_and_uptodate(self, classification, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" kw = { - "translations": self.site.config['TRANSLATIONS'], - "messages": self.site.MESSAGES, - "output_folder": self.site.config['OUTPUT_FOLDER'], - "feed_length": self.site.config['FEED_LENGTH'], - "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "filters": self.site.config['FILTERS'], - "index_file": self.site.config['INDEX_FILE'], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "index_display_post_count": self.site.config['INDEX_DISPLAY_POST_COUNT'], - "indexes_title": self.site.config['INDEXES_TITLE'], - "strip_indexes": self.site.config['STRIP_INDEXES'], - "blog_title": self.site.config["BLOG_TITLE"], - "generate_atom": self.site.config["GENERATE_ATOM"], - "site_url": self.site.config["SITE_URL"], } - - template_name = "index.tmpl" - for lang in kw["translations"]: - 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("index" + feed, None, 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("index" + feed, None, lang), i, displayed_i, - lang, self.site, force_addition, extension) - - filtered_posts = self._get_filtered_posts(lang, kw["show_untranslated_posts"]) - - indexes_title = kw['indexes_title'](lang) or kw['blog_title'](lang) - self.number_of_pages[lang] = self._compute_number_of_pages(filtered_posts, kw['index_display_post_count']) - - context = {} - context["pagekind"] = ["main_index", "index"] - - 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 = { - "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'], + context = { + "title": self.site.config["INDEXES_TITLE"](lang) or self.site.config["BLOG_TITLE"](lang), + "description": self.site.config["BLOG_DESCRIPTION"](lang), + "pagekind": ["main_index", "index"], } - 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. - - Example: - - link://index/3 => /index-3.html - """ - extension = None - if is_feed: - extension = ".atom" - index_file = os.path.splitext(self.site.config['INDEX_FILE'])[0] + extension - else: - index_file = self.site.config['INDEX_FILE'] - if lang in self.number_of_pages: - number_of_pages = self.number_of_pages[lang] - else: - number_of_pages = self._compute_number_of_pages(self._get_filtered_posts(lang, self.site.config['SHOW_UNTRANSLATED_POSTS']), self.site.config['INDEX_DISPLAY_POST_COUNT']) - self.number_of_pages[lang] = number_of_pages - return utils.adjust_name_for_index_path_list([_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['INDEX_PATH'], - index_file] if _f], - name, - utils.get_displayed_page_number(name, number_of_pages, self.site), - lang, - 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. - - Example: - - 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) + kw.update(context) + return context, kw diff --git a/nikola/plugins/task/page_index.plugin b/nikola/plugins/task/page_index.plugin new file mode 100644 index 0000000000..416e18eace --- /dev/null +++ b/nikola/plugins/task/page_index.plugin @@ -0,0 +1,12 @@ +[Core] +name = classify_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 = Taxonomy diff --git a/nikola/plugins/task/page_index.py b/nikola/plugins/task/page_index.py new file mode 100644 index 0000000000..818b7a29b1 --- /dev/null +++ b/nikola/plugins/task/page_index.py @@ -0,0 +1,104 @@ +# -*- 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 page index.""" + +from __future__ import unicode_literals + +from nikola.plugin_categories import Taxonomy + + +class PageIndex(Taxonomy): + """Classify for the page index.""" + + name = "classify_page_index" + + classification_name = "page_index_folder" + metadata_name = None + overview_page_variable_name = "page_folder" + more_than_one_classifications_per_post = False + has_hierarchy = True + include_posts_from_subhierarchies = False + show_list_as_index = False + generate_atom_feeds_for_post_lists = False + template_for_single_list = "list.tmpl" + template_for_classification_overview = None + always_disable_rss = True + apply_to_posts = False + apply_to_pages = True + omit_empty_classifications = True + also_create_classifications_from_other_languages = False + + def is_enabled(self, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + return self.site.config["PAGE_INDEX"] + + def classify(self, post, lang): + """Classify the given post for the given language.""" + destpath = post.destination_path(lang, sep='/') + index_len = len(self.site.config["INDEX_FILE"]) + if destpath[-(1 + index_len):] == '/' + self.site.config["INDEX_FILE"]: + destpath = destpath[:-(1 + index_len)] + i = destpath.rfind('/') + return destpath[:i] if i >= 0 else '' + + def get_classification_friendly_name(self, hierarchy, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return '/'.join(hierarchy) + + def get_path(self, hierarchy, lang, type='page'): + """A path handler for the given classification.""" + return hierarchy, 'always' + + def extract_hierarchy(self, dirname): + """Given a classification, return a list of parts in the hierarchy.""" + return dirname.split('/') if dirname else [] + + def recombine_classification_from_hierarchy(self, hierarchy): + """Given a list of parts in the hierarchy, return the classification string.""" + return '/'.join(hierarchy) + + def provide_context_and_uptodate(self, dirname, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + kw = { + "translations": self.site.config['TRANSLATIONS'], + "filters": self.site.config['FILTERS'], + } + context = { + "title": self.site.config['BLOG_TITLE'](lang), + "pagekind": ["list", "front_page"] if dirname == '' else ["list"], + } + kw.update(context) + return context, kw + + def should_generate_classification_list(self, dirname, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + short_destination = dirname + '/' + self.site.config['INDEX_FILE'] + 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, sep='/') == short_destination: + return False + return True diff --git a/nikola/plugins/task/rss.py b/nikola/plugins/task/rss.py deleted file mode 100644 index 780559bcf3..0000000000 --- a/nikola/plugins/task/rss.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- 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. - -"""Generate RSS feeds.""" - -from __future__ import unicode_literals, print_function -import os -try: - from urlparse import urljoin -except ImportError: - from urllib.parse import urljoin # NOQA - -from nikola import utils -from nikola.nikola import _enclosure -from nikola.plugin_categories import Task - - -class GenerateRSS(Task): - """Generate RSS feeds.""" - - name = "generate_rss" - - def set_site(self, site): - """Set Nikola site.""" - site.register_path_handler('rss', self.rss_path) - return super(GenerateRSS, self).set_site(site) - - def gen_tasks(self): - """Generate RSS feeds.""" - kw = { - "translations": self.site.config["TRANSLATIONS"], - "filters": self.site.config["FILTERS"], - "blog_title": self.site.config["BLOG_TITLE"], - "site_url": self.site.config["SITE_URL"], - "base_url": self.site.config["BASE_URL"], - "blog_description": self.site.config["BLOG_DESCRIPTION"], - "output_folder": self.site.config["OUTPUT_FOLDER"], - "feed_teasers": self.site.config["FEED_TEASERS"], - "feed_plain": self.site.config["FEED_PLAIN"], - "show_untranslated_posts": self.site.config['SHOW_UNTRANSLATED_POSTS'], - "feed_length": self.site.config['FEED_LENGTH'], - "feed_previewimage": self.site.config["FEED_PREVIEWIMAGE"], - "tzinfo": self.site.tzinfo, - "feed_read_more_link": self.site.config["FEED_READ_MORE_LINK"], - "feed_links_append_query": self.site.config["FEED_LINKS_APPEND_QUERY"], - } - self.site.scan_posts() - # Check for any changes in the state of use_in_feeds for any post. - # Issue #934 - kw['use_in_feeds_status'] = ''.join( - ['T' if x.use_in_feeds else 'F' for x in self.site.timeline] - ) - yield self.group_task() - for lang in kw["translations"]: - output_name = os.path.join(kw['output_folder'], - self.site.path("rss", None, lang)) - deps = [] - deps_uptodate = [] - if kw["show_untranslated_posts"]: - posts = self.site.posts[:kw['feed_length']] - else: - posts = [x for x in self.site.posts 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("rss", None, lang).lstrip('/')) - - task = { - 'basename': 'generate_rss', - '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"], - kw["blog_description"](lang), 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.task.rss')] + deps_uptodate, - } - yield utils.apply_filters(task, kw['filters']) - - def rss_path(self, name, lang): - """A link to the RSS feed path. - - Example: - - link://rss => /blog/rss.xml - """ - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['RSS_PATH'], 'rss.xml'] if _f] diff --git a/nikola/plugins/task/sections.plugin b/nikola/plugins/task/sections.plugin new file mode 100644 index 0000000000..146f2f82df --- /dev/null +++ b/nikola/plugins/task/sections.plugin @@ -0,0 +1,12 @@ +[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 = Taxonomy diff --git a/nikola/plugins/task/sections.py b/nikola/plugins/task/sections.py new file mode 100644 index 0000000000..42be9acf91 --- /dev/null +++ b/nikola/plugins/task/sections.py @@ -0,0 +1,119 @@ +# -*- 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 ClassifySections(Taxonomy): + """Classify the posts by sections.""" + + name = "classify_sections" + + classification_name = "section_index" + metadata_name = None + overview_page_variable_name = "sections" + more_than_one_classifications_per_post = False + has_hierarchy = False + generate_atom_feeds_for_post_lists = False + template_for_classification_overview = 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 = site.config["POSTS_SECTION_ARE_INDEXES"] + self.template_for_single_list = "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, lang=None): + """Return True if this taxonomy is enabled, or False otherwise.""" + 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.""" + return [post.section_slug(lang)] + + def _get_section_name(self, section, lang): + # Check whether we have a name for this section + if section in self.site.config['POSTS_SECTION_NAME'](lang): + return self.site.config['POSTS_SECTION_NAME'](lang)[section] + else: + return section.replace('-', ' ').title() + + def get_classification_friendly_name(self, section, lang, only_last_component=False): + """Extract a friendly name from the classification.""" + return self._get_section_name(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], 'always' + + def provide_context_and_uptodate(self, section, lang): + """Provide data for the context and the uptodate list for the list of the given classifiation.""" + kw = { + "messages": self.site.MESSAGES, + } + section_name = self._get_section_name(section, lang) + # Compose section title + section_title = section_name + posts_section_title = self.site.config['POSTS_SECTION_TITLE'](lang) + if isinstance(posts_section_title, dict): + if section in posts_section_title: + section_title = posts_section_title[section] + elif isinstance(posts_section_title, utils.bytes_str, utils.unicode_str): + section_title = posts_section_title + section_title = section_title.format(name=section_name) + # Compose context + context = { + "title": section_title, + "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 + + 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, 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) diff --git a/nikola/plugins/task/tags.py b/nikola/plugins/task/tags.py index 6ee0e4a987..b18d5c63af 100644 --- a/nikola/plugins/task/tags.py +++ b/nikola/plugins/task/tags.py @@ -75,7 +75,7 @@ def gen_tasks(self): "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"], + "feed_links_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'], "taglist_minimum_post_count": self.site.config['TAGLIST_MINIMUM_POSTS'], @@ -379,7 +379,7 @@ def tag_rss(self, tag, lang, posts, kw, is_category): (lang, "{0} ({1})".format(kw["blog_title"](lang), self._get_title(tag, is_category)), kw["site_url"], None, post_list, output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], - feed_url, _enclosure, kw["feed_link_append_query"]))], + feed_url, _enclosure, kw["feed_links_append_query"]))], 'clean': True, 'uptodate': [utils.config_changed(kw, 'nikola.plugins.task.tags:rss')] + deps_uptodate, 'task_dep': ['render_posts'], diff --git a/nikola/plugins/task/rss.plugin b/nikola/plugins/task/taxonomies.plugin similarity index 51% rename from nikola/plugins/task/rss.plugin rename to nikola/plugins/task/taxonomies.plugin index 4dd8aba08f..5dbb4f13de 100644 --- a/nikola/plugins/task/rss.plugin +++ b/nikola/plugins/task/taxonomies.plugin @@ -1,13 +1,12 @@ [Core] -name = generate_rss -module = rss +name = render_taxonomies +module = taxonomies [Documentation] author = Roberto Alsina version = 1.0 website = https://getnikola.com/ -description = Generate RSS feeds. +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..fbb306ff99 --- /dev/null +++ b/nikola/plugins/task/taxonomies.py @@ -0,0 +1,307 @@ +# -*- 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 + + +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 taxonomy 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['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: + 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"]], + alg=natsort.ns.F | natsort.ns.IC) + taxonomy.sort_classifications(classifications, lang) + + # Set up classifications in context + 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: + 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 clipped_flat_hierarchy] + + # Prepare rendering + context["permalink"] = self.site.link("{}_index".format(taxonomy.classification_name), None, lang) + 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, + [], + 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, description, 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(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 = [] + for post in filtered_posts: + deps += post.deps(lang) + deps_uptodate += post.deps_uptodate(lang) + blog_title = kw["blog_title"](lang) + task = { + 'basename': str(self.name), + 'name': output_name, + 'file_dep': deps, + 'targets': [output_name], + 'actions': [(utils.generic_rss_renderer, + (lang, "{0} ({1})".format(blog_title, title) if blog_title != title else blog_title, + kw["site_url"], description, filtered_posts, + output_name, kw["feed_teasers"], kw["feed_plain"], kw['feed_length'], + feed_url, _enclosure, kw["feed_links_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 "pagekind" not in context: + context["pagekind"] = ["index", "tag_page"] + template_name = taxonomy.template_for_single_list + + 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(self.site.config['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 = taxonomy.template_for_single_list + output_name = os.path.join(self.site.config['OUTPUT_FOLDER'], self.site.path(kind, classification, lang)) + context["lang"] = lang + context["posts"] = filtered_posts + context["kind"] = kind + 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) + yield task + + if taxonomy.generate_atom_feeds_for_post_lists and self.site.config['GENERATE_ATOM']: + yield self._generate_classification_page_as_list_atom(kind, taxonomy, classification, filtered_posts, context, kw, lang) + + def _filter_list(self, post_list, lang): + """Return only the posts which should be shown for this language.""" + if self.site.config["SHOW_UNTRANSLATED_POSTS"]: + return post_list + else: + return [x for x in post_list if x.is_translation_available(lang)] + + def _generate_subclassification_page(self, taxonomy, node, context, kw, lang): + """Render a list of subclassifications.""" + def get_subnode_data(subnode): + return [ + taxonomy.get_classification_friendly_name(subnode.classification_path, lang, only_last_component=True), + self.site.link(taxonomy.classification_name, subnode.classification_name, lang), + len(self._filter_list(self.site.posts_per_classification[taxonomy.classification_name][lang][subnode.classification_name], lang)) + ] + + items = [get_subnode_data(subnode) for subnode in node.children] + context = copy(context) + context["lang"] = lang + context["permalink"] = self.site.link(taxonomy.classification_name, node.classification_name, lang) + if "pagekind" not in context: + context["pagekind"] = ["list", "archive_page"] + context["items"] = items + task = self.site.generic_post_list_renderer( + lang, + [], + os.path.join(kw['output_folder'], self.site.path(taxonomy.classification_name, node.classification_name, lang)), + taxonomy.show_list_as_subcategories_list, + kw['filters'], + context, + ) + task_cfg = {1: kw, 2: items} + task['uptodate'] = task['uptodate'] + [utils.config_changed(task_cfg, 'nikola.plugins.task.taxonomy')] + task['basename'] = self.name + return task + + def _generate_classification_page(self, taxonomy, classification, post_list, lang): + """Render index or post list and associated feeds per classification.""" + # Filter list + filtered_posts = self._filter_list(post_list, lang) + if len(filtered_posts) == 0 and taxonomy.omit_empty_classifications: + return + # Should we create this list? + if not taxonomy.should_generate_classification_list(classification, filtered_posts, lang): + return + # Get data + context, kw = taxonomy.provide_context_and_uptodate(classification, lang) + kw = copy(kw) + kw['messages'] = self.site.MESSAGES + 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_links_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'] + context = copy(context) + context["permalink"] = self.site.link(taxonomy.classification_name, classification, lang) + # Decide what to do + if taxonomy.has_hierarchy and taxonomy.show_list_as_subcategories_list: + # Determine whether there are subcategories + node = self.site.hierarchy_lookup_per_classification[taxonomy.classification_name][lang][classification] + # Are there subclassifications? + if len(node.children) > 0: + # Yes: create list with subclassifications instead of list of posts + yield self._generate_subclassification_page(taxonomy, node, context, kw, lang) + return + # Generate RSS feed + if kw["generate_rss"] and not taxonomy.always_disable_rss: + yield self._generate_classification_page_as_rss(taxonomy, classification, filtered_posts, context['title'], context.get("description"), 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 [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 + 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): + 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