Repository: getnikola/nikola

Issue:
Augment post-list directive with more filters
Currently, filtering posts that go into post-lists only works with tags or slugs. In the legacy site I currently want to migrate, I have views in the form of /press/releases/<year>/ that would filter by date but could also filter by globbing the path name. I wonder if there is a generic way to filter by multiple meta data entries.

While we're at it: once you add powerful filters to post-lists and get something what is called "the loop" in php sites, i.e. filter/sort/slice/templatize, the currently implemented indexes for posts-by-year, posts-by-author and posts-by-category just become special cases of these posts-lists. You could basically create paginated indexes for all posts-list queries and offer rss feeds for each of them, too.



Discussion:
For `date`: https://gist.github.com/Kwpolska/2779f7d1ea006971aad1

(I’ll relicense to MIT if wanted)

`categories` added.


Resolving patch:
diff --git a/nikola/packages/datecond/__init__.py b/nikola/packages/datecond/__init__.py
new file mode 100644
--- /dev/null
+++ b/nikola/packages/datecond/__init__.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+# Date Conditionals (datecond)
+# Version 0.1.2
+# Copyright © 2015-2016, Chris Warrick.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions, and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions, and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the author of this software nor the names of
+#    contributors to this software may be used to endorse or promote
+#    products derived from this software without specific prior written
+#    consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Date range parser."""
+
+from __future__ import print_function, unicode_literals
+import dateutil.parser
+import re
+import operator
+
+
+__all__ = ('date_in_range',)
+CLAUSE = re.compile('(year|month|day|hour|minute|second|weekday|isoweekday)?'
+                    ' ?(==|!=|<=|>=|<|>) ?(.*)')
+OPERATORS = {
+    '==': operator.eq,
+    '!=': operator.ne,
+    '<=': operator.le,
+    '>=': operator.ge,
+    '<': operator.lt,
+    '>': operator.gt,
+}
+
+
+def date_in_range(date_range, date, debug=True):
+    """Check if date is in the range specified.
+
+    Format:
+    * comma-separated clauses (AND)
+    * clause: attribute comparison_operator value (spaces optional)
+        * attribute: year, month, day, hour, month, second, weekday, isoweekday
+          or empty for full datetime
+        * comparison_operator: == != <= >= < >
+        * value: integer or dateutil-compatible date input
+    """
+    out = True
+
+    for item in date_range.split(','):
+        attribute, comparison_operator, value = CLAUSE.match(
+            item.strip()).groups()
+        if attribute in ('weekday', 'isoweekday'):
+            left = getattr(date, attribute)()
+            right = int(value)
+        elif attribute:
+            left = getattr(date, attribute)
+            right = int(value)
+        else:
+            left = date
+            right = dateutil.parser.parse(value)
+        if debug:  # pragma: no cover
+            print("    <{0} {1} {2}>".format(left, comparison_operator, right))
+        out = out and OPERATORS[comparison_operator](left, right)
+    return out
diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py
--- a/nikola/plugins/compile/rest/post_list.py
+++ b/nikola/plugins/compile/rest/post_list.py
@@ -37,6 +37,7 @@
 
 from nikola import utils
 from nikola.plugin_categories import RestExtension
+from nikola.packages.datecond import date_in_range
 
 # WARNING: the directive name is post-list
 #          (with a DASH instead of an UNDERSCORE)
@@ -86,10 +87,19 @@ class PostList(Directive):
         Reverse the order of the post-list.
         Defaults is to not reverse the order of posts.
 
-    ``sort``: string
+    ``sort`` : string
         Sort post list by one of each post's attributes, usually ``title`` or a
         custom ``priority``.  Defaults to None (chronological sorting).
 
+    ``date`` : string
+        Show posts that match date range specified by this option. Format:
+
+        * comma-separated clauses (AND)
+        * clause: attribute comparison_operator value (spaces optional)
+          * attribute: year, month, day, hour, month, second, weekday, isoweekday; or empty for full datetime
+          * comparison_operator: == != <= >= < >
+          * value: integer or dateutil-compatible date input
+
     ``tags`` : string [, string...]
         Filter posts to show only posts having at least one of the ``tags``.
         Defaults to None.
@@ -136,6 +146,7 @@ class PostList(Directive):
         'lang': directives.unchanged,
         'template': directives.path,
         'id': directives.unchanged,
+        'date': directives.unchanged,
     }
 
     def run(self):
@@ -154,17 +165,21 @@ def run(self):
         lang = self.options.get('lang', utils.LocaleBorg().current_lang)
         template = self.options.get('template', 'post_list_directive.tmpl')
         sort = self.options.get('sort')
+        date = self.options.get('date')
 
         output = _do_post_list(start, stop, reverse, tags, categories, slugs, post_type,
-                               show_all, lang, template, sort, state=self.state, site=self.site)
+                               show_all, lang, template, sort, state=self.state, site=self.site, date=date)
         self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE")
-        return [nodes.raw('', output, format='html')]
+        if output:
+            return [nodes.raw('', output, format='html')]
+        else:
+            return []
 
 
 def _do_post_list(start=None, stop=None, reverse=False, tags=None, categories=None,
                   slugs=None, post_type='post', show_all=False, lang=None,
                   template='post_list_directive.tmpl', sort=None, id=None,
-                  data=None, state=None, site=None):
+                  data=None, state=None, site=None, date=None):
     if lang is None:
         lang = utils.LocaleBorg().current_lang
     if site.invariant:  # for testing purposes
@@ -213,6 +228,9 @@ def _do_post_list(start=None, stop=None, reverse=False, tags=None, categories=No
     if sort:
         filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC)
 
+    if date:
+        filtered_timeline = [p for p in filtered_timeline if date_in_range(date, p.date)]
+
     for post in filtered_timeline[start:stop:step]:
         if slugs:
             cont = True


Test patch: