prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Admin UI suggestions
  As I've been using the admin UI a bit for my class, I just had a few things that I thought would be nice to have/change in it:
  - move the "add user" button to the top -- it is annoying with lots of users to have to scroll all the way down to find it
  - add some simple statistics at the top of the page: number of running servers, number of currently active users, etc.
  - it would be awesome to be able to sort users by the different columns -- admin, alphabetically, by last seen
  - currently, if you shut down a user's server, it causes the page to refresh which often jumps you up to the top (or just somewhere else). It would be nice if the update could be done in the background without actually reloading the page.
  
  Obviously, none of these are urgent, but I think they would make the admin experience a little easier. I can open separate issues for them if so desired.
  
  
  </issue>
  <code>
  [start of jupyterhub/handlers/pages.py]
  1 """Basic html-rendering handlers."""
  2 
  3 # Copyright (c) Jupyter Development Team.
  4 # Distributed under the terms of the Modified BSD License.
  5 
  6 from tornado import web
  7 
  8 from .. import orm
  9 from ..utils import admin_only, url_path_join
  10 from .base import BaseHandler
  11 
  12 
  13 class RootHandler(BaseHandler):
  14     """Render the Hub root page.
  15     
  16     Currently redirects to home if logged in,
  17     shows big fat login button otherwise.
  18     """
  19     def get(self):
  20         if self.get_current_user():
  21             self.redirect(
  22                 url_path_join(self.hub.server.base_url, 'home'),
  23                 permanent=False,
  24             )
  25             return
  26         
  27         html = self.render_template('index.html',
  28             login_url=self.settings['login_url'],
  29         )
  30         self.finish(html)
  31 
  32 class HomeHandler(BaseHandler):
  33     """Render the user's home page."""
  34 
  35     @web.authenticated
  36     def get(self):
  37         html = self.render_template('home.html',
  38             user=self.get_current_user(),
  39         )
  40         self.finish(html)
  41 
  42 
  43 class AdminHandler(BaseHandler):
  44     """Render the admin page."""
  45 
  46     @admin_only
  47     def get(self):
  48         html = self.render_template('admin.html',
  49             user=self.get_current_user(),
  50             users=self.db.query(orm.User),
  51             admin_access=self.settings.get('admin_access', False),
  52         )
  53         self.finish(html)
  54 
  55 
  56 default_handlers = [
  57     (r'/', RootHandler),
  58     (r'/home', HomeHandler),
  59     (r'/admin', AdminHandler),
  60 ]
  61
  [end of jupyterhub/handlers/pages.py]
  [start of jupyterhub/apihandlers/base.py]
  1 """Base API handlers"""
  2 # Copyright (c) Jupyter Development Team.
  3 # Distributed under the terms of the Modified BSD License.
  4 
  5 import json
  6 
  7 from http.client import responses
  8 
  9 from tornado import web
  10 
  11 from ..handlers import BaseHandler
  12 
  13 class APIHandler(BaseHandler):
  14     def get_json_body(self):
  15         """Return the body of the request as JSON data."""
  16         if not self.request.body:
  17             return None
  18         body = self.request.body.strip().decode('utf-8')
  19         try:
  20             model = json.loads(body)
  21         except Exception:
  22             self.log.debug("Bad JSON: %r", body)
  23             self.log.error("Couldn't parse JSON", exc_info=True)
  24             raise web.HTTPError(400, 'Invalid JSON in body of request')
  25         return model
  26         
  27     
  28     def write_error(self, status_code, **kwargs):
  29         """Write JSON errors instead of HTML"""
  30         exc_info = kwargs.get('exc_info')
  31         message = ''
  32         status_message = responses.get(status_code, 'Unknown Error')
  33         if exc_info:
  34             exception = exc_info[1]
  35             # get the custom message, if defined
  36             try:
  37                 message = exception.log_message % exception.args
  38             except Exception:
  39                 pass
  40 
  41             # construct the custom reason, if defined
  42             reason = getattr(exception, 'reason', '')
  43             if reason:
  44                 status_message = reason
  45         
  46         self.write(json.dumps({
  47             'status': status_code,
  48             'message': message or status_message,
  49         }))
  50
  [end of jupyterhub/apihandlers/base.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py
  --- a/jupyterhub/apihandlers/base.py
  +++ b/jupyterhub/apihandlers/base.py
  @@ -42,7 +42,7 @@
               reason = getattr(exception, 'reason', '')
               if reason:
                   status_message = reason
  -        
  +        self.set_header('Content-Type', 'application/json')
           self.write(json.dumps({
               'status': status_code,
               'message': message or status_message,
  diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py
  --- a/jupyterhub/handlers/pages.py
  +++ b/jupyterhub/handlers/pages.py
  @@ -45,10 +45,52 @@
   
       @admin_only
       def get(self):
  +        available = {'name', 'admin', 'running', 'last_activity'}
  +        default_sort = ['admin', 'name']
  +        mapping = {
  +            'running': '_server_id'
  +        }
  +        default_order = {
  +            'name': 'asc',
  +            'last_activity': 'desc',
  +            'admin': 'desc',
  +            'running': 'desc',
  +        }
  +        sorts = self.get_arguments('sort') or default_sort
  +        orders = self.get_arguments('order')
  +        
  +        for bad in set(sorts).difference(available):
  +            self.log.warn("ignoring invalid sort: %r", bad)
  +            sorts.remove(bad)
  +        for bad in set(orders).difference({'asc', 'desc'}):
  +            self.log.warn("ignoring invalid order: %r", bad)
  +            orders.remove(bad)
  +        
  +        # add default sort as secondary
  +        for s in default_sort:
  +            if s not in sorts:
  +                sorts.append(s)
  +        if len(orders) < len(sorts):
  +            for col in sorts[len(orders):]:
  +                orders.append(default_order[col])
  +        else:
  +            orders = orders[:len(sorts)]
  +        
  +        # this could be one incomprehensible nested list comprehension
  +        # get User columns
  +        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]
  +        # get User.col.desc() order objects
  +        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]
  +        
  +        users = self.db.query(orm.User).order_by(*ordered)
  +        running = users.filter(orm.User.server != None)
  +        
           html = self.render_template('admin.html',
               user=self.get_current_user(),
  -            users=self.db.query(orm.User),
               admin_access=self.settings.get('admin_access', False),
  +            users=users,
  +            running=running,
  +            sort={s:o for s,o in zip(sorts, orders)},
           )
           self.finish(html)
  
verification_info: {"golden_diff": "diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py\n--- a/jupyterhub/apihandlers/base.py\n+++ b/jupyterhub/apihandlers/base.py\n@@ -42,7 +42,7 @@\n             reason = getattr(exception, 'reason', '')\n             if reason:\n                 status_message = reason\n-        \n+        self.set_header('Content-Type', 'application/json')\n         self.write(json.dumps({\n             'status': status_code,\n             'message': message or status_message,\ndiff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py\n--- a/jupyterhub/handlers/pages.py\n+++ b/jupyterhub/handlers/pages.py\n@@ -45,10 +45,52 @@\n \n     @admin_only\n     def get(self):\n+        available = {'name', 'admin', 'running', 'last_activity'}\n+        default_sort = ['admin', 'name']\n+        mapping = {\n+            'running': '_server_id'\n+        }\n+        default_order = {\n+            'name': 'asc',\n+            'last_activity': 'desc',\n+            'admin': 'desc',\n+            'running': 'desc',\n+        }\n+        sorts = self.get_arguments('sort') or default_sort\n+        orders = self.get_arguments('order')\n+        \n+        for bad in set(sorts).difference(available):\n+            self.log.warn(\"ignoring invalid sort: %r\", bad)\n+            sorts.remove(bad)\n+        for bad in set(orders).difference({'asc', 'desc'}):\n+            self.log.warn(\"ignoring invalid order: %r\", bad)\n+            orders.remove(bad)\n+        \n+        # add default sort as secondary\n+        for s in default_sort:\n+            if s not in sorts:\n+                sorts.append(s)\n+        if len(orders) < len(sorts):\n+            for col in sorts[len(orders):]:\n+                orders.append(default_order[col])\n+        else:\n+            orders = orders[:len(sorts)]\n+        \n+        # this could be one incomprehensible nested list comprehension\n+        # get User columns\n+        cols = [ getattr(orm.User, mapping.get(c, c)) for c in sorts ]\n+        # get User.col.desc() order objects\n+        ordered = [ getattr(c, o)() for c, o in zip(cols, orders) ]\n+        \n+        users = self.db.query(orm.User).order_by(*ordered)\n+        running = users.filter(orm.User.server != None)\n+        \n         html = self.render_template('admin.html',\n             user=self.get_current_user(),\n-            users=self.db.query(orm.User),\n             admin_access=self.settings.get('admin_access', False),\n+            users=users,\n+            running=running,\n+            sort={s:o for s,o in zip(sorts, orders)},\n         )\n         self.finish(html)\n", "issue": "Admin UI suggestions\nAs I've been using the admin UI a bit for my class, I just had a few things that I thought would be nice to have/change in it:\n- move the \"add user\" button to the top -- it is annoying with lots of users to have to scroll all the way down to find it\n- add some simple statistics at the top of the page: number of running servers, number of currently active users, etc.\n- it would be awesome to be able to sort users by the different columns -- admin, alphabetically, by last seen\n- currently, if you shut down a user's server, it causes the page to refresh which often jumps you up to the top (or just somewhere else). It would be nice if the update could be done in the background without actually reloading the page.\n\nObviously, none of these are urgent, but I think they would make the admin experience a little easier. I can open separate issues for them if so desired.\n\n", "before_files": [{"content": "\"\"\"Basic html-rendering handlers.\"\"\"\n\n# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nfrom tornado import web\n\nfrom .. import orm\nfrom ..utils import admin_only, url_path_join\nfrom .base import BaseHandler\n\n\nclass RootHandler(BaseHandler):\n    \"\"\"Render the Hub root page.\n    \n    Currently redirects to home if logged in,\n    shows big fat login button otherwise.\n    \"\"\"\n    def get(self):\n        if self.get_current_user():\n            self.redirect(\n                url_path_join(self.hub.server.base_url, 'home'),\n                permanent=False,\n            )\n            return\n        \n        html = self.render_template('index.html',\n            login_url=self.settings['login_url'],\n        )\n        self.finish(html)\n\nclass HomeHandler(BaseHandler):\n    \"\"\"Render the user's home page.\"\"\"\n\n    @web.authenticated\n    def get(self):\n        html = self.render_template('home.html',\n            user=self.get_current_user(),\n        )\n        self.finish(html)\n\n\nclass AdminHandler(BaseHandler):\n    \"\"\"Render the admin page.\"\"\"\n\n    @admin_only\n    def get(self):\n        html = self.render_template('admin.html',\n            user=self.get_current_user(),\n            users=self.db.query(orm.User),\n            admin_access=self.settings.get('admin_access', False),\n        )\n        self.finish(html)\n\n\ndefault_handlers = [\n    (r'/', RootHandler),\n    (r'/home', HomeHandler),\n    (r'/admin', AdminHandler),\n]\n", "path": "jupyterhub/handlers/pages.py"}, {"content": "\"\"\"Base API handlers\"\"\"\n# Copyright (c) Jupyter Development Team.\n# Distributed under the terms of the Modified BSD License.\n\nimport json\n\nfrom http.client import responses\n\nfrom tornado import web\n\nfrom ..handlers import BaseHandler\n\nclass APIHandler(BaseHandler):\n    def get_json_body(self):\n        \"\"\"Return the body of the request as JSON data.\"\"\"\n        if not self.request.body:\n            return None\n        body = self.request.body.strip().decode('utf-8')\n        try:\n            model = json.loads(body)\n        except Exception:\n            self.log.debug(\"Bad JSON: %r\", body)\n            self.log.error(\"Couldn't parse JSON\", exc_info=True)\n            raise web.HTTPError(400, 'Invalid JSON in body of request')\n        return model\n        \n    \n    def write_error(self, status_code, **kwargs):\n        \"\"\"Write JSON errors instead of HTML\"\"\"\n        exc_info = kwargs.get('exc_info')\n        message = ''\n        status_message = responses.get(status_code, 'Unknown Error')\n        if exc_info:\n            exception = exc_info[1]\n            # get the custom message, if defined\n            try:\n                message = exception.log_message % exception.args\n            except Exception:\n                pass\n\n            # construct the custom reason, if defined\n            reason = getattr(exception, 'reason', '')\n            if reason:\n                status_message = reason\n        \n        self.write(json.dumps({\n            'status': status_code,\n            'message': message or status_message,\n        }))\n", "path": "jupyterhub/apihandlers/base.py"}]}