From 8ee9869ca044ab22d93e40947c27cee65eec736b Mon Sep 17 00:00:00 2001 From: Rollin Thomas Date: Wed, 1 Aug 2018 13:29:42 -0700 Subject: [PATCH] Add an example simple announcement service --- examples/service-announcement/README.md | 60 +++++++++++++++ examples/service-announcement/announcement.py | 76 +++++++++++++++++++ .../service-announcement/jupyterhub_config.py | 10 +++ .../service-announcement/templates/page.html | 14 ++++ 4 files changed, 160 insertions(+) create mode 100644 examples/service-announcement/README.md create mode 100644 examples/service-announcement/announcement.py create mode 100644 examples/service-announcement/jupyterhub_config.py create mode 100644 examples/service-announcement/templates/page.html diff --git a/examples/service-announcement/README.md b/examples/service-announcement/README.md new file mode 100644 index 00000000..f61f3505 --- /dev/null +++ b/examples/service-announcement/README.md @@ -0,0 +1,60 @@ + +# Simple Announcement Service Example + +This is a simple service that allows administrators to manage announcements +that appear when JupyterHub renders pages. + +To run the service as a hub-managed service simply include in your JupyterHub +configuration file something like: + + c.JupyterHub.services = [ + { + 'name': 'announcement', + 'url': 'http://127.0.0.1:8888', + 'command': ["python", "-m", "announcement"], + } + ] + +This starts the announcements service up at `/services/announcement` when +JupyterHub launches. By default the announcement text is empty. + +The `announcement` module has a configurable port (default 8888) and an API +prefix setting. By default the API prefix is `JUPYTERHUB_SERVICE_PREFIX` if +that environment variable is set or `/` if it is not. + +## Managing the Announcement + +Admin users can set the announcement text with an API token: + + $ curl -X POST -H "Authorization: token " \ + -d "{'announcement':'JupyterHub will be upgraded on August 14!'}" \ + https://.../services/announcement + +Anyone can read the announcement: + + $ curl https://.../services/announcement | python -m json.tool + { + announcement: "JupyterHub will be upgraded on August 14!", + timestamp: "...", + user: "..." + } + +The time the announcement was posted is recorded in the `timestamp` field and +the user who posted the announcement is recorded in the `user` field. + +To clear the announcement text, just DELETE. Only admin users can do this. + + $ curl -X POST -H "Authorization: token " \ + https://.../services/announcement + +## Seeing the Announcement in JupyterHub + +To be able to render the announcement, include the provide `page.html` template +that extends the base `page.html` template. Set `c.JupyterHub.template_paths` +in JupyterHub's configuration to include the path to the extending template. +The template changes the `announcement` element and does a JQuery `$.get()` call +to retrieve the announcement text. + +JupyterHub's configurable announcement template variables can be set for various +pages like login, logout, spawn, and home. Including the template provided in +this example overrides all of those. diff --git a/examples/service-announcement/announcement.py b/examples/service-announcement/announcement.py new file mode 100644 index 00000000..1dd452c2 --- /dev/null +++ b/examples/service-announcement/announcement.py @@ -0,0 +1,76 @@ + +import argparse +import datetime +import json +import os + +from jupyterhub.services.auth import HubAuthenticated +from tornado import escape, gen, ioloop, web + + +class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler): + """Dynamically manage page announcements""" + + def initialize(self, storage): + """Create storage for announcement text""" + self.storage = storage + + @web.authenticated + def post(self): + """Update announcement""" + user = self.get_current_user() + if user is None or not user.get("admin", False): + raise web.HTTPError(403) + doc = escape.json_decode(self.request.body) + self.storage["announcement"] = doc["announcement"] + self.storage["timestamp"] = datetime.datetime.now().isoformat() + self.storage["user"] = user["name"] + self.write_to_json(self.storage) + + def get(self): + """Retrieve announcement""" + self.write_to_json(self.storage) + + @web.authenticated + def delete(self): + """Clear announcement""" + user = self.get_current_user() + if user is None or not user.get("admin", False): + raise web.HTTPError(403) + self.storage["announcement"] = "" + self.write_to_json(self.storage) + + def write_to_json(self, doc): + """Write dictionary document as JSON""" + self.set_header("Content-Type", "application/json; charset=UTF-8") + self.write(escape.utf8(json.dumps(doc))) + + +def main(): + args = parse_arguments() + application = create_application(**vars(args)) + application.listen(args.port) + ioloop.IOLoop.current().start() + + +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("--api-prefix", "-a", + default=os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/"), + help="application API prefix") + parser.add_argument("--port", "-p", + default=8888, + help="port for API to listen on", + type=int) + return parser.parse_args() + + +def create_application(api_prefix="/", + handler=AnnouncementRequestHandler, + **kwargs): + storage = dict(announcement="", timestamp="", user="") + return web.Application([(api_prefix, handler, dict(storage=storage))]) + + +if __name__ == "__main__": + main() diff --git a/examples/service-announcement/jupyterhub_config.py b/examples/service-announcement/jupyterhub_config.py new file mode 100644 index 00000000..f4350c0f --- /dev/null +++ b/examples/service-announcement/jupyterhub_config.py @@ -0,0 +1,10 @@ + +c.JupyterHub.services = [ + { + 'name': 'announcement', + 'url': 'http://127.0.0.1:8888', + 'command': ["python", "-m", "announcement"], + } +] + +c.JupyterHub.template_paths = ["templates"] diff --git a/examples/service-announcement/templates/page.html b/examples/service-announcement/templates/page.html new file mode 100644 index 00000000..5ba023dd --- /dev/null +++ b/examples/service-announcement/templates/page.html @@ -0,0 +1,14 @@ +{% extends "templates/page.html" %} +{% block announcement %} +
+
+{% endblock %} + +{% block script %} +{{ super() }} + +{% endblock %}