Merge pull request #3148 from djramones/period-archives-context

This commit is contained in:
Justin Mayer 2023-10-28 22:22:11 +02:00 committed by GitHub
commit 85bf98232d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 289 additions and 67 deletions

View file

@ -564,44 +564,47 @@ written over time.
Example usage:: Example usage::
YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html' YEAR_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/index.html'
YEAR_ARCHIVE_URL = 'posts/{date:%Y}/'
MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html' MONTH_ARCHIVE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/index.html'
MONTH_ARCHIVE_URL = 'posts/{date:%Y}/{date:%b}/'
With these settings, Pelican will create an archive of all your posts for the With these settings, Pelican will create an archive of all your posts for the
year at (for instance) ``posts/2011/index.html`` and an archive of all your year at (for instance) ``posts/2011/index.html`` and an archive of all your
posts for the month at ``posts/2011/Aug/index.html``. posts for the month at ``posts/2011/Aug/index.html``. These can be accessed
through the URLs ``posts/2011/`` and ``posts/2011/Aug/``, respectively.
.. note:: .. note::
Period archives work best when the final path segment is ``index.html``. Period archives work best when the final path segment is ``index.html``.
This way a reader can remove a portion of your URL and automatically arrive This way a reader can remove a portion of your URL and automatically arrive
at an appropriate archive of posts, without having to specify a page name. at an appropriate archive of posts, without having to specify a page name.
.. data:: YEAR_ARCHIVE_URL = ''
The URL to use for per-year archives of your posts. Used only if you have
the ``{url}`` placeholder in ``PAGINATION_PATTERNS``.
.. data:: YEAR_ARCHIVE_SAVE_AS = '' .. data:: YEAR_ARCHIVE_SAVE_AS = ''
The location to save per-year archives of your posts. The location to save per-year archives of your posts.
.. data:: MONTH_ARCHIVE_URL = '' .. data:: YEAR_ARCHIVE_URL = ''
The URL to use for per-month archives of your posts. Used only if you have The URL to use for per-year archives of your posts. You should set this if
the ``{url}`` placeholder in ``PAGINATION_PATTERNS``. you enable per-year archives.
.. data:: MONTH_ARCHIVE_SAVE_AS = '' .. data:: MONTH_ARCHIVE_SAVE_AS = ''
The location to save per-month archives of your posts. The location to save per-month archives of your posts.
.. data:: DAY_ARCHIVE_URL = '' .. data:: MONTH_ARCHIVE_URL = ''
The URL to use for per-day archives of your posts. Used only if you have the The URL to use for per-month archives of your posts. You should set this if
``{url}`` placeholder in ``PAGINATION_PATTERNS``. you enable per-month archives.
.. data:: DAY_ARCHIVE_SAVE_AS = '' .. data:: DAY_ARCHIVE_SAVE_AS = ''
The location to save per-day archives of your posts. The location to save per-day archives of your posts.
.. data:: DAY_ARCHIVE_URL = ''
The URL to use for per-day archives of your posts. You should set this if
you enable per-day archives.
``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the ``DIRECT_TEMPLATES`` work a bit differently than noted above. Only the
``_SAVE_AS`` settings are available, but it is available for any direct ``_SAVE_AS`` settings are available, but it is available for any direct
template. template.

View file

