Advanced Concepts

Last Updated: June 2017

This tutorial introduces advanced concepts for Tethys developers. The topics covered include:

  • Tethys Services API
  • PersistentStores API
  • Gizmo JavaScript APIs
  • JavaScript and AJAX
  • Permissions API
  • Advanced HTML forms - File Upload
  • Plotting Gizmos

0. Start From Intermediate Solution (Optional)

If you wish to use the intermediate solution as a starting point:

$ git clone https://github.com/tethysplatform/tethysapp-dam_inventory.git
$ cd tethysapp-dam_inventory
$ git checkout intermediate-solution

1. Persistent Store Database

In the Intermediate Concepts tutorial we implemented a file-based database as the persisting mechanism for the app. However, simple file based databases typically don't perform well in a web application environment, because of the possibility of many concurrent requests trying to access the file. In this section we'll refactor the Model to use an SQL database, rather than files.

  1. Open the app.py and define a new PersistentStoreDatabaseSetting by adding the persistent_store_settings method to your app class:
from tethys_sdk.app_settings import PersistentStoreDatabaseSetting

class DamInventory(TethysAppBase):
    """
    Tethys app class for Dam Inventory.
    """
    ...
    def persistent_store_settings(self):
        """
        Define Persistent Store Settings.
        """
        ps_settings = (
            PersistentStoreDatabaseSetting(
                name='primary_db',
                description='primary database',
                initializer='dam_inventory.model.init_primary_db',
                required=True
            ),
        )

        return ps_settings

Tethys provides the library SQLAlchemy as an interface with SQL databases. SQLAlchemy provides an Object Relational Mapper (ORM) API, which allows data models to be defined using Python and an object-oriented approach. With SQLAlchemy, you can harness the power of SQL databases without writing SQL. As a primer to SQLAlchemy ORM, we highly recommend you complete the Object Relational Tutorial.

  1. Define a table called dams by creating a new class in model.py called Dam:
import json
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Float, String
from sqlalchemy.orm import sessionmaker

from .app import DamInventory as app

Base = declarative_base()


# SQLAlchemy ORM definition for the dams table
class Dam(Base):
    """
    SQLAlchemy Dam DB Model
    """
    __tablename__ = 'dams'

    # Columns
    id = Column(Integer, primary_key=True)
    latitude = Column(Float)
    longitude = Column(Float)
    name = Column(String)
    owner = Column(String)
    river = Column(String)
    date_built = Column(String)

Tip

SQLAlchemy Data Models: Each class in an SQLAlchemy data model defines a table in the database. The model you defined above consists of a single table called "dams", as denoted by the __tablename__ property of the Dam class. The Dam class inherits from a Base class that we created in the previous lines from the declarative_base function. This inheritance notifies SQLAlchemy that the Dam class is part of the data model.

The class defines seven other properties that are instances of SQLAlchemy Column class: id, latitude, longitude, name, owner, river, date_built. These properties define the columns of the "dams" table. The column type and options are defined by the arguments passed to the Column class. For example, the latitude column is of type Float while the id column is of type Integer. The id column is flagged as the primary key for the table. IDs will be generated for each object when they are committed.

This class is not only used to define the tables for your persistent store, it is also used to create new entries and query the database.

For more information on Persistent Stores, see: Persistent Stores API.

  1. Refactor the add_new_dam and get_all_dams functions in model.py to use the SQL database instead of the files:
def add_new_dam(location, name, owner, river, date_built):
    """
    Persist new dam.
    """
    # Convert GeoJSON to Python dictionary
    location_dict = json.loads(location)
    location_geometry = location_dict['geometries'][0]
    longitude = location_geometry['coordinates'][0]
    latitude = location_geometry['coordinates'][1]

    # Create new Dam record
    new_dam = Dam(
        latitude=latitude,
        longitude=longitude,
        name=name,
        owner=owner,
        river=river,
        date_built=date_built
    )

    # Get connection/session to database
    Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
    session = Session()

    # Add the new dam record to the session
    session.add(new_dam)

    # Commit the session and close the connection
    session.commit()
    session.close()


