-
Notifications
You must be signed in to change notification settings - Fork 0
How To Add Scenarios to a MarinePlanner Project
- 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
toINSTALLED_APPS
in your settings.py file. - Be sure to also have madrona-features and madrona-analysistools installed.
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),
If Planning Units are part of your Scenario-building workflow, you will need to change the following:
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)
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)
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 = {}
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)
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
- Call Scenario's version of this view, but passing it results from your custom
-
def get_filter_results(request, query=False, notes=[]):
- same reason as
get_filter_count
- same reason as
-
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)
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.
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:
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/'
- generally this uses madrona-features' API and looks like
- 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}'
- generally something like
- 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'sviews.py
file.