Repository: mozilla/nunjucks (js)

Issue:
Add dot notation support to `sort` filter
#1276 adds helper to support for dot-notation in `groupby` filter. Same helper could be used for `sort` filter.

https://jinja.palletsprojects.com/en/2.11.x/templates/#sort

Pull request description:
Use attribute getter for `sort` filter
## Summary

Proposed change:

Add support for dot separated attributes in `sort` filter.

Closes #1298.


## Checklist

I've completed the checklist below to ensure I didn't forget anything. This makes reviewing this PR as easy as possible for the maintainers. And it gets this change released as soon as possible.

* [x] Proposed change helps towards [*purpose of this project*](https://github.com/mozilla/nunjucks/blob/master/CONTRIBUTING.md#purpose).
* [x] [*Documentation*](https://github.com/mozilla/nunjucks/tree/master/docs/) is added / updated to describe proposed change.
* [x] [*Tests*](https://github.com/mozilla/nunjucks/tree/master/tests) are added / updated to cover proposed change.
* [x] [*Changelog*](https://github.com/mozilla/nunjucks/blob/master/CHANGELOG.md) has an entry for proposed change (if user-facing fix or feature).

<!-- Tick of items by replacing `[ ]` by `[x]` -->

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adeb436..f7ce2d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
 Changelog
 =========
 
+Unreleased
+----------
+
+* Add support for nested attributes on
+  [`sort` filter](https://mozilla.github.io/nunjucks/templating.html#sort-arr-reverse-casesens-attr);
+  respect `throwOnUndefined` if sort attribute is undefined.
+
 3.2.2 (Jul 20 2020)
 -------------------
 
diff --git a/nunjucks/src/filters.js b/nunjucks/src/filters.js
index 1a99c1e..40dce7b 100644
--- a/nunjucks/src/filters.js
+++ b/nunjucks/src/filters.js
@@ -447,13 +447,21 @@ exports.sum = sum;
 
 exports.sort = r.makeMacro(
   ['value', 'reverse', 'case_sensitive', 'attribute'], [],
-  (arr, reversed, caseSens, attr) => {
+  function sortFilter(arr, reversed, caseSens, attr) {
     // Copy it
     let array = lib.map(arr, v => v);
+    let getAttribute = lib.getAttrGetter(attr);
 
     array.sort((a, b) => {
-      let x = (attr) ? a[attr] : a;
-      let y = (attr) ? b[attr] : b;
+      let x = (attr) ? getAttribute(a) : a;
+      let y = (attr) ? getAttribute(b) : b;
+
+      if (
+        this.env.opts.throwOnUndefined &&
+        attr && (x === undefined || y === undefined)
+      ) {
+        throw new TypeError(`sort: attribute "${attr}" resolved to undefined`);
+      }
 
       if (!caseSens && lib.isString(x) && lib.isString(y)) {
         x = x.toLowerCase();
diff --git a/nunjucks/src/lib.js b/nunjucks/src/lib.js
index 6e66621..376db95 100644
--- a/nunjucks/src/lib.js
+++ b/nunjucks/src/lib.js
@@ -210,6 +210,8 @@ function getAttrGetter(attribute) {
   };
 }
 
+exports.getAttrGetter = getAttrGetter;
+
 function groupBy(obj, val, throwOnUndefined) {
   const result = {};
   const iterator = isFunction(val) ? val : getAttrGetter(val);
diff --git a/nunjucks/src/runtime.js b/nunjucks/src/runtime.js
index 7b0a434..d57c741 100644
--- a/nunjucks/src/runtime.js
+++ b/nunjucks/src/runtime.js
@@ -82,7 +82,7 @@ class Frame {
 }
 
 function makeMacro(argNames, kwargNames, func) {
-  return (...macroArgs) => {
+  return function macro(...macroArgs) {
     var argCount = numArgs(macroArgs);
     var args;
     var kwargs = getKeywordArgs(macroArgs);


Test patch:
diff --git a/tests/filters.js b/tests/filters.js
index 2c56bc7..5ed7861 100644
--- a/tests/filters.js
+++ b/tests/filters.js
@@ -825,6 +825,35 @@
       equal('{% for i in [ {n:3},{n:5},{n:2},{n:1},{n:4},{n:6}] | sort(attribute="n") %}{{ i.n }}{% endfor %}',
         '123456');
 
+      const nestedAttributeSortTemplate = '{% for item in items | sort(attribute="meta.age") %}{{ item.name }}{% endfor %}';
+      equal(
+        nestedAttributeSortTemplate,
+        {
+          items: [
+            {name: 'james', meta: {age: 25}},
+            {name: 'fred', meta: {age: 18}},
+            {name: 'john', meta: {age: 19}}
+          ]
+        },
+        'fredjohnjames'
+      );
+
+      expect(function() {
+        render(
+          nestedAttributeSortTemplate,
+          {
+            items: [
+              {name: 'james', meta: {age: 25}},
+              {name: 'fred', meta: {age: 18}},
+              {name: 'john', meta: {title: 'Developer'}}
+            ]
+          },
+          {
+            throwOnUndefined: true
+          }
+        );
+      }).to.throwError(/sort: attribute "meta\.age" resolved to undefined/);
+
       finish(done);
     });