def get_all_dams():
    """
    Get all persisted dams.
    """
    # Get connection/session to database
    Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
    session = Session()

    # Query for all dam records
    dams = session.query(Dam).all()
    session.close()

    return dams

Important

Don't forget to close your session objects when you are done. Eventually you will run out of connections to the database if you don't, which will cause unsightly errors.

  1. Create a new function called init_primary_db at the bottom of model.py. This function is used to initialize the data database by creating the tables and adding any initial data.
def init_primary_db(engine, first_time):
    """
    Initializer for the primary database.
    """
    # Create all the tables
    Base.metadata.create_all(engine)

    # Add data
    if first_time:
        # Make session
        Session = sessionmaker(bind=engine)
        session = Session()

        # Initialize database with two dams
        dam1 = Dam(
            latitude=40.406624,
            longitude=-111.529133,
            name="Deer Creek",
            owner="Reclamation",
            river="Provo River",
            date_built="April 12, 1993"
        )

        dam2 = Dam(
            latitude=40.598168,
            longitude=-111.424055,
            name="Jordanelle",
            owner="Reclamation",
            river="Provo River",
            date_built="1941"
        )

        # Add the dams to the session, commit, and close
        session.add(dam1)
        session.add(dam2)
        session.commit()
        session.close()
  1. Refactor home controller in controllers.py to use new model objects:
@login_required()
def home(request):
    """
    Controller for the app home page.
    """
    # Get list of dams and create dams MVLayer:
    dams = get_all_dams()
    features = []
    lat_list = []
    lng_list = []

    for dam in dams:
        lat_list.append(dam.latitude)
        lng_list.append(dam.longitude)

        dam_feature = {
            'type': 'Feature',
            'geometry': {
                'type': 'Point',
                'coordinates': [dam.longitude, dam.latitude],

            },
            'properties': {
                'id': dam.id,
                'name': dam.name,
                'owner': dam.owner,
                'river': dam.river,
                'date_built': dam.date_built
            }
        }
        features.append(dam_feature)

    ...
  1. Refactor the list_dams controller to use the new model objects:
