Repository: getnikola/nikola

Issue:
Make Post objects sortable/comparable
I'd like to be able to [re]order the posts in a template (e.g., ``tag.tmpl``, and specifically in Jinja in my case) by their titles.

I'm imagining I'd write something like ``for post in posts|sort``.  That does work after adding an ``__lt__`` operator to the Post class:

```python
def __lt__(self, other):
   return self.title() < other.title()
```

I realize that sorting by title is not the only way to sort posts, though, so making the sort filter order in this way may not be the best implementation.  An alternative would be to leverage the "attribute" parameter of Jinja's [sort filter](http://jinja.pocoo.org/docs/dev/templates/#sort), which lets one tell it what to sort by; this would require exposing the Post object's title as an attribute:

```python
@property
def title_str(self):
    return self.title()
```

Then this would work: ``for post in posts|sort(attribute="title_str")``


Discussion:
I can think of a better solution: a smarter `sort_posts` filter/function, available for templates and general use, that would handle all those edge cases. I’ll work on it later.

Resolving patch:
diff --git a/nikola/nikola.py b/nikola/nikola.py
--- a/nikola/nikola.py
+++ b/nikola/nikola.py
@@ -1201,6 +1201,7 @@ def _set_global_context_from_config(self):
         self._GLOBAL_CONTEXT['posts_section_from_meta'] = self.config.get('POSTS_SECTION_FROM_META')
         self._GLOBAL_CONTEXT['posts_section_name'] = self.config.get('POSTS_SECTION_NAME')
         self._GLOBAL_CONTEXT['posts_section_title'] = self.config.get('POSTS_SECTION_TITLE')
+        self._GLOBAL_CONTEXT['sort_posts'] = utils.sort_posts
 
         # IPython theme configuration.  If a website has ipynb enabled in post_pages
         # we should enable the IPython CSS (leaving that up to the theme itself).
diff --git a/nikola/plugins/template/jinja.py b/nikola/plugins/template/jinja.py
--- a/nikola/plugins/template/jinja.py
+++ b/nikola/plugins/template/jinja.py
@@ -38,7 +38,7 @@
     jinja2 = None  # NOQA
 
 from nikola.plugin_categories import TemplateSystem
-from nikola.utils import makedirs, req_missing
+from nikola.utils import makedirs, req_missing, sort_posts
 
 
 class JinjaTemplates(TemplateSystem):
@@ -65,6 +65,7 @@ def set_directories(self, directories, cache_folder):
         self.lookup.trim_blocks = True
         self.lookup.lstrip_blocks = True
         self.lookup.filters['tojson'] = json.dumps
+        self.lookup.filters['sort_posts'] = sort_posts
         self.lookup.globals['enumerate'] = enumerate
         self.lookup.globals['isinstance'] = isinstance
         self.lookup.globals['tuple'] = tuple
diff --git a/nikola/utils.py b/nikola/utils.py
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -35,6 +35,7 @@
 import locale
 import logging
 import natsort
+import operator
 import os
 import re
 import json
@@ -81,8 +82,9 @@
 
 from nikola import DEBUG
 
-__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real', 'get_theme_chain', 'load_messages', 'copy_tree',
-           'copy_file', 'slugify', 'unslugify', 'to_datetime', 'apply_filters',
+__all__ = ('CustomEncoder', 'get_theme_path', 'get_theme_path_real',
+           'get_theme_chain', 'load_messages', 'copy_tree', 'copy_file',
+           'slugify', 'unslugify', 'to_datetime', 'apply_filters',
            'config_changed', 'get_crumbs', 'get_tzname', 'get_asset_path',
            '_reload', 'unicode_str', 'bytes_str', 'unichr', 'Functionary',
            'TranslatableSetting', 'TemplateHookRegistry', 'LocaleBorg',
@@ -91,10 +93,11 @@
            'ask', 'ask_yesno', 'options2docstring', 'os_path_split',
            'get_displayed_page_number', 'adjust_name_for_index_path_list',
            'adjust_name_for_index_path', 'adjust_name_for_index_link',
-           'NikolaPygmentsHTML', 'create_redirect', 'TreeNode', 'clone_treenode',
-           'flatten_tree_structure', 'parse_escaped_hierarchical_category_name',
-           'join_hierarchical_category_path', 'clean_before_deployment', 'indent',
-           'load_data', 'html_unescape')
+           'NikolaPygmentsHTML', 'create_redirect', 'TreeNode',
+           'clone_treenode', 'flatten_tree_structure',
+           'parse_escaped_hierarchical_category_name',
+           'join_hierarchical_category_path', 'clean_before_deployment',
+           'sort_posts', 'indent', 'load_data', 'html_unescape',)
 
 # Are you looking for 'generic_rss_renderer'?
 # It's defined in nikola.nikola.Nikola (the site object).
@@ -1947,6 +1950,40 @@ def clean_before_deployment(site):
     return undeployed_posts
 
 
+def sort_posts(posts, *keys):
+    """Sort posts by a given predicate. Helper function for templates.
+
+    If a key starts with '-', it is sorted in descending order.
+
+    Usage examples::
+
+        sort_posts(timeline, 'title', 'date')
+        sort_posts(timeline, 'author', '-section_name')
+    """
+    # We reverse the keys to get the usual ordering method: the first key
+    # provided is the most important sorting predicate (first by 'title', then
+    # by 'date' in the first example)
+    for key in reversed(keys):
+        if key.startswith('-'):
+            key = key[1:]
+            reverse = True
+        else:
+            reverse = False
+        try:
+            # An attribute (or method) of the Post object
+            a = getattr(posts[0], key)
+            if callable(a):
+                keyfunc = operator.methodcaller(key)
+            else:
+                keyfunc = operator.attrgetter(key)
+        except AttributeError:
+            # Post metadata
+            keyfunc = operator.methodcaller('meta', key)
+
+        posts = sorted(posts, reverse=reverse, key=keyfunc)
+    return posts
+
+
 # Stolen from textwrap in Python 3.4.3.
 def indent(text, prefix, predicate=None):
     """Add 'prefix' to the beginning of selected lines in 'text'.


Test patch: