Skip to content

How To Add Scenarios to a MarinePlanner Project

Ryan Hodges edited this page Feb 1, 2018 · 6 revisions

Installation

  • clone this repo into your project
    • git clone https://github.com/Ecotrust/madrona-scenarios.git
  • Activate your python virtualenvironment (if using one)
  • pip install -e /absolute/path/to/madrona-scenarios
  • Add scenarios to INSTALLED_APPS in your settings.py file.
  • Be sure to also have madrona-features and madrona-analysistools installed.

Scenario Models

To include Madrona Scenario models in a custom MarinePlanner application, make the following additions to your models.py file:

from django.db import models
from features.registry import register
from scenarios.models import Scenario

@register
class CustomScenario(Scenario):   ### This class can have any name to fit your needs
    field = models....   ### add your fields that you would like to have for filtering or reporting
    ...
    class Options:
        verbose_name = 'Name of your Scenario Objects'
        icon_url = 'a/url/that/is/probably/good/for/something.png'
        form = 'customapp.forms.ScenarioForm'   ### The Django form to be used for creating/editing scenarios
        form_template = 'customapp/form.html'   ### A template for displaying your form in the browser
        show_template = 'customapp/show.html'   ### A template for viewing scenario details (default = 'scenarios/show.html')

You will need to create your custom icon image, form, and templates. Vanilla examples can be found in this project.

Be sure to make relevant edits to your custom app's urls.py file for each API endpoint you wish to hit:

url(r'get_scenarios$', get_scenarios),

Filtering and Planning Units

If Planning Units are part of your Scenario-building workflow, you will need to change the following:

Models.py

Expand on the generic PlanningUnit model in this app by once again modifying your models.py file:

from scenarios.models import PlanningUnit

class CustomPlanningUnit(PlanningUnit):
    # your_fields = ....

Add something like this to your custom Scenarios model:

    def run_filters(self, query):
        # logic to filter your custom PlanningUnits by your scenario's values
        return query
    
    def run(self, result=None):
        result = {CUSTOM_PLANNING_UNIT_MODEL}.objects.all()
        return super(type(self), self).run(result)

Scenario Fields (area example)

Filters come in several different flavors, and using them is dependent on your field names. First off, the form will need to know whether or not to apply a filter, so start of with a boolean. Name it something simple but descriptive. For this example, we're going to filter on a field called 'area':

    area = models.BooleanField(default=False)

In some cases, filtering on a yes/no clause is going to be enough. In many cases, filtering needs to be more complex. There are 4 filter types prepared for handling these:

  • min: filter out planning units with a value less than a given number
  • max: filter out planning units with a value greater than a given number
  • input: filter out planning units with a value that does not match a condition related to an input string
  • checkbox: filter out planning units that do not match a series of yes/no conditions

To add these, you will need to define further fields in your custom Scenario model, naming them with the base field name, then followed by an underscore (_) and finally the filter type listed above.

Now you are likely to want people to filter out planning units that have either too many or too few square meters of area, so we'll use some float fields (or Integer if you don't need to be flexible):

    area_min = models.FloatField(null=True, blank=True, default=None)
    area_max = models.FloatField(null=True, blank=True, default=None)

forms.py

You will need to import any specialty widgets (such as the SliderWidgets from analysistools) as well as ScenarioForm:

from .models import *
from scenarios.forms import ScenarioForm
from django import forms
from django.forms.widgets import *
from analysistools.widgets import SliderWidget, DualSliderWidget

Create a custom form based on ScenarioForm and make sure your form fields to match the scenario fields you wish to filter on. For the acres example:

