summaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task/archive.py
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/plugins/task/archive.py')
-rw-r--r--nikola/plugins/task/archive.py404
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]