@login_required()
def list_dams(request):
    """
    Show all dams in a table view.
    """
    dams = get_all_dams()
    table_rows = []

    for dam in dams:
        table_rows.append(
            (
                dam.name, dam.owner,
                dam.river, dam.date_built
            )
        )

    ...
  1. Add Persistent Store Service to Tethys Portal:

    1. Go to Tethys Portal Home in a web browser (e.g. http://localhost:8000/apps/)
    2. Select Site Admin from the drop down next to your username.
    3. Scroll down to Tethys Services section and select Persistent Store Services link.
    4. Click on the Add Persistent Store Service button.
    5. Give the Persistent Store Service a name and fill out the connection information.

Important

The username and password for the persistent store service must be a superuser to use spatial persistent stores.

  1. Assign Persistent Store Service to Dam Inventory App:

    1. Go to Tethys Portal Home in a web browser (e.g. http://localhost:8000/apps/)
    2. Select Site Admin from the drop down next to your username.
    3. Scroll down to Tethys Apps section and select Installed App link.
    4. Select the Dam Inventory link.
    5. Scroll down to the Persistent Store Database Settings section.
    6. Assign the Persistent Store Service that you created in Step 4 to the primary_db.
  2. Execute syncstores command to initialize Persistent Store database:

    (tethys) $ tethys syncstores dam_inventory
    

2. Use Custom Settings

In the Beginner Concepts tutorial, we created a custom setting named max_dams. In this section, we'll show you how to use the custom setting in one of your controllers.

  1. Modify the add_dam controller, such that it won't add a new dam if the max_dams limit has been reached:
from .model import Dam
from .app import DamInventory as app

...

@login_required()
def add_dam(request):
    """
    Controller for the Add Dam page.
    """

    ...

    # Handle form submission
    if request.POST and 'add-button' in request.POST:

        ...

        if not has_errors:
            # Get value of max_dams custom setting
            max_dams = app.get_custom_setting('max_dams')

            # Query database for count of dams
            Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
            session = Session()
            num_dams = session.query(Dam).count()

            # Only add the dam if we have not exceed max_dams
            if num_dams < max_dams:
                add_new_dam(location=location, name=name, owner=owner, river=river, date_built=date_built)
            else:
                messages.warning(request, 'Unable to add dam "{0}", because the inventory is full.'.format(name))

            return redirect(reverse('dam_inventory:home'))

        messages.error(request, "Please fix errors.")

    ...

Tip

For more information on app settings, see App Settings API.

3. Use JavaScript APIs

JavaScript is the programming language that is used to program web browsers. You can use JavaScript in you Tethys apps to enrich the user experience and add dynamic effects. Many of the Tethys Gizmos include JavaScript APIs to allow you to access the underlying JavaScript objects and library to customize them. In this section, we'll use the JavaScript API of the Map View gizmo to add pop-ups to the map whenever the users clicks on one of the dams.

  1. Modify the MVLayer in the home controller to make the layer selectable:
...

dams_layer = MVLayer(

    ...

    feature_selection=True
)

...
  1. Create a new file called /public/js/map.js and add the following contents:
$(function()
{
    // Create new Overlay with the #popup element
    var popup = new ol.Overlay({
        element: document.getElementById('popup')
    });

    // Get the Open Layers map object from the Tethys MapView
    var map = TETHYS_MAP_VIEW.getMap();

    // Get the Select Interaction from the Tethys MapView
    var select_interaction = TETHYS_MAP_VIEW.getSelectInteraction();

    // Add the popup overlay to the map
    map.addOverlay(popup);

    // When selected, call function to display properties
    select_interaction.getFeatures().on('change:length', function(e)
    {
        var popup_element = popup.getElement();

        if (e.target.getArray().length > 0)
        {
            // this means there is at least 1 feature selected
            var selected_feature = e.target.item(0); // 1st feature in Collection

            // Get coordinates of the point to set position of the popup
            var coordinates = selected_feature.getGeometry().getCoordinates();

            var popup_content = '<div class="dam-popup">' +
                                    '<p><b>' + selected_feature.get('name') + '</b></p>' +
                                    '<table class="table  table-condensed">' +
                                        '<tr>' +
                                            '<th>Owner:</th>' +
                                            '<td>' + selected_feature.get('owner') + '</td>' +
                                        '</tr>' +
                                        '<tr>' +
                                            '<th>River:</th>' +
                                            '<td>' + selected_feature.get('river') + '</td>' +
                                        '</tr>' +
                                        '<tr>' +
                                            '<th>Date Built:</th>' +
                                            '<td>' + selected_feature.get('date_built') + '</td>' +
                                        '</tr>' +
                                    '</table>' +
                                '</div>';

            // Clean up last popup and reinitialize
            $(popup_element).popover('destroy');

            // Delay arbitrarily to wait for previous popover to
            // be deleted before showing new popover.
            setTimeout(function() {
                popup.setPosition(coordinates);

                $(popup_element).popover({
                  'placement': 'top',
                  'animation': true,
                  'html': true,
                  'content': popup_content
                });

                $(popup_element).popover('show');
            }, 500);
        } else {
            // remove pop up when selecting nothing on the map
            $(popup_element).popover('destroy');
        }
    });
});
  1. Open /templates/dam_inventory/home.html, add a new div element to the app_content area of the page with an id popup, and load the map.js script to the bottom of the page:
...

{% block app_content %}
  {% gizmo dam_inventory_map %}
  <div id="popup"></div>
{% endblock %}

...

{% block scripts %}
  {{ block.super }}
  <script src="{% static 'dam_inventory/js/map.js' %}" type="text/javascript"></script>
{% endblock %}
  1. Open public/css/map.css and add the following contents:
...

.popover-content {
    width: 240px;
}

4. App Permissions

By default, any user logged into the app can access any part of it. You may want to restrict access to certain areas of the app to privileged users. This can be done using the Permissions API. Let's modify the app so that only admin users of the app can add dams to the app.

  1. Define permissions for the app by adding the permissions method to the app class in the app.py:
...

from tethys_sdk.permissions import Permission, PermissionGroup

class DamInventory(TethysAppBase):
    """
    Tethys app class for Dam Inventory.
    """
    ...

    def permissions(self):
        """
        Define permissions for the app.
        """
        add_dams = Permission(
            name='add_dams',
            description='Add dams to inventory'
        )

        admin = PermissionGroup(
            name='admin',
            permissions=(add_dams,)
        )

        permissions = (admin,)

        return permissions
  1. Protect the Add Dam view with the add_dams permission by replacing the login_required decorator with the permission_required decorator to the add_dams controller:
from tethys_sdk.permissions import permission_required

...

@permission_required('add_dams')
def add_dam(request):
    """
    Controller for the Add Dam page.
    """
    ...
  1. Add a context variable called can_add_dams to the context of each controller with the value of the return value of the has_permission function:
from tethys_sdk.permissions import has_permission

@login_required()
def home(request):
    """
    Controller for the app home page.
    """
    ...

    context = {
        ...
        'can_add_dams': has_permission(request, 'add_dams')
    }

    return render(request, 'dam_inventory/home.html', context)


@permission_required('add_dams')
def add_dam(request):
    """
    Controller for the Add Dam page.
    """
    ...

    context = {
        ...
        'can_add_dams': has_permission(request, 'add_dams')
    }

    return render(request, 'dam_inventory/add_dam.html', context)


@login_required()
def list_dams(request):
    """
    Show all dams in a table view.
    """
    dams = get_all_dams()
    context = {
        ...
        'can_add_dams': has_permission(request, 'add_dams')
    }
    return render(request, 'dam_inventory/list_dams.html', context)
  1. Use the can_add_dams variable to determine whether to show or hide the navigation link to the Add Dam View in base.html:
{% block app_navigation_items %}
  ...
  <li class="{% if request.path == home_url %}active{% endif %}"><a href="{{ home_url }}">Home</a></li>
  <li class="{% if request.path == list_dam_url %}active{% endif %}"><a href="{{ list_dam_url }}">Dams</a></li>
  {% if can_add_dams %}
  <li class="{% if request.path == add_dam_url %}active{% endif %}"><a href="{{ add_dam_url }}">Add Dam</a></li>
  {% endif %}
{% endblock %}
  1. Use the can_add_dams variable to determine whether to show or hide the "Add Dam" button in home.html:
{% block app_actions %}
  {% if can_add_dams %}
    {% gizmo add_dam_button %}
  {% endif %}
{% endblock %}
  1. The admin user of Tethys is a superuser and has all permissions. To test the permissions, create two new users: one with the admin permissions group and one without it. Then login with these users:

    1. Go to Tethys Portal Home in a web browser (e.g. http://localhost:8000/apps/)
    2. Select Site Admin from the drop down next to your username.
    3. Scroll to the Authentication and Authorization section.
    4. Select the Users link.
    5. Press the Add User button.
    6. Enter "diadmin" as the username and enter a password. Take note of the password for later.
    7. Press the Save button.
    8. Scroll down to the Groups section.
    9. Select the dam_inventory:admin group and press the right arrow to add the user to that group.
    10. Press the Save button.
    11. Repeat steps e-f for user named "diviewer". DO NOT add "diviewer" user to any groups.
    12. Press the Save button.
  2. Log in each user. If the permission has been applied correctly, "diviewer" should not be able to see the Add Dam link and should be redirected if the Add Dam view is linked to directly. "diadmin" should be able to add dams.

Tip

For more details on Permissions, see: Permissions API.

Todo

Split into another tutorial here?

6. File Upload

CSV File Upload Create new page for uploading the hydrograph.

  1. New Model function
def assign_hydrograph_to_dam(dam_id, hydrograph_file):
    """
    Parse hydrograph file and add to database, assigning to appropriate dam.
    """
    # Parse file
    hydro_points = []

    try:

        for line in hydrograph_file:
            sline = line.split(',')

            try:
                time = int(sline[0])
                flow = float(sline[1])
                hydro_points.append(HydrographPoint(time=time, flow=flow))
            except ValueError:
                continue

        if len(hydro_points) > 0:
            Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
            session = Session()

            # Get dam object
            dam = session.query(Dam).get(int(dam_id))

            # Overwrite old hydrograph
            hydrograph = dam.hydrograph

            # Create new hydrograph if not assigned already
            if not hydrograph:
                hydrograph = Hydrograph()
                dam.hydrograph = hydrograph

            # Remove old points if any
            for hydro_point in hydrograph.points:
                session.delete(hydro_point)

            # Assign points to hydrograph
            hydrograph.points = hydro_points

            # Persist to database
            session.commit()
            session.close()

    except Exception as e:
        # Careful not to hide error. At the very least log it to the console
        print(e)
        return False

    return True
  1. New Template: assign_hydrograph.html
{% extends "dam_inventory/base.html" %}
{% load tethys_gizmos %}

{% block app_content %}
  <h1>Assign Hydrograph</h1>
  <p>Select a dam and a hydrograph file to assign to that dam. The file should be a csv with two columns: time (hours) and flow (cfs).</p>
  <form id="add-hydrograph-form" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {% gizmo dam_select_input %}
    <div class="form-group{% if hydrograph_file_error %} has-error{% endif %}">
      <label class="control-label">Hydrograph File</label>
      <input type="file" name="hydrograph-file">
      {% if hydrograph_file_error %}<p class="help-block">{{ hydrograph_file_error }}</p>{% endif %}
    </div>
  </form>
{% endblock %}

{% block app_actions %}
  {% gizmo cancel_button %}
  {% gizmo add_button %}
{% endblock %}
  1. New Controller
from .model import assign_hydrograph_to_dam

...

@login_required()
def assign_hydrograph(request):
    """
    Controller for the Add Hydrograph page.
    """
    # Get dams from database
    Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
    session = Session()
    all_dams = session.query(Dam).all()

    # Defaults
    dam_select_options = [(dam.name, dam.id) for dam in all_dams]
    selected_dam = None
    hydrograph_file = None

    # Errors
    dam_select_errors = ''
    hydrograph_file_error = ''

    # Case where the form has been submitted
    if request.POST and 'add-button' in request.POST:
        # Get Values
        has_errors = False
        selected_dam = request.POST.get('dam-select', None)

        if not selected_dam:
            has_errors = True
            dam_select_errors = 'Dam is Required.'

        # Get File
        if request.FILES and 'hydrograph-file' in request.FILES:
            # Get a list of the files
            hydrograph_file = request.FILES.getlist('hydrograph-file')

        if not hydrograph_file and len(hydrograph_file) > 0:
            has_errors = True
            hydrograph_file_error = 'Hydrograph File is Required.'

        if not has_errors:
            # Process file here
            success = assign_hydrograph_to_dam(selected_dam, hydrograph_file[0])

            # Provide feedback to user
            if success:
                messages.info(request, 'Successfully assigned hydrograph.')
            else:
                messages.info(request, 'Unable to assign hydrograph. Please try again.')
            return redirect(reverse('dam_inventory:home'))

        messages.error(request, "Please fix errors.")

    dam_select_input = SelectInput(
        display_text='Dam',
        name='dam-select',
        multiple=False,
        options=dam_select_options,
        initial=selected_dam,
        error=dam_select_errors
    )

    add_button = Button(
        display_text='Add',
        name='add-button',
        icon='glyphicon glyphicon-plus',
        style='success',
        attributes={'form': 'add-hydrograph-form'},
        submit=True
    )

    cancel_button = Button(
        display_text='Cancel',
        name='cancel-button',
        href=reverse('dam_inventory:home')
    )

    context = {
        'dam_select_input': dam_select_input,
        'hydrograph_file_error': hydrograph_file_error,
        'add_button': add_button,
        'cancel_button': cancel_button,
        'can_add_dams': has_permission(request, 'add_dams')
    }

    session.close()

    return render(request, 'dam_inventory/assign_hydrograph.html', context)
  1. New UrlMap
class DamInventory(TethysAppBase):
    """
    Tethys app class for Dam Inventory.
    """
    ...

    def url_maps(self):
        """
        Add controllers
        """
        UrlMap = url_map_maker(self.root_url)

        url_maps = (

            ...

            UrlMap(
                name='assign_hydrograph',
                url='dam-inventory/hydrographs/assign',
                controller='dam_inventory.controllers.assign_hydrograph'
            ),
        )

        return url_maps
  1. Update navigation
{% block app_navigation_items %}
  <li class="title">App Navigation</li>
  ...
  {% url 'dam_inventory:assign_hydrograph' as assign_hydrograph_url %}
  ...
  <li class="{% if request.path == assign_hydrograph_url %}active{% endif %}"><a href="{{ assign_hydrograph_url }}">Assign Hydrograph</a></li>
{% endblock %}
  1. Test upload with these files:

7. URL Variables and Plotting

Create a new page with hydrograph plotted for selected Dam

  1. Create Template hydrograph.html
{% extends "dam_inventory/base.html" %}
{% load tethys_gizmos %}

{% block app_navigation_items %}
  <li class="title">App Navigation</li>
  <li class=""><a href="{% url 'dam_inventory:dams' %}">Back</a></li>
{% endblock %}

{% block app_content %}
  {% gizmo hydrograph_plot %}
{% endblock %}
  1. Create helpers.py
from plotly import graph_objs as go
from tethys_gizmos.gizmo_options import PlotlyView

from tethysapp.dam_inventory.app import DamInventory as app
from tethysapp.dam_inventory.model import Hydrograph


def create_hydrograph(hydrograph_id, height='520px', width='100%'):
    """
    Generates a plotly view of a hydrograph.
    """
    # Get objects from database
    Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
    session = Session()
    hydrograph = session.query(Hydrograph).get(int(hydrograph_id))
    dam = hydrograph.dam
    time = []
    flow = []
    for hydro_point in hydrograph.points:
        time.append(hydro_point.time)
        flow.append(hydro_point.flow)

    # Build up Plotly plot
    hydrograph_go = go.Scatter(
        x=time,
        y=flow,
        name='Hydrograph for {0}'.format(dam.name),
        line={'color': '#0080ff', 'width': 4, 'shape': 'spline'},
    )
    data = [hydrograph_go]
    layout = {
        'title': 'Hydrograph for {0}'.format(dam.name),
        'xaxis': {'title': 'Time (hr)'},
        'yaxis': {'title': 'Flow (cfs)'},
    }
    figure = {'data': data, 'layout': layout}
    hydrograph_plot = PlotlyView(figure, height=height, width=width)
    session.close()
    return hydrograph_plot
  1. Create Controller
from .helpers import create_hydrograph

...

@login_required()
def hydrograph(request, hydrograph_id):
    """
    Controller for the Hydrograph Page.
    """
    hydrograph_plot = create_hydrograph(hydrograph_id)

    context = {
        'hydrograph_plot': hydrograph_plot,
        'can_add_dams': has_permission(request, 'add_dams')
    }
    return render(request, 'dam_inventory/hydrograph.html', context)

Tip

For more information about plotting in Tethys apps, see Plotly View, Bokeh View, and Plot View.

  1. Add UrlMap with URL Variable
class DamInventory(TethysAppBase):
    """
    Tethys app class for Dam Inventory.
    """
    ...

    def url_maps(self):
        """
        Add controllers
        """
        UrlMap = url_map_maker(self.root_url)

        url_maps = (
            ...

            UrlMap(
                name='hydrograph',
                url='dam-inventory/hydrographs/{hydrograph_id}',
                controller='dam_inventory.controllers.hydrograph'
            ),
        )

        return url_maps
  1. Modify list_dams controller:
.. todo::

    Find a way to get links into data tables view

8. Dynamic Hydrograph Plot in Pop-Ups

Add Hydrographs to pop-ups if they exist.

  1. Add Plotly Gizmo dependency to home.html:
{% extends "dam_inventory/base.html" %}
{% load tethys_gizmos staticfiles %}

{% block import_gizmos %}
  {% import_gizmo_dependency plotly_view %}
{% endblock %}

...
  1. Create a template for the AJAX plot (hydrograph_ajax.html)
{% load tethys_gizmos %}

{% if hydrograph_plot %}
  {% gizmo hydrograph_plot %}
{% endif %}
  1. Create an AJAX controller hydrograph_ajax
@login_required()
def hydrograph_ajax(request, dam_id):
    """
    Controller for the Hydrograph Page.
    """
    # Get dams from database
    Session = app.get_persistent_store_database('primary_db', as_sessionmaker=True)
    session = Session()
    dam = session.query(Dam).get(int(dam_id))

    if dam.hydrograph:
        hydrograph_plot = create_hydrograph(dam.hydrograph.id, height='300px')
    else:
        hydrograph_plot = None

    context = {
        'hydrograph_plot': hydrograph_plot,
    }

    session.close()
    return render(request, 'dam_inventory/hydrograph_ajax.html', context)
  1. Create an AJAX UrlMap
class DamInventory(TethysAppBase):
    """
    Tethys app class for Dam Inventory.
    """
    ...

    def url_maps(self):
        """
        Add controllers
        """
        UrlMap = url_map_maker(self.root_url)

        url_maps = (
            ...

            UrlMap(
                name='hydrograph_ajax',
                url='dam-inventory/hydrographs/{dam_id}/ajax',
                controller='dam_inventory.controllers.hydrograph_ajax'
            ),
        )

        return url_maps
  1. Load the plot dynamically using JavaScript and AJAX (modify map.js)
$(function()
{
    // Create new Overlay with the #popup element
    var popup = new ol.Overlay({
        element: document.getElementById('popup')
    });

    // Get the Open Layers map object from the Tethys MapView
    var map = TETHYS_MAP_VIEW.getMap();

    // Get the Select Interaction from the Tethys MapView
    var select_interaction = TETHYS_MAP_VIEW.getSelectInteraction();

    // Add the popup overlay to the map
    map.addOverlay(popup);

    // When selected, call function to display properties
    select_interaction.getFeatures().on('change:length', function(e)
    {
        var popup_element = popup.getElement();

        if (e.target.getArray().length > 0)
        {
            // this means there is at least 1 feature selected
            var selected_feature = e.target.item(0); // 1st feature in Collection

            // Get coordinates of the point to set position of the popup
            var coordinates = selected_feature.getGeometry().getCoordinates();

            // Load hydrograph dynamically with AJAX
            $.ajax({
                url: '/apps/dam-inventory/hydrographs/' + selected_feature.get('id') + '/ajax/',
                method: 'GET',
                success: function(plot_html) {
                    var popup_content = '<div class="dam-popup">' +
                        '<p><b>' + selected_feature.get('name') + '</b></p>' +
                        '<table class="table  table-condensed">' +
                            '<tr>' +
                                '<th>Owner:</th>' +
                                '<td>' + selected_feature.get('owner') + '</td>' +
                            '</tr>' +
                            '<tr>' +
                                '<th>River:</th>' +
                                '<td>' + selected_feature.get('river') + '</td>' +
                            '</tr>' +
                            '<tr>' +
                                '<th>Date Built:</th>' +
                                '<td>' + selected_feature.get('date_built') + '</td>' +
                            '</tr>' +
                        '</table>' +
                        plot_html +
                    '</div>';

                    // Clean up last popup and reinitialize
                    $(popup_element).popover('destroy');

                    // Delay arbitrarily to wait for previous popover to
                    // be deleted before showing new popover.
                    setTimeout(function() {
                        popup.setPosition(coordinates);

                        $(popup_element).popover({
                          'placement': 'top',
                          'animation': true,
                          'html': true,
                          'content': popup_content
                        });

                        $(popup_element).popover('show');
                    }, 500);
                }
            });

        } else {
            // remove pop up when selecting nothing on the map
            $(popup_element).popover('destroy');
        }
    });
});
  1. Update map.css:
.popover-content {
    width: 400px;
    max-height: 300px;
    overflow-y: auto;
}

.popover {
    max-width: none;
}

#inner-app-content {
    padding: 0;
}

#app-content, #inner-app-content, #map_view_outer_container {
    height: 100%;
}

9. Solution

This concludes the Advanced Tutorial. You can view the solution on GitHub at https://github.com/tethysplatform/tethysapp-dam_inventory or clone it as follows:

$ git clone https://github.com/tethysplatform/tethysapp-dam_inventory.git
$ cd tethysapp-dam_inventory
$ git checkout advanced-solution