diff options
Diffstat (limited to 'nikola/plugins/task/archive.py')
| -rw-r--r-- | nikola/plugins/task/archive.py | 404 |
1 files changed, 199 insertions, 205 deletions
diff --git a/nikola/plugins/task/archive.py b/nikola/plugins/task/archive.py index 126aed4..4cbf215 100644 --- a/nikola/plugins/task/archive.py +++ b/nikola/plugins/task/archive.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,222 +24,216 @@ # 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 - +from collections import defaultdict -class Archive(Task): +import natsort - """Render the post archives.""" - - name = "render_archive" +import nikola.utils +from nikola.plugin_categories import Taxonomy + + +class Archive(Taxonomy): + """Classify the post archives.""" + + name = "classify_archive" + + classification_name = "archive" + 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 + subcategories_list_template = "list.tmpl" + template_for_classification_overview = None + always_disable_rss = True + always_disable_atom = True + apply_to_posts = True + apply_to_pages = False + minimum_post_count_per_classification_in_overview = 1 + omit_empty_classifications = False + add_other_languages_variable = True + path_handler_docstrings = { + 'archive_index': False, + 'archive': """Link to archive path, name is the year. + + Example: + + link://archive/2013 => /archives/2013/index.html""", + 'archive_atom': False, + 'archive_rss': 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) - 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 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]) + # 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 = not site.config['CREATE_FULL_ARCHIVES'] + self.show_list_as_index = site.config['ARCHIVES_ARE_INDEXES'] + self.template_for_single_list = "archiveindex.tmpl" if site.config['ARCHIVES_ARE_INDEXES'] else "archive.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().set_site(site) + + 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.""" + classification = self.extract_hierarchy(classification) + if len(classification) == 0: + return self.site.MESSAGES[lang]['Archive'] + elif len(classification) == 1: + return classification[0] + elif len(classification) == 2: + if only_last_component: + date_str = "{month}" + else: + date_str = "{month_year}" + return nikola.utils.LocaleBorg().format_date_in_string( + date_str, + datetime.date(int(classification[0]), int(classification[1]), 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) + if only_last_component: + return str(classification[2]) + return nikola.utils.LocaleBorg().format_date_in_string( + "{month_day_year}", + datetime.date(int(classification[0]), int(classification[1]), int(classification[2])), + lang) + + def get_path(self, classification, lang, dest_type='page'): + """Return a path for the given classification.""" + components = [self.site.config['ARCHIVE_PATH'](lang)] + 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(self.site.config['ARCHIVE_FILENAME'](lang)) + 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, node=None): + """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)) 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] for month, link 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)) 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): - """Return archive paths.""" - 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 - 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] + 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"] + elif len(hierarchy) == 1: + title = kw["messages"][lang]["Posts for year %s"] % hierarchy[0] + elif len(hierarchy) == 2: + title = nikola.utils.LocaleBorg().format_date_in_string( + kw["messages"][lang]["Posts for {month_year}"], + datetime.date(int(hierarchy[0]), int(hierarchy[1]), 1), + lang) + elif len(hierarchy) == 3: + title = nikola.utils.LocaleBorg().format_date_in_string( + kw["messages"][lang]["Posts for {month_day_year}"], + datetime.date(int(hierarchy[0]), int(hierarchy[1]), int(hierarchy[2])), + lang) else: - return [_f for _f in [self.site.config['TRANSLATIONS'][lang], - self.site.config['ARCHIVE_PATH'], - archive_file] if _f] + raise Exception("Cannot interpret classification {}!".format(repr(classification))) - def archive_atom_path(self, name, lang): - """Return Atom archive paths.""" - return self.archive_path(name, lang, is_feed=True) + context = { + "title": title, + "pagekind": [page_kind, "archive_page"], + "create_archive_navigation": self.site.config["CREATE_ARCHIVE_NAVIGATION"], + "archive_name": classification + } + + # Generate links for hierarchies + if context["create_archive_navigation"]: + if hierarchy: + # Up level link makes sense only if this is not the top-level + # page (hierarchy is empty) + parent = '/'.join(hierarchy[:-1]) + context["up_archive"] = self.site.link('archive', parent, lang) + context["up_archive_name"] = self.get_classification_friendly_name(parent, lang) + else: + context["up_archive"] = None + context["up_archive_name"] = None + + nodelevel = len(hierarchy) + flat_samelevel = self.archive_navigation[lang][nodelevel] + idx = flat_samelevel.index(classification) + if idx == -1: + raise Exception("Cannot find classification {0} in flat hierarchy!".format(classification)) + previdx, nextidx = idx - 1, idx + 1 + # If the previous index is -1, or the next index is 1, the previous/next archive does not exist. + context["previous_archive"] = self.site.link('archive', flat_samelevel[previdx], lang) if previdx != -1 else None + context["previous_archive_name"] = self.get_classification_friendly_name(flat_samelevel[previdx], lang) if previdx != -1 else None + context["next_archive"] = self.site.link('archive', flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None + context["next_archive_name"] = self.get_classification_friendly_name(flat_samelevel[nextidx], lang) if nextidx != len(flat_samelevel) else None + context["archive_nodelevel"] = nodelevel + context["has_archive_navigation"] = bool(context["previous_archive"] or context["up_archive"] or context["next_archive"]) + else: + context["has_archive_navigation"] = False + kw.update(context) + return context, kw + + 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.""" + # Build a lookup table for archive navigation, if we’ll need one. + if self.site.config['CREATE_ARCHIVE_NAVIGATION']: + if flat_hierarchy_per_lang is None: + raise ValueError('Archives need flat_hierarchy_per_lang') + self.archive_navigation = {} + for lang, flat_hierarchy in flat_hierarchy_per_lang.items(): + self.archive_navigation[lang] = defaultdict(list) + for node in flat_hierarchy: + if not self.site.config["SHOW_UNTRANSLATED_POSTS"]: + if not [x for x in posts_per_classification_per_language[lang][node.classification_name] if x.is_translation_available(lang)]: + continue + self.archive_navigation[lang][len(node.classification_path)].append(node.classification_name) + + # We need to sort it. Natsort means it’s year 10000 compatible! + for k, v in self.archive_navigation[lang].items(): + self.archive_navigation[lang][k] = natsort.natsorted(v, alg=natsort.ns.F | natsort.ns.IC) + + return super().postprocess_posts_per_classification(posts_per_classification_per_language, flat_hierarchy_per_lang, hierarchy_lookup_per_lang) + + def should_generate_classification_page(self, classification, post_list, lang): + """Only generates list of posts for classification if this function returns True.""" + return classification == '' or len(post_list) > 0 + + def get_other_language_variants(self, classification, lang, classifications_per_language): + """Return a list of variants of the same classification in other languages.""" + return [(other_lang, classification) for other_lang, lookup in classifications_per_language.items() if classification in lookup and other_lang != lang] |
