diff --git a/openlobby/core/api/schema.py b/openlobby/core/api/schema.py
index 4bd463243cc9c44ea140f4a45419967e6475e65e..9060e6ca465f24f7a8757dd0247cac4707374a2f 100644
--- a/openlobby/core/api/schema.py
+++ b/openlobby/core/api/schema.py
@@ -1,14 +1,23 @@
 import graphene
-from graphene import relay
 from django.db.models import Count, Q
+from graphene import relay
 
 from . import types
-from ..models import OpenIdClient
 from .paginator import Paginator
 from .sanitizers import extract_text
 from .. import search
+from ..models import OpenIdClient
 from ..models import User, Report
 
+AUTHOR_SORT_LAST_NAME_ID = 1
+AUTHOR_SORT_TOTAL_REPORTS_ID = 2
+
+class AuthorSortEnum(graphene.Enum):
+    LAST_NAME = AUTHOR_SORT_LAST_NAME_ID
+    TOTAL_REPORTS = AUTHOR_SORT_TOTAL_REPORTS_ID
+
+    class Meta:
+        description = 'Sort by field.'
 
 class AuthorsConnection(relay.Connection):
     total_count = graphene.Int()
@@ -38,6 +47,8 @@ class Query:
     authors = relay.ConnectionField(
         AuthorsConnection,
         description='List of Authors. Returns first 10 nodes if pagination is not specified.',
+        sort=AuthorSortEnum(),
+        reversed=graphene.Boolean(default_value=False, description="Reverse order of sort")
     )
     search_reports = relay.ConnectionField(
         SearchReportsConnection,
@@ -59,10 +70,10 @@ class Query:
         paginator = Paginator(**kwargs)
 
         total = User.objects.filter(is_author=True).count()
-        authors = User.objects.filter(is_author=True)\
-            .annotate(total_reports=Count('report', filter=Q(report__is_draft=False)))\
-            .order_by('last_name', 'first_name')[
-            paginator.slice_from:paginator.slice_to]
+
+        authors = User.objects\
+                      .sorted(**kwargs)\
+                      .filter(is_author=True)[paginator.slice_from:paginator.slice_to]
 
         page_info = paginator.get_page_info(total)
 
diff --git a/openlobby/core/models.py b/openlobby/core/models.py
index 470de4c2f75960aba1845fb0006b208c7e3d9210..8c46ec0e6455123c6318a998e037bd34e0888686 100644
--- a/openlobby/core/models.py
+++ b/openlobby/core/models.py
@@ -1,9 +1,26 @@
+import time
+
 from django.conf import settings
-from django.db import models
+from django.contrib.auth.base_user import BaseUserManager
 from django.contrib.auth.models import AbstractUser
 from django.contrib.postgres.fields import JSONField
+from django.db import models
+from django.db.models import Count
+from django.db.models import Q
 from django.utils import timezone
-import time
+
+
+class UserManager(BaseUserManager):
+    def sorted(self, **kwargs):
+        # inline import intentionally
+        from openlobby.core.api.schema import AUTHOR_SORT_LAST_NAME_ID, AUTHOR_SORT_TOTAL_REPORTS_ID
+        qs = self.get_queryset().annotate(total_reports=Count('report', filter=Q(report__is_draft=False)))
+        sort_field = kwargs.get('sort', AUTHOR_SORT_LAST_NAME_ID)
+        if sort_field == AUTHOR_SORT_LAST_NAME_ID:
+            return qs.order_by('{}last_name'.format('-' if kwargs.get('reversed', False) else ''), 'first_name')
+        elif sort_field == AUTHOR_SORT_TOTAL_REPORTS_ID:
+            return qs.order_by('{}total_reports'.format('' if kwargs.get('reversed', False) else '-'), 'last_name')
+        raise NotImplemented("Other sort types are not implemented")
 
 
 class User(AbstractUser):
@@ -14,12 +31,13 @@ class User(AbstractUser):
     extra = JSONField(null=True, blank=True)
     is_author = models.BooleanField(default=False)
     has_colliding_name = models.BooleanField(default=False)
+    objects = UserManager()
 
     def save(self, *args, **kwargs):
         # deal with first name and last name collisions
         if self.is_author:
             collisions = User.objects.filter(first_name=self.first_name, last_name=self.last_name,
-                is_author=True).exclude(id=self.id)
+                                             is_author=True).exclude(id=self.id)
             if collisions.count() > 0:
                 self.has_colliding_name = True
                 collisions.update(has_colliding_name=True)
diff --git a/tests/schema/snapshots/snap_test_authors.py b/tests/schema/snapshots/snap_test_authors.py
index af1983302e23288e0d90904421bb8aa855bb14f8..cf9fba0e09487210dbca4670b052d2d65f9c3fd3 100644
--- a/tests/schema/snapshots/snap_test_authors.py
+++ b/tests/schema/snapshots/snap_test_authors.py
@@ -266,3 +266,248 @@ snapshots['test_with_reports 1'] = {
         }
     }
 }