class CustomScenarioForm(ScenarioForm):
    area = forms.BooleanField(
        label = "Area",
        help_text="Only include planning units with area within a given range",
        required=False,
        widget=CheckboxInput(
            attrs={
                'class': 'parameters hidden_checkbox'
            }
        )
    area_min = forms.FloatField(
        required=False,
        initial=100,
        widget=forms.TextInput(
            attrs={
                'class': 'slidervalue',
                'pre_text': 'Area'
            }
        )
    )
    area_max = forms.FloatField(
        required=False,
        initial=500,
        widget=forms.TextInput(
            attrs={
                'class': 'slidervalue',
                'pre_text': 'to',
                'post_text': 'm<sup>2</sup>'
            }
        )
    )
    area_input = forms.FloatField(
        widget=DualSliderWidget(
            'area_min',
            'area_max',
            min=0,
            max=1000,
            step=100
        )
    )

Next, the Filter form wizard relies on having each filter provided as a list of the form: Base field, minimum value field, maximum value field, input field, checkboxes fields

Scenario forms have a built-in function for translating given text into fields called _get_fields() that takes as an argument a list of the above lists. You need to override get_steps(self) to return a list of those results. This is traditionally broken up into steps, so that the form can be presented in a number of pages.

Our example code would look like:

    def get_step_0_fields(self):
        names = [
            ('area', 'area_min', 'area_max', 'area_input'),
        ]
        return self._get_fields(names)
    def get_stes(self):
        return self.get_step_0_fields(),

Note that if you don't have 4 or more fields to put in, replace the missing values with None, like ('area', None, None, 'area_input'). Checkbox fields are added as a fifth list item with no max, min, or input values (see PEW-EFH Checkbox field definition and filter list )

Also note the 'comma' after the return - the return expects a list of values to go through. If you only have one page, and do not add the comma to indicate a 'list of 1' then you will get errors.

Finally add a Meta section for your form to associate it with your custom scenario model:

    class Meta(ScenarioForm.Meta):
        model = YOUR_CUSTOM_SCENARIO_MODEL
        exclude = list(ScenarioForm.Meta.exclude)
        for f in model.output_fields():
            exclude.append(f.attname)
        widgets = {}

Frontend (js/html)

It is very unfortunate that the current form depends on the Knockout.js bindings to not only manipulate the map to reveal the results, but to even manage the filter form wizard itself. Until this dependency is removed, you will need to create a custom scenarios.js file:

  • Copy scenarios.js to your custom app's staticfiles folder
  • Include your scenarios.js in your html file that will be displaying the form.
    • For demos this will be in a customized copy of demo.html
  • Edit your scenarios.js inside of the definition of ScenarioFormModels to add observables for your base fields:
function scenarioFormModel(options) {
    var self = this;
    self.area = ko.observable(false);
    ...

Note: You do not need to add observables for non-base filter fields (_min, _max, _input, or _checkbox)

views.py

You need to override the following views in your custom app:

  • def run_filter_query(filters):
    • like you did in your custom scenario model's 'run_filters', you need to re-write all of your filter logic here. This is so that filtering can be done before you have a real scenario instance.
  • def get_filter_count(request, query=False, notes=[]):
    • Call Scenario's version of this view, but passing it results from your custom run_filter_query
  • def get_filter_results(request, query=False, notes=[]):
    • same reason as get_filter_count
  • def get_planningunits(request):
    • convert your custom planningunits into json
  • def get_scenarios(request, scenario_model='YOUR_CUSTOM_SCENARIO_MODEL_NAME'):
    • call scenarios.views.get_scenarios passing along the name of your custom scenario model (all lowercase)

urls.py

Be sure to make relevant edits to your custom app's urls.py file for each API endpoint you wish to hit:

url(r'get_planningunits$', views.get_planningunits),
url(r'get_filter_count$', views.get_filter_count),
url(r'get_filter_results$', views.get_filter_results),
....

Be sure to check out Updating Planning Units for more info on how to load these and integrate the models.

Demo and Testing Filters

Scenarios has a built-in demo page, you can access it at /scenario/demo once you have scenarios installed. Of course, being a demo, you'll need to add planning-unit records, and the only attribute worth filtering on to create scenarios is area.

As you create your custom planning units and scenarios, you will want to test these as well, and hijacking the demo page is the perfect place to test this out. There are two ways to do this:

Use custom settings

Update your project's settings.py file to include:

  • GET_SCENARIOS_URL = a custom view endpoint to request all custom scenarios objects
  • SCENARIO_FORM_URL = a custom form for filtering your planning units into scenarios
    • generally this uses madrona-features' API and looks like '/features/{custom_scenario_model}/form/'
  • SCENARIO_LINK_BASE = prefix for interacting with your scenario via the Madrona API:
    • generally something like '/features/{custom_scenario_model}/{module_name}_{custom_scenario_model}'

Override the 'demo' view

  • Update your project's urls.py file to catch '/scenario/demo/$' and pass it to a custom view
  • copy/paste/edit the Scenario demo view as necessary into your custom module's views.py file.