@ -71,6 +71,8 @@ All templates will receive the variables defined in your settings file, as long
as they are in all-caps. You can access them directly. as they are in all-caps. You can access them directly.
.. _common_variables:
Common Variables Common Variables
---------------- ----------------
@ -92,6 +94,10 @@ dates The same list of articles, but ordered by date,
ascending. ascending.
hidden_articles The list of hidden articles hidden_articles The list of hidden articles
drafts The list of draft articles drafts The list of draft articles
period_archives A dictionary containing elements related to
time-period archives (if enabled). See the section
:ref:`Listing and Linking to Period Archives
<period_archives_variable>` for details.
authors A list of (author, articles) tuples, containing all authors A list of (author, articles) tuples, containing all
the authors and corresponding articles (values) the authors and corresponding articles (values)
categories A list of (category, articles) tuples, containing categories A list of (category, articles) tuples, containing
@ -348,6 +354,63 @@ period_archives.html template
<https://github.com/getpelican/pelican/blob/master/pelican/themes/simple/templates/period_archives.html>`_. <https://github.com/getpelican/pelican/blob/master/pelican/themes/simple/templates/period_archives.html>`_.
.. _period_archives_variable:
Listing and Linking to Period Archives
""""""""""""""""""""""""""""""""""""""
The ``period_archives`` variable can be used to generate a list of links to
the set of period archives that Pelican generates. As a :ref:`common variable
<common_variables>`, it is available for use in any template, so you
can implement such an index in a custom direct template, or in a sidebar
visible across different site pages.
``period_archives`` is a dict that may contain ``year``, ``month``, and/or
``day`` keys, depending on which ``*_ARCHIVE_SAVE_AS`` settings are enabled.
The corresponding value is a list of dicts, where each dict in turn represents
a time period (ordered according to the ``NEWEST_FIRST_ARCHIVES`` setting)
with the following keys and values:
=================== ===================================================
Key Value
=================== ===================================================
period The same tuple as described in
``period_archives.html``, e.g.
``(2023, 'June', 18)``.
period_num The same tuple as described in
``period_archives.html``, e.g. ``(2023, 6, 18)``.
url The URL to the period archive page, e.g.
``posts/2023/06/18/``. This is controlled by the
corresponding ``*_ARCHIVE_URL`` setting.
save_as The path to the save location of the period archive
page file, e.g. ``posts/2023/06/18/index.html``.
This is used internally by Pelican and is usually
not relevant to themes.
articles A list of :ref:`Article <object-article>` objects
that fall under the time period.
dates Same list as ``articles``, but ordered according
to the ``NEWEST_FIRST_ARCHIVES`` setting.
=================== ===================================================
Here is an example of how ``period_archives`` can be used in a template:
.. code-block:: html+jinja
<ul>
{% for archive in period_archives.month %}
<li>
<a href="{{ SITEURL }}/{{ archive.url }}">
{{ archive.period | reverse | join(' ') }} ({{ archive.articles|count }})
</a>
</li>
{% endfor %}
</ul>
You can change ``period_archives.month`` in the ``for`` statement to
``period_archives.year`` or ``period_archives.day`` as appropriate, depending
on the time period granularity desired.
Objects Objects
======= =======

View file