+
+snapshots['test_sort_by_last_name 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_last_name_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_total_reports 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_total_reports_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
+
+snapshots['test_sort_by_default_reversed 1'] = {
+    'data': {
+        'authors': {
+            'edges': [
+                {
+                    'cursor': 'MQ==',
+                    'node': {
+                        'extra': '{"movies": 1}',
+                        'firstName': 'Winston',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjE=',
+                        'lastName': 'Wolfe',
+                        'totalReports': 2
+                    }
+                },
+                {
+                    'cursor': 'Mg==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Spongebob',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjI=',
+                        'lastName': 'Squarepants',
+                        'totalReports': 1
+                    }
+                },
+                {
+                    'cursor': 'Mw==',
+                    'node': {
+                        'extra': None,
+                        'firstName': 'Shaun',
+                        'hasCollidingName': False,
+                        'id': 'QXV0aG9yOjM=',
+                        'lastName': 'Sheep',
+                        'totalReports': 0
+                    }
+                }
+            ],
+            'pageInfo': {
+                'endCursor': 'Mw==',
+                'hasNextPage': False,
+                'hasPreviousPage': False,
+                'startCursor': 'MQ=='
+            },
+            'totalCount': 3
+        }
+    }
+}
diff --git a/tests/schema/test_authors.py b/tests/schema/test_authors.py
index 0e83176eb619f6d23935f667e1eb9bd0e4a50734..5c59a37386187dff0949ef7bb8dedee57d827897 100644
--- a/tests/schema/test_authors.py
+++ b/tests/schema/test_authors.py
@@ -199,3 +199,148 @@ def test_with_reports(client, snapshot):
     """
     response = call_api(client, query)
     snapshot.assert_match(response)
+
+def test_sort_by_last_name(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: LAST_NAME) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_last_name_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: LAST_NAME, reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_total_reports(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: TOTAL_REPORTS, reversed: false) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_total_reports_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(sort: TOTAL_REPORTS, reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
+
+def test_sort_by_default_reversed(client, snapshot):
+    prepare_reports()
+    query = """
+    query {
+        authors(reversed: true) {
+            totalCount
+            edges {
+                cursor
+                node {
+                    id
+                    firstName
+                    lastName
+                    hasCollidingName
+                    totalReports
+                    extra
+                }
+            }
+            pageInfo {
+                hasPreviousPage
+                hasNextPage
+                startCursor
+                endCursor
+            }
+        }
+    }
+    """
+    response = call_api(client, query)
+    snapshot.assert_match(response)
\ No newline at end of file
diff --git a/tests/test_models.py b/tests/test_models.py
index 2cc7a8c4c64d063d0824ad4bbb8df4b73ed9081d..80e382a634e2b929e4c7a7ec5912bd2d1cee682d 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,11 +1,11 @@
-import pytest
-import arrow
-from django.conf import settings
 from unittest.mock import patch
 
-from openlobby.core.models import Report, User, OpenIdClient, LoginAttempt
+import arrow
+import pytest
+from django.conf import settings
+from openlobby.core.api.schema import AUTHOR_SORT_LAST_NAME_ID, AUTHOR_SORT_TOTAL_REPORTS_ID
 from openlobby.core.documents import ReportDoc
-
+from openlobby.core.models import Report, User, OpenIdClient, LoginAttempt
 
 pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures('django_es')]
 
@@ -82,7 +82,7 @@ def test_login_attempt__default_expiration():
     client = OpenIdClient.objects.create(name='a', client_id='b', client_secret='c')
     with patch('openlobby.core.models.time.time', return_value=10000):
         attempt = LoginAttempt.objects.create(openid_client=client, state='foo',
-            app_redirect_uri='http://openlobby/app')
+                                              app_redirect_uri='http://openlobby/app')
     assert attempt.expiration == 10000 + settings.LOGIN_ATTEMPT_EXPIRATION
 
 
@@ -118,3 +118,42 @@ def test_user__name_collision_excludes_self_on_update():
     u = User.objects.create(username='a', is_author=True, first_name='Ryan', last_name='Gosling')
     u.save()
     assert User.objects.get(username='a').has_colliding_name is False
+
+
+def test_user__sorted_default():
+    User.objects.create(username='a', is_author=False, first_name='Ryan', last_name='AGosling')
+    User.objects.create(username='b', is_author=True, first_name='Ryan', last_name='BGosling')
+    User.objects.create(username='c', is_author=False, first_name='Ryan', last_name='CGosling')
+    assert User.objects.sorted()[0].username == 'a'
+
+
+def test_user__sorted_default_reversed():
+    User.objects.create(username='a', is_author=False, first_name='Ryan', last_name='AGosling')
+    User.objects.create(username='b', is_author=True, first_name='Ryan', last_name='BGosling')
+    User.objects.create(username='c', is_author=False, first_name='Ryan', last_name='CGosling')
+    assert User.objects.sorted(reversed=True)[0].username == 'c'
+
+
+def test_user__sorted_last_name():
+    User.objects.create(username='a', is_author=False, first_name='Ryan', last_name='AGosling')
+    User.objects.create(username='b', is_author=True, first_name='Ryan', last_name='BGosling')
+    User.objects.create(username='c', is_author=False, first_name='Ryan', last_name='CGosling')
+    assert User.objects.sorted(sort=AUTHOR_SORT_LAST_NAME_ID)[0].username == 'a'
+    assert User.objects.sorted(sort=AUTHOR_SORT_LAST_NAME_ID, reversed=False)[0].username == 'a'
+    assert User.objects.sorted(sort=AUTHOR_SORT_LAST_NAME_ID, reversed=True)[0].username == 'c'
+
+
+def test_user__sorted_total_reports():
+    author = User.objects.create(username='a', is_author=True, first_name='Ryan', last_name='AGosling')
+    User.objects.create(username='b', is_author=True, first_name='Ryan', last_name='BGosling')
+    date = arrow.get(2018, 1, 1).datetime
+    Report.objects.create(
+        id=7,
+        author=author,
+        date=date,
+        published=date,
+        body='Lorem ipsum.',
+    )
+    assert User.objects.sorted(sort=AUTHOR_SORT_TOTAL_REPORTS_ID)[0].username == 'a'
+    assert User.objects.sorted(sort=AUTHOR_SORT_TOTAL_REPORTS_ID, reversed=False)[0].username == 'a'
+    assert User.objects.sorted(sort=AUTHOR_SORT_TOTAL_REPORTS_ID, reversed=True)[0].username == 'b'