{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As the core user guides including the [Introduction](../getting_started/Introduction.ipynb) have demonstrated, it is easy to display Panel apps in the notebook, launch them from an interactive Python prompt, and deploy them as a standalone Bokeh server app from the commandline. However, it is also often useful to embed a Panel app in large web application, such as a Django web server. Using Panel with Django requires a bit more work than for notebooks and Bokeh servers.\n",
    "\n",
    "To run this example app yourself, you will first need to install django 2 (e.g. `conda install \"django=2\"`).\n",
    "\n",
    "Additionally you should also install the `channels` library (using `pip install channels`). This makes it possible to run bokeh without launching a separate Tornado server."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Configuration\n",
    "\n",
    "Before we start adding a bokeh app to our Django server we have to set up some of the basic plumbing. In the `examples/apps/django2/project` folder we will add some basic configurations.\n",
    "\n",
    "First of all we need to set up a Asynchronous Server Gateway Interface (ASGI) instead of the usual WSGI setup. For this purpose we add `examples/apps/django2/project/asgi.py`:\n",
    "\n",
    "```python\n",
    "import os\n",
    "\n",
    "import django\n",
    "\n",
    "from channels.routing import get_default_application\n",
    "\n",
    "os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')\n",
    "\n",
    "django.setup()\n",
    "\n",
    "application = get_default_application()\n",
    "```\n",
    "\n",
    "\n",
    "Next we need to ensure the routing is configured correctly to handle a bokeh server in `examples/apps/django2/project/routing.py`:\n",
    "\n",
    "\n",
    "```python\n",
    "from channels.auth import AuthMiddlewareStack\n",
    "from channels.routing import ProtocolTypeRouter, URLRouter\n",
    "from django.apps import apps\n",
    "\n",
    "bokeh_app_config = apps.get_app_config('bokeh.server.django')\n",
    "\n",
    "application = ProtocolTypeRouter({\n",
    "    'websocket': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_websocket_urlpatterns())),\n",
    "    'http': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_http_urlpatterns())),\n",
    "})\n",
    "```\n",
    "\n",
    "Lastly we need to add some configuration to `examples/apps/django2/project/settings.py`. As a first step we need to add both `channels` and `bokeh.server.django` to the ``INSTALLED_APPS``:\n",
    "\n",
    "```python\n",
    "\n",
    "INSTALLED_APPS = [\n",
    "    ...\n",
    "    'channels',\n",
    "    'bokeh.server.django',\n",
    "]\n",
    "```\n",
    "\n",
    "Secondly we need to declare the `bokehjsdir` as part of the `STATICFILES_DIRS`:\n",
    "\n",
    "```python\n",
    "from bokeh.settings import bokehjsdir\n",
    "\n",
    "STATICFILES_DIRS = [bokehjsdir()]\n",
    "```\n",
    "\n",
    "Now we need to add any templates we have:\n",
    "\n",
    "```python\n",
    "TEMPLATES = [\n",
    "    {\n",
    "        'DIRS': [os.path.join(BASE_DIR, 'sliders', 'templates')],\n",
    "        ...\n",
    "    }\n",
    "]\n",
    "```\n",
    "\n",
    "and lastly add the app(s) and `static_extensions()` to the `urlpatterns` in the `urls.py` file:\n",
    "\n",
    "```\n",
    "from bokeh.server.django import autoload, static_extensions\n",
    "from django.apps import apps\n",
    "from django.contrib import admin\n",
    "from django.urls import path, include\n",
    "from django.contrib.staticfiles.urls import staticfiles_urlpatterns\n",
    "\n",
    "import sliders.pn_app as sliders_app\n",
    "\n",
    "pn_app_config = apps.get_app_config('bokeh.server.django')\n",
    "\n",
    "urlpatterns = [\n",
    "    path('sliders/', include('sliders.urls')),\n",
    "    path('admin/', admin.site.urls),\n",
    "]\n",
    "\n",
    "bokeh_apps = [\n",
    "    autoload(\"sliders\", sliders_app.app),\n",
    "]\n",
    "\n",
    "urlpatterns += static_extensions()\n",
    "urlpatterns += staticfiles_urlpatterns()\n",
    "```\n",
    "\n",
    "Now it's time to configure an actual app and add it to our Django server.\n",
    "\n",
    "## Sliders app\n",
    "\n",
    "Based on a standard Django2 app template, this app shows how to integrate Panel with a Django view\n",
    "\n",
    "The sliders app is in `examples/apps/django2/sliders`. We will cover the following additions/modifications to the Django2 app template:\n",
    "\n",
    "  * `sliders/sinewave.py`: a parameterized object (representing your pre-existing code)\n",
    "\n",
    "  * `sliders/pn_app.py`: creates an app function from the SineWave class\n",
    "\n",
    "  * `sliders/apps.py`: how a Django app can import and use Bokeh server\n",
    "\n",
    "  * `sliders/views.py` and `templates/base.html`: getting the Bokeh app into a Django view"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![screenshot of sliders app](../_static/sliders.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To start with, in `sliders/sinewave.py` we create a parameterized object to serve as a placeholder for your own, existing code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import param\n",
    "from bokeh.models import ColumnDataSource\n",
    "from bokeh.plotting import figure\n",
    "\n",
    "\n",
    "class SineWave(param.Parameterized):\n",
    "    offset = param.Number(default=0.0, bounds=(-5.0, 5.0))\n",
    "    amplitude = param.Number(default=1.0, bounds=(-5.0, 5.0))\n",
    "    phase = param.Number(default=0.0, bounds=(0.0, 2 * np.pi))\n",
    "    frequency = param.Number(default=1.0, bounds=(0.1, 5.1))\n",
    "    N = param.Integer(default=200, bounds=(0, None))\n",
    "    x_range = param.Range(default=(0, 4 * np.pi), bounds=(0, 4 * np.pi))\n",
    "    y_range = param.Range(default=(-2.5, 2.5), bounds=(-10, 10))\n",
    "\n",
    "    def __init__(self, **params):\n",
    "        super(SineWave, self).__init__(**params)\n",
    "        x, y = self.sine()\n",
    "        self.cds = ColumnDataSource(data=dict(x=x, y=y))\n",
    "        self.plot = figure(plot_height=400, plot_width=400,\n",
    "                           tools=\"crosshair, pan, reset, save, wheel_zoom\",\n",
    "                           x_range=self.x_range, y_range=self.y_range)\n",
    "        self.plot.line('x', 'y', source=self.cds, line_width=3, line_alpha=0.6)\n",
    "\n",
    "    @param.depends('N', 'frequency', 'amplitude', 'offset', 'phase', 'x_range', 'y_range', watch=True)\n",
    "    def update_plot(self):\n",
    "        x, y = self.sine()\n",
    "        self.cds.data = dict(x=x, y=y)\n",
    "        self.plot.x_range.start, self.plot.x_range.end = self.x_range\n",
    "        self.plot.y_range.start, self.plot.y_range.end = self.y_range\n",
    "\n",
    "    def sine(self):\n",
    "        x = np.linspace(0, 4 * np.pi, self.N)\n",
    "        y = self.amplitude * np.sin(self.frequency * x + self.phase) + self.offset\n",
    "        return x, y\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "However the app itself is defined we need to configure an entry point, which is a function that accepts a bokeh Document and adds the application to it. In case of the slider app it looks like this:\n",
    "\n",
    "```python\n",
    "import panel as pn\n",
    "\n",
    "from .sinewave import SineWave\n",
    "\n",
    "def app(doc):\n",
    "    sw = SineWave()\n",
    "    row = pn.Row(sw.param, sw.plot)\n",
    "    row.server_doc(doc)\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we create a ``views.py`` file which returns a view the Django server can render:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "# Create your views here.\n",
    "from bokeh.embed import server_document\n",
    "\n",
    "from django.http import HttpRequest, HttpResponse\n",
    "from django.shortcuts import render\n",
    "\n",
    "\n",
    "def sliders(request: HttpRequest) -> HttpResponse:\n",
    "    script = server_document(request.build_absolute_uri())\n",
    "    return render(request, \"base.html\", dict(script=script))\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `base.html` template should be in the `TEMPLATES` `DIRS` directory we declared in the `settings.py` file above. A very basic template might look like this but can be as complex as you need:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```html\n",
    "<!DOCTYPE html>\n",
    "<html>\n",
    "  <head>\n",
    "    <title>Panel in Django: sliders</title>\n",
    "  </head>\n",
    "  <body>\n",
    "  {% block content %}\n",
    "    {{ script|safe }}\t  \n",
    "  {% endblock %}\n",
    "  </body>\n",
    "</html>\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we declare a `urls.py` file to declare the urlpattern where to serve the sliders app to Django:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```python\n",
    "from django.urls import path\n",
    "\n",
    "from . import views\n",
    "\n",
    "app_name = 'sliders'\n",
    "urlpatterns = [\n",
    "    path('', views.sliders, name='sliders'),\n",
    "]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should be able to run this app yourself by changing to the `examples/apps/django2` directory and then running: `python manage.py runserver`; then visit http://localhost:8000/sliders in your browser to try the app."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multiple apps\n",
    "\n",
    "\n",
    "This is the most basic configuration for a bokeh server. It is of course possible to add multiple apps in the same way and then registering them with Django in the way described in the [configuration](#Configuration) section above. To see a multi-app Django server have a look at ``examples/apps/django_multi_apps`` and launch it with `python manage.py runserver` as before."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