@ -295,6 +295,7 @@ class ArticlesGenerator(CachingGenerator):
self.drafts = [] # only drafts in default language self.drafts = [] # only drafts in default language
self.drafts_translations = [] self.drafts_translations = []
self.dates = {} self.dates = {}
self.period_archives = defaultdict(list)
self.tags = defaultdict(list) self.tags = defaultdict(list)
self.categories = defaultdict(list) self.categories = defaultdict(list)
self.related_posts = [] self.related_posts = []
@ -493,64 +494,17 @@ class ArticlesGenerator(CachingGenerator):
except PelicanTemplateNotFound: except PelicanTemplateNotFound:
template = self.get_template('archives') template = self.get_template('archives')
period_save_as = { for granularity in self.period_archives:
'year': self.settings['YEAR_ARCHIVE_SAVE_AS'], for period in self.period_archives[granularity]:
'month': self.settings['MONTH_ARCHIVE_SAVE_AS'],
'day': self.settings['DAY_ARCHIVE_SAVE_AS'],
}
period_url = {
'year': self.settings['YEAR_ARCHIVE_URL'],
'month': self.settings['MONTH_ARCHIVE_URL'],
'day': self.settings['DAY_ARCHIVE_URL'],
}
period_date_key = {
'year': attrgetter('date.year'),
'month': attrgetter('date.year', 'date.month'),
'day': attrgetter('date.year', 'date.month', 'date.day')
}
def _generate_period_archives(dates, key, save_as_fmt, url_fmt):
"""Generate period archives from `dates`, grouped by
`key` and written to `save_as`.
"""
# `dates` is already sorted by date
for _period, group in groupby(dates, key=key):
archive = list(group)
articles = [a for a in self.articles if a in archive]
# arbitrarily grab the first date so that the usual
# format string syntax can be used for specifying the
# period archive dates
date = archive[0].date
save_as = save_as_fmt.format(date=date)
url = url_fmt.format(date=date)
context = self.context.copy() context = self.context.copy()
context['period'] = period['period']
context['period_num'] = period['period_num']
if key == period_date_key['year']: write(period['save_as'], template, context,
context["period"] = (_period,) articles=period['articles'], dates=period['dates'],
context["period_num"] = (_period,) template_name='period_archives', blog=True,
else: url=period['url'], all_articles=self.articles)
month_name = calendar.month_name[_period[1]]
if key == period_date_key['month']:
context["period"] = (_period[0],
month_name)
else:
context["period"] = (_period[0],
month_name,
_period[2])
context["period_num"] = tuple(_period)
write(save_as, template, context, articles=articles,
dates=archive, template_name='period_archives',
blog=True, url=url, all_articles=self.articles)
for period in 'year', 'month', 'day':
save_as = period_save_as[period]
url = period_url[period]
if save_as:
key = period_date_key[period]
_generate_period_archives(self.dates, key, save_as, url)
def generate_direct_templates(self, write): def generate_direct_templates(self, write):
"""Generate direct templates pages""" """Generate direct templates pages"""
@ -690,6 +644,9 @@ class ArticlesGenerator(CachingGenerator):
self.dates.sort(key=attrgetter('date'), self.dates.sort(key=attrgetter('date'),
reverse=self.context['NEWEST_FIRST_ARCHIVES']) reverse=self.context['NEWEST_FIRST_ARCHIVES'])
self.period_archives = self._build_period_archives(
self.dates, self.articles, self.settings)
# and generate the output :) # and generate the output :)
# order the categories per name # order the categories per name
@ -704,10 +661,80 @@ class ArticlesGenerator(CachingGenerator):
'articles', 'drafts', 'hidden_articles', 'articles', 'drafts', 'hidden_articles',
'dates', 'tags', 'categories', 'dates', 'tags', 'categories',
'authors', 'related_posts')) 'authors', 'related_posts'))
# _update_context flattens dicts, which should not happen to
# period_archives, so we update the context directly for it:
self.context['period_archives'] = self.period_archives
self.save_cache() self.save_cache()
self.readers.save_cache() self.readers.save_cache()
signals.article_generator_finalized.send(self) signals.article_generator_finalized.send(self)
def _build_period_archives(self, sorted_articles, articles, settings):
"""
Compute the groupings of articles, with related attributes, for
per-year, per-month, and per-day archives.
"""
period_archives = defaultdict(list)
period_archives_settings = {
'year': {
'save_as': settings['YEAR_ARCHIVE_SAVE_AS'],
'url': settings['YEAR_ARCHIVE_URL'],
},
'month': {
'save_as': settings['MONTH_ARCHIVE_SAVE_AS'],
'url': settings['MONTH_ARCHIVE_URL'],
},
'day': {
'save_as': settings['DAY_ARCHIVE_SAVE_AS'],
'url': settings['DAY_ARCHIVE_URL'],
},
}
granularity_key_func = {
'year': attrgetter('date.year'),
'month': attrgetter('date.year', 'date.month'),
'day': attrgetter('date.year', 'date.month', 'date.day'),
}
for granularity in 'year', 'month', 'day':
save_as_fmt = period_archives_settings[granularity]['save_as']
url_fmt = period_archives_settings[granularity]['url']
key_func = granularity_key_func[granularity]
if not save_as_fmt:
# the archives for this period granularity are not needed
continue
for period, group in groupby(sorted_articles, key=key_func):
archive = {}
dates = list(group)
archive['dates'] = dates
archive['articles'] = [a for a in articles if a in dates]
# use the first date to specify the period archive URL
# and save_as; the specific date used does not matter as
# they all belong to the same period
d = dates[0].date
archive['save_as'] = save_as_fmt.format(date=d)
archive['url'] = url_fmt.format(date=d)
if granularity == 'year':
archive['period'] = (period,)
archive['period_num'] = (period,)
else:
month_name = calendar.month_name[period[1]]
if granularity == 'month':
archive['period'] = (period[0], month_name)
else:
archive['period'] = (period[0], month_name, period[2])
archive['period_num'] = tuple(period)
period_archives[granularity].append(archive)
return period_archives
def generate_output(self, writer): def generate_output(self, writer):
self.generate_feeds(writer) self.generate_feeds(writer)
self.generate_pages(writer) self.generate_pages(writer)

View file

