577 lines
15 KiB
HTML
577 lines
15 KiB
HTML
{% extends "base_page.html" %}
|
|
{% load compress crispy_forms_tags i18n leaflet_tags static %}
|
|
|
|
{% block stylesheets %}
|
|
{{ block.super }}
|
|
{% leaflet_css %}
|
|
<style> html, body, #main { width: 100; height:100%; } </style>
|
|
|
|
<style>
|
|
.savebutton {
|
|
position: fixed;
|
|
bottom: 0;
|
|
padding: 8px 15px;
|
|
z-index: 9999;
|
|
|
|
background-color: hsla(111, 25%, 84%, 1);
|
|
border-top-right-radius: 5px;
|
|
border-top-left-radius: 5px;
|
|
}
|
|
|
|
.savebutton--icon { margin-left: 15px; margin-right: 2px; }
|
|
.savebutton--icon-failed { color: #d9534f; }
|
|
.savebutton--details { font-style: italic; }
|
|
|
|
.draftprompt { display: none; }
|
|
.draftprompt--delete { margin-left: 10px; }
|
|
.draftprompt--details { margin-left: 10px; font-weight: bold; }
|
|
|
|
.zoomhelptext {
|
|
font-size: 14px;
|
|
background-color: rgba(255, 255, 255, 80%);
|
|
padding: 2px 10px;
|
|
border: 1px solid #ccc;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.filewidget--input { position: absolute; left: -1000px; }
|
|
.filewidget--list {
|
|
list-style-type: none;
|
|
padding-left: 0;
|
|
margin-left: 0;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.filewidget--file {
|
|
border: 1px solid #aaa;
|
|
display: inline-block;
|
|
padding: 4px 8px;
|
|
width: 34em;
|
|
background-color: #eee;
|
|
border-radius: 4px;
|
|
cursor: default;
|
|
}
|
|
|
|
.filewidget--file--icon {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.filewidget--file--actions {
|
|
float: right;
|
|
}
|
|
|
|
.filewidget--file--actions a {
|
|
color: black;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.filewidget--file--actions a:hover {
|
|
color: red;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block page_title %}{% trans "Submit a Case Study" %} - {{ block.super }}{% endblock %}
|
|
{% block description %}{% trans "Here you can submit a case study for review and it will be added to the map." %}{% endblock %}
|
|
|
|
{% block inner %}
|
|
<div class="draftprompt alert alert-info">
|
|
<div>
|
|
<b class="lead">{% trans "You have a saved draft." %}</b>
|
|
<p>{% trans "Do you want to restore it? Restoring the draft will overwrite any other data you have input into the form below." %}</p>
|
|
</div>
|
|
<button class="draftprompt--restore btn btn-success">{% trans "Restore draft" %}</button>
|
|
<button class="draftprompt--delete btn btn-default">{% trans "Delete draft" %}</button>
|
|
<span class="draftprompt--details"></span>
|
|
</div>
|
|
|
|
<div class="savebutton" style="display: none">
|
|
<button class="savebutton--button btn btn-success">{% trans "Save" %}</button>
|
|
<i class="savebutton--icon"></i> <span class="savebutton--details"></span>
|
|
</div>
|
|
|
|
<input type="hidden" id="form_errors" value="{% if form.errors %}true{% else %}false{% endif %}">
|
|
|
|
{% crispy form %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{{ form.media }}
|
|
{% leaflet_js %}
|
|
<script>
|
|
ZoomHelpText = L.Control.extend({
|
|
options: {
|
|
position: 'bottomleft'
|
|
},
|
|
onAdd: function (map) {
|
|
return L.DomUtil.create('div', 'zoomhelptext')
|
|
},
|
|
setContent: function (content) {
|
|
this.getContainer().innerHTML = content
|
|
}
|
|
})
|
|
|
|
// See GeometryField source (static/leaflet/leaflet.forms.js) to override more stuff...
|
|
MinimumZoomField = L.GeometryField.extend({
|
|
zoomLevelTooLow: function() {
|
|
if (this._controlsShown !== false) {
|
|
this._map.removeControl(this._drawControl)
|
|
this._controlsShown = false
|
|
}
|
|
|
|
this._zoomHelpText.setContent(
|
|
"{% trans "Please zoom in further to place a marker on the map." %}"
|
|
)
|
|
},
|
|
|
|
zoomLevelOk: function() {
|
|
if (this._controlsShown !== true) {
|
|
this._map.addControl(this._drawControl)
|
|
this._controlsShown = true
|
|
}
|
|
|
|
this._zoomHelpText.setContent(
|
|
"{% trans "Please use the marker tool on the left to select a location." %}"
|
|
)
|
|
},
|
|
|
|
addTo: function (map) {
|
|
// super()
|
|
L.GeometryField.prototype.addTo.call(this, map)
|
|
this._controlsShown = true
|
|
this._map = map
|
|
|
|
// Add a help text control
|
|
this._zoomHelpText = new ZoomHelpText().addTo(map)
|
|
|
|
// Only allow editing past a certain zoom level (see #56)
|
|
// Remove the edit controls
|
|
this.zoomLevelTooLow()
|
|
|
|
// Enable or disable depending on zoom level
|
|
map.addEventListener('zoomend', evt => {
|
|
if (map.getZoom() >= 13) {
|
|
this.zoomLevelOk()
|
|
} else {
|
|
this.zoomLevelTooLow()
|
|
}
|
|
})
|
|
|
|
// Respond to underlying text field changing
|
|
const textarea = document.getElementById(this.options.fieldid)
|
|
textarea.addEventListener('change', evt => {
|
|
this.load()
|
|
})
|
|
|
|
const triggerChange = evt => {
|
|
document.getElementById('case-study-form').dispatchEvent(new Event('dirty'))
|
|
}
|
|
map.on(L.Draw.Event.CREATED, triggerChange)
|
|
map.on(L.Draw.Event.EDITED, triggerChange)
|
|
map.on(L.Draw.Event.DELETED, triggerChange)
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<!-- Conditional logic for hiding and un-hiding fields. -->
|
|
<script>
|
|
// Here we define the fields we need to conditionally toggle.
|
|
// TODO: Move this knowledge out of the template
|
|
var conditionalFields = [{
|
|
"field": "#id_land_ownership",
|
|
"showHide": ["#div_id_land_ownership_details"],
|
|
"condition": ["OTH"]
|
|
},
|
|
{
|
|
"field": "#id_location_context",
|
|
"showHide": ["#div_id_type_of_ecosystem"],
|
|
"condition": ["RUR"]
|
|
},
|
|
{
|
|
"field": "#id_power_technology",
|
|
"showHide": ["#div_id_power_technology_other"],
|
|
"condition": ["OT"]
|
|
},
|
|
{ // 2.1 - Power Generation
|
|
"field": "#id_sector_of_economy",
|
|
"showHide": ["#power_generation_questions"],
|
|
"condition": ["RN"]
|
|
},
|
|
{
|
|
"field": "#id_generation_technology",
|
|
"showHide": ["#div_id_biomass_detail"],
|
|
"condition": ["BIOG", "OTHB"]
|
|
},
|
|
{
|
|
"field": "#id_generation_technology",
|
|
"showHide": ["#div_id_generation_technology_other"],
|
|
"condition": ["OTHR"]
|
|
},
|
|
{ // 2.2 - Power Grids
|
|
"field": "#id_sector_of_economy",
|
|
"showHide": ["#power_grids_energy_storage_questions"],
|
|
"condition": ["PG"]
|
|
},
|
|
{ // 2.3 - Supply of minerals
|
|
"field": "#id_sector_of_economy",
|
|
"showHide": ["#mineral_commodity_questions"],
|
|
"condition": ["SM"]
|
|
},
|
|
{ // 3.1
|
|
"field": "#id_positive_or_negative",
|
|
"showHide": ["#positive_case_questions"],
|
|
"condition": ["P"]
|
|
},
|
|
{ // 3.2
|
|
"field": "#id_positive_or_negative",
|
|
"showHide": ["#negative_case_questions"],
|
|
"condition": ["N"]
|
|
}];
|
|
|
|
// Here we define the checkboxes that we need to use to
|
|
// conditionally toggle fields - they use slightly different
|
|
// logic as they rely on the 'checked' attribute rather than value.
|
|
var conditionalCheckboxes = [
|
|
{
|
|
"checkbox": "#id_affects_indigenous",
|
|
"showHide": "#div_id_affects_indigenous_detail"
|
|
},
|
|
{
|
|
"checkbox": "#id_shown_on_other_platforms",
|
|
"showHide": "#div_id_shown_on_other_platforms_detail"
|
|
},
|
|
{
|
|
"checkbox": "#id_negative_case_reasons_8",
|
|
"showHide": "#div_id_negative_case_reasons_other"
|
|
}
|
|
];
|
|
|
|
// Define a function that hides the field and then creates a listener to toggle the field.
|
|
// Takes a single conditionalField dictionary with (field, showHide and condition).
|
|
function addConditionalField(item) {
|
|
hideAll(item.showHide);
|
|
$(item.field).change(function(){
|
|
// Get the value of the option
|
|
var value = $(this).val();
|
|
|
|
// Check the value matches any of the conditions.
|
|
if (isInArray(value, item.condition)){
|
|
showAll(item.showHide);
|
|
} else {
|
|
hideAll(item.showHide);
|
|
}
|
|
});
|
|
}
|
|
|
|
// This function does the same as addConditionalField except it checks the status of
|
|
// checkboxes instead of fields.
|
|
function addConditionalCheckbox(item) {
|
|
$(item.showHide).hide();
|
|
$(item.checkbox).change(function(){
|
|
if ($(this).is(":checked")) {
|
|
$(item.showHide).show();
|
|
} else {
|
|
$(item.showHide).hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Helper functions
|
|
function show(tag) {
|
|
$(tag).show();
|
|
}
|
|
|
|
function hide(tag) {
|
|
$(tag).hide();
|
|
}
|
|
|
|
function showAll(tags) {
|
|
tags.forEach(show);
|
|
}
|
|
|
|
function hideAll(tags) {
|
|
tags.forEach(hide);
|
|
}
|
|
|
|
function isInArray(value, array) {
|
|
return array.indexOf(value) > -1;
|
|
}
|
|
|
|
$(document).ready(function(){
|
|
// Button classes for tab navigation
|
|
$('.btnNext').click(function(){
|
|
$('.nav-tabs > .active').next('li').find('a').trigger('click');
|
|
});
|
|
|
|
$('.btnPrevious').click(function(){
|
|
$('.nav-tabs > .active').prev('li').find('a').trigger('click');
|
|
});
|
|
|
|
$('form').attr('novalidate', 'novalidate');
|
|
|
|
conditionalFields.forEach(addConditionalField);
|
|
conditionalCheckboxes.forEach(addConditionalCheckbox);
|
|
|
|
conditionalFields.forEach(function(item){
|
|
$(item.field).change();
|
|
});
|
|
|
|
conditionalCheckboxes.forEach(function(item){
|
|
$(item.checkbox).change();
|
|
});
|
|
});
|
|
|
|
</script>
|
|
|
|
<script src="{% static 'map/plugins/FormSaver.js' %}"></script>
|
|
<script src="{% static 'map/plugins/jquery.dirtyforms.min.js' %}"></script>
|
|
<script>
|
|
"use strict";
|
|
|
|
//
|
|
// UI ELEMENTS
|
|
//
|
|
|
|
class SaveButton {
|
|
constructor(div) {
|
|
this.root = div
|
|
this.root.className = "savebutton"
|
|
this.root.style.display = "block"
|
|
|
|
this.element = {
|
|
root: this.root,
|
|
button: this.root.querySelector('.savebutton--button'),
|
|
icon: this.root.querySelector('.savebutton--icon'),
|
|
details: this.root.querySelector('.savebutton--details')
|
|
}
|
|
|
|
this.switchStateInitial()
|
|
}
|
|
|
|
changeState(data) {
|
|
var data = data || {}
|
|
this.element.button.innerText = data.buttonText || "Save"
|
|
this.element.button.disabled = data.buttonClickable === false ? true : false
|
|
this.element.icon.className = 'savebutton--icon ' + (data.iconClasses || "")
|
|
this.element.details.innerText = data.detailsText || ""
|
|
}
|
|
|
|
switchStateInitial() {
|
|
this.changeState({
|
|
buttonClickable: false
|
|
})
|
|
}
|
|
|
|
switchStateUnsaved() {
|
|
this.changeState({
|
|
detailsText: "{% trans "You have unsaved changes. Click here to save a draft, which you can access next time you are here." %}"
|
|
})
|
|
}
|
|
|
|
switchStateSaving() {
|
|
this.changeState({
|
|
buttonText: "{% trans "Saving..." %}",
|
|
iconClasses: "fa fa-spinner fa-spin"
|
|
})
|
|
}
|
|
|
|
switchStateSaveSuccess() {
|
|
this.changeState({
|
|
buttonText: "{% trans "Saved" %}",
|
|
detailsText: "{% trans "Saved successfully." %}",
|
|
iconClasses: "fa fa-check",
|
|
buttonClickable: false
|
|
})
|
|
}
|
|
|
|
switchStateSaveFailed(reason = "") {
|
|
this.changeState({
|
|
detailsText: "{% trans "Save failed! " %}" + reason,
|
|
iconClasses: "fa fa-exclamation-triangle savebutton--icon-failed"
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
class DraftPrompt {
|
|
constructor(opts) {
|
|
this.restoreDraft = opts.restoreFn
|
|
this.deleteDraft = opts.deleteFn
|
|
|
|
this.root = opts.root
|
|
this.element = {
|
|
root: this.root,
|
|
restore: this.root.querySelector('.draftprompt--restore'),
|
|
delete: this.root.querySelector('.draftprompt--delete'),
|
|
details: this.root.querySelector('.draftprompt--details')
|
|
}
|
|
|
|
// Restore should restore, then hide the prompt
|
|
this.element.restore.addEventListener('click', () => {
|
|
this.restoreDraft()
|
|
this.switchStateHidden()
|
|
})
|
|
|
|
// Delete button will delete the draft
|
|
this.element.delete.addEventListener('click', () => {
|
|
if (window.confirm("{% trans 'Are you sure you want to delete your draft?' %}")) {
|
|
this.switchStateDeleting()
|
|
this.deleteDraft().then(ok => ok ?
|
|
this.switchStateHidden() :
|
|
this.switchStateDeleteFailed()
|
|
)
|
|
}
|
|
})
|
|
|
|
this.switchStateShown()
|
|
}
|
|
|
|
changeState(data) {
|
|
var data = data || {}
|
|
this.element.root.style.display = data.visible ? "block" : "none"
|
|
this.element.details.innerHTML = data.detailsText || ""
|
|
}
|
|
|
|
switchStateHidden() {
|
|
this.changeState({
|
|
visible: false
|
|
})
|
|
}
|
|
|
|
switchStateShown() {
|
|
this.changeState({
|
|
visible: true
|
|
})
|
|
}
|
|
|
|
switchStateDeleting() {
|
|
this.changeState({
|
|
visible: true,
|
|
details: '<i classs="fa fa-spinner fa-spin"></i>'
|
|
})
|
|
}
|
|
|
|
switchStateDeleteFailed() {
|
|
this.changeState({
|
|
details: '{% trans "Delete failed!" %}'
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// API UTILITIES
|
|
//
|
|
|
|
function apiGetDraft() {
|
|
return fetch('/en-gb/case-study/draft', {
|
|
method: 'GET',
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
|
}
|
|
})
|
|
}
|
|
|
|
function apiPutDraft(formSaver) {
|
|
if (!formSaver) {
|
|
throw new Error("apiPutDraft: parameter not provided")
|
|
}
|
|
|
|
return fetch('/en-gb/case-study/draft', {
|
|
method: 'PUT',
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({ version: 1, data: formSaver.serialise() })
|
|
})
|
|
}
|
|
|
|
function apiDeleteDraft() {
|
|
return fetch('/en-gb/case-study/draft', {
|
|
method: 'DELETE',
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"X-CSRFToken": jQuery("[name=csrfmiddlewaretoken]").val(),
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
//
|
|
// GNARLY BITS TYING API & UI STUFF TOGETHER
|
|
//
|
|
|
|
function showDraftPrompt(formSaver, serialisedForm) {
|
|
let prompt = new DraftPrompt({
|
|
root: document.querySelector('.draftprompt'),
|
|
restoreFn: () => formSaver.deserialise(serialisedForm),
|
|
deleteFn: () => apiDeleteDraft().then(response => response.ok)
|
|
})
|
|
}
|
|
|
|
function initDrafts() {
|
|
const formSaver = new FormSaver({
|
|
formId: 'case-study-form',
|
|
except: [ 'csrfmiddlewaretoken' ]
|
|
})
|
|
|
|
// Use whether the form has errors as a proxy for whether the server has
|
|
// returned us data in the form. In this case, don't show a draft.
|
|
const formHasErrors = document.getElementById('form_errors').value === 'true' ? true : false
|
|
if (!formHasErrors) {
|
|
// Get the previous draft, if any
|
|
apiGetDraft()
|
|
.then(response => response.json())
|
|
.then(json => {
|
|
// Handle the case where we didn't get a response
|
|
if (!json) return
|
|
|
|
if (json.version !== 1) {
|
|
throw new Error("Bad JSON response version")
|
|
}
|
|
|
|
showDraftPrompt(formSaver, json.data.form)
|
|
}).catch(err => {
|
|
console.error(err);
|
|
})
|
|
}
|
|
|
|
// Init the button controller
|
|
var button = new SaveButton(document.querySelector('.savebutton'))
|
|
|
|
// Dirty forms set up
|
|
$('#case-study-form').dirtyForms();
|
|
$('#case-study-form').on('dirty.dirtyforms', ev => {
|
|
button.switchStateUnsaved()
|
|
});
|
|
|
|
// Save button
|
|
button.element.button.addEventListener('click', evt => {
|
|
button.switchStateSaving()
|
|
|
|
apiPutDraft(formSaver).then(response => {
|
|
if (response.ok) {
|
|
button.switchStateSaveSuccess();
|
|
$('#case-study-form').dirtyForms('setClean');
|
|
document.querySelector('.draftprompt').style.display = "none"
|
|
} else {
|
|
button.switchStateSaveFailed();
|
|
}
|
|
}).catch(err => {
|
|
console.error(err);
|
|
button.switchStateSaveFailed();
|
|
})
|
|
})
|
|
}
|
|
|
|
// https://github.com/snikch/jquery.dirtyforms
|
|
|
|
initDrafts()
|
|
</script>
|
|
|
|
{% endblock %}
|