Repository: ckan/ckan

Issue:
user_list action ignores order_by param
### CKAN Version if known (or site URL)
2.8

### Please describe the expected behaviour

```py
users = toolkit.get_action('user_list')(context, {
    'order_by': 'number_created_packages'
})
```
sorts users by `number_created_packages`

### Please describe the actual behaviour

```py
users = toolkit.get_action('user_list')(context, {
    'order_by': 'number_created_packages'
})
```
sorts users by name

### What steps can be taken to reproduce the issue? 

see above

---

Bug is here:
https://github.com/ckan/ckan/blob/64774670b722ed1bd3808901137fa80753d3c772/ckan/logic/action/get.py#L750:L757

Related discussion: https://github.com/okfn/ckanext-unhcr/pull/268#discussion_r395679730


Discussion:

Resolving patch:
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -704,7 +704,9 @@ def user_list(context, data_dict):
       string (optional) (you must be a sysadmin to use this filter)
     :type email: string
     :param order_by: which field to sort the list by (optional, default:
-      ``'name'``). Can be any user field.
+      ``'display_name'``). Users can be sorted by ``'id'``, ``'name'``,
+      ``'fullname'``, ``'display_name'``, ``'created'``, ``'about'``,
+      ``'sysadmin'`` or ``'number_created_packages'``.
     :type order_by: string
     :param all_fields: return full user dictionaries instead of just names.
       (optional, default: ``True``)
@@ -721,7 +723,7 @@ def user_list(context, data_dict):
 
     q = data_dict.get('q', '')
     email = data_dict.get('email')
-    order_by = data_dict.get('order_by', 'name')
+    order_by = data_dict.get('order_by', 'display_name')
     all_fields = asbool(data_dict.get('all_fields', True))
 
     if all_fields:
@@ -747,14 +749,31 @@ def user_list(context, data_dict):
     if email:
         query = query.filter_by(email=email)
 
+    order_by_field = None
     if order_by == 'edits':
         raise ValidationError('order_by=edits is no longer supported')
-    query = query.order_by(
-        _case([(
-            _or_(model.User.fullname == None,
-                    model.User.fullname == ''),
-            model.User.name)],
-            else_=model.User.fullname))
+    elif order_by == 'number_created_packages':
+        order_by_field = order_by
+    elif order_by != 'display_name':
+        try:
+            order_by_field = getattr(model.User, order_by)
+        except AttributeError:
+            pass
+    if order_by == 'display_name' or order_by_field is None:
+        query = query.order_by(
+            _case(
+                [(
+                    _or_(
+                        model.User.fullname == None,
+                        model.User.fullname == ''
+                    ),
+                    model.User.name
+                )],
+                else_=model.User.fullname
+            )
+        )
+    else:
+        query = query.order_by(order_by_field)
 
     # Filter deleted users
     query = query.filter(model.User.state != model.State.DELETED)


Test patch:
diff --git a/ckan/tests/logic/action/test_get.py b/ckan/tests/logic/action/test_get.py
--- a/ckan/tests/logic/action/test_get.py
+++ b/ckan/tests/logic/action/test_get.py
@@ -907,6 +907,87 @@ def test_user_list_filtered_by_email(self):
         got_user = got_users[0]
         assert got_user == user_a["name"]
 
+    def test_user_list_order_by_default(self):
+        default_user = helpers.call_action('get_site_user', ignore_auth=True)
+
+        users = [
+            factories.User(fullname="Xander Bird", name="bird_x"),
+            factories.User(fullname="Max Hankins", name="hankins_m"),
+            factories.User(fullname="", name="morgan_w"),
+            factories.User(fullname="Kathy Tillman", name="tillman_k"),
+        ]
+        expected_names = [
+            u['name'] for u in [
+                users[3],  # Kathy Tillman
+                users[1],  # Max Hankins
+                users[2],  # morgan_w
+                users[0],  # Xander Bird
+            ]
+        ]
+
+        got_users = helpers.call_action('user_list')
+        got_names = [
+            u['name'] for u in got_users if u['name'] != default_user['name']
+        ]
+
+        assert got_names == expected_names
+
+    def test_user_list_order_by_fullname_only(self):
+        default_user = helpers.call_action('get_site_user', ignore_auth=True)
+
+        users = [
+            factories.User(fullname="Xander Bird", name="bird_x"),
+            factories.User(fullname="Max Hankins", name="hankins_m"),
+            factories.User(fullname="", name="morgan_w"),
+            factories.User(fullname="Kathy Tillman", name="tillman_k"),
+        ]
+        expected_fullnames = sorted([u['fullname'] for u in users])
+
+        got_users = helpers.call_action('user_list', order_by='fullname')
+        got_fullnames = [
+            u['fullname'] for u in got_users if u['name'] != default_user['name']
+        ]
+
+        assert got_fullnames == expected_fullnames
+
+    def test_user_list_order_by_created_datasets(self):
+        default_user = helpers.call_action('get_site_user', ignore_auth=True)
+
+        users = [
+            factories.User(fullname="Xander Bird", name="bird_x"),
+            factories.User(fullname="Max Hankins", name="hankins_m"),
+            factories.User(fullname="Kathy Tillman", name="tillman_k"),
+        ]
+        datasets = [
+            factories.Dataset(user=users[1]),
+            factories.Dataset(user=users[1])
+        ]
+        for dataset in datasets:
+            dataset["title"] = "Edited title"
+            helpers.call_action(
+                'package_update', context={'user': users[1]['name']}, **dataset
+            )
+        expected_names = [
+            u['name'] for u in [
+                users[0],  # 0 packages created
+                users[2],  # 0 packages created
+                users[1],  # 2 packages created
+            ]
+        ]
+
+        got_users = helpers.call_action(
+            'user_list', order_by='number_created_packages'
+        )
+        got_names = [
+            u['name'] for u in got_users if u['name'] != default_user['name']
+        ]
+
+        assert got_names == expected_names
+
+    def test_user_list_order_by_edits(self):
+        with pytest.raises(logic.ValidationError):
+            helpers.call_action('user_list', order_by='edits')
+
 
 @pytest.mark.usefixtures("clean_db", "with_request_context")
 class TestUserShow(object):