@ -405,6 +405,135 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertIn(custom_template, self.articles) self.assertIn(custom_template, self.articles)
self.assertIn(standard_template, self.articles) self.assertIn(standard_template, self.articles)
def test_period_archives_context(self):
"""Test correctness of the period_archives context values."""
old_locale = locale.setlocale(locale.LC_ALL)
locale.setlocale(locale.LC_ALL, 'C')
settings = get_settings()
settings['CACHE_PATH'] = self.temp_cache
# No period archives enabled:
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
generator.generate_context()
period_archives = generator.context['period_archives']
self.assertEqual(len(period_archives.items()), 0)
# Year archives enabled:
settings['YEAR_ARCHIVE_SAVE_AS'] = 'posts/{date:%Y}/index.html'
settings['YEAR_ARCHIVE_URL'] = 'posts/{date:%Y}/'
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
generator.generate_context()
period_archives = generator.context['period_archives']
abbreviated_archives = {
granularity: {period['period'] for period in periods}
for granularity, periods in period_archives.items()
}
expected = {'year': {(1970,), (2010,), (2012,), (2014,)}}
self.assertEqual(expected, abbreviated_archives)
# Month archives enabled:
settings['MONTH_ARCHIVE_SAVE_AS'] = \
'posts/{date:%Y}/{date:%b}/index.html'
settings['MONTH_ARCHIVE_URL'] = \
'posts/{date:%Y}/{date:%b}/'
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
generator.generate_context()
period_archives = generator.context['period_archives']
abbreviated_archives = {
granularity: {period['period'] for period in periods}
for granularity, periods in period_archives.items()
}
expected = {
'year': {(1970,), (2010,), (2012,), (2014,)},
'month': {
(1970, 'January'),
(2010, 'December'),
(2012, 'December'),
(2012, 'November'),
(2012, 'October'),
(2014, 'February'),
},
}
self.assertEqual(expected, abbreviated_archives)
# Day archives enabled:
settings['DAY_ARCHIVE_SAVE_AS'] = \
'posts/{date:%Y}/{date:%b}/{date:%d}/index.html'
settings['DAY_ARCHIVE_URL'] = \
'posts/{date:%Y}/{date:%b}/{date:%d}/'
context = get_context(settings)
generator = ArticlesGenerator(
context=context, settings=settings,
path=CONTENT_DIR, theme=settings['THEME'], output_path=None)
generator.generate_context()
period_archives = generator.context['period_archives']
abbreviated_archives = {
granularity: {period['period'] for period in periods}
for granularity, periods in period_archives.items()
}
expected = {
'year': {(1970,), (2010,), (2012,), (2014,)},
'month': {
(1970, 'January'),
(2010, 'December'),
(2012, 'December'),
(2012, 'November'),
(2012, 'October'),
(2014, 'February'),
},
'day': {
(1970, 'January', 1),
(2010, 'December', 2),
(2012, 'December', 20),
(2012, 'November', 29),
(2012, 'October', 30),
(2012, 'October', 31),
(2014, 'February', 9),
},
}
self.assertEqual(expected, abbreviated_archives)
# Further item values tests
filtered_archives = [
p for p in period_archives['day']
if p['period'] == (2014, 'February', 9)
]
self.assertEqual(len(filtered_archives), 1)
sample_archive = filtered_archives[0]
self.assertEqual(sample_archive['period_num'], (2014, 2, 9))
self.assertEqual(
sample_archive['save_as'], 'posts/2014/Feb/09/index.html')
self.assertEqual(
sample_archive['url'], 'posts/2014/Feb/09/')
articles = [
d for d in generator.articles if
d.date.year == 2014 and
d.date.month == 2 and
d.date.day == 9
]
self.assertEqual(len(sample_archive['articles']), len(articles))
dates = [
d for d in generator.dates if
d.date.year == 2014 and
d.date.month == 2 and
d.date.day == 9
]
self.assertEqual(len(sample_archive['dates']), len(dates))
self.assertEqual(sample_archive['dates'][0].title, dates[0].title)
self.assertEqual(sample_archive['dates'][0].date, dates[0].date)
locale.setlocale(locale.LC_ALL, old_locale)
def test_period_in_timeperiod_archive(self): def test_period_in_timeperiod_archive(self):
""" """
Test that the context of a generated period_archive is passed Test that the context of a generated period_archive is passed