Add points of interest
Do a bunch of refactoring in the process. Current display on homepage is a little shonky but that can be improved when there is something using it.
This commit is contained in:
parent
f2823445a4
commit
acfbb513de
@ -11,7 +11,7 @@ from leaflet.forms.widgets import LeafletWidget
|
|||||||
|
|
||||||
from apps.files.models import File, ImageFile
|
from apps.files.models import File, ImageFile
|
||||||
|
|
||||||
from .models import CaseStudy, SpatialRefSys
|
from .models import CaseStudy, SpatialRefSys, PointOfInterest
|
||||||
from .widgets import JSONFileListWidget
|
from .widgets import JSONFileListWidget
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +19,40 @@ class MinimumZoomWidget(LeafletWidget):
|
|||||||
geometry_field_class = 'MinimumZoomField'
|
geometry_field_class = 'MinimumZoomField'
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterest(forms.models.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PointOfInterest, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper(self)
|
||||||
|
self.helper.form_id = 'case-study-form'
|
||||||
|
self.helper.form_class = 'form-horizontal'
|
||||||
|
self.helper.form_method = 'post'
|
||||||
|
self.helper.form_action = 'add'
|
||||||
|
self.helper.label_class = 'col-lg-2'
|
||||||
|
self.helper.field_class = 'col-lg-10'
|
||||||
|
self.helper.include_media = False
|
||||||
|
|
||||||
|
self.helper.form_action = reverse('point-of-interest-form')
|
||||||
|
self.helper.add_input(Submit('submit', _('Submit'), css_class='btn-success center-block'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PointOfInterest
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'location': MinimumZoomWidget(attrs={
|
||||||
|
'settings_overrides': {
|
||||||
|
'SCALE': False
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'title',
|
||||||
|
'location',
|
||||||
|
'synopsis',
|
||||||
|
'link',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class BaseCaseStudyForm(forms.models.ModelForm):
|
class BaseCaseStudyForm(forms.models.ModelForm):
|
||||||
"""Base form class for the CaseStudy model."""
|
"""Base form class for the CaseStudy model."""
|
||||||
|
|
||||||
|
@ -1015,6 +1015,14 @@ class CaseStudy(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestQuerySet(models.QuerySet):
|
||||||
|
def approved(self):
|
||||||
|
return self.filter(
|
||||||
|
approved=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PointOfInterest(models.Model):
|
class PointOfInterest(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = 'points of interest'
|
verbose_name_plural = 'points of interest'
|
||||||
@ -1022,6 +1030,8 @@ class PointOfInterest(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
objects = PointOfInterestQuerySet.as_manager()
|
||||||
|
|
||||||
author = models.ForeignKey(
|
author = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
models.SET_NULL,
|
models.SET_NULL,
|
||||||
|
@ -4,71 +4,6 @@
|
|||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% leaflet_css %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_title %}{% trans "Submit a Case Study" %} - {{ block.super }}{% endblock %}
|
{% block page_title %}{% trans "Submit a Case Study" %} - {{ block.super }}{% endblock %}
|
||||||
@ -98,80 +33,10 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ form.media }}
|
{{ form.media }}
|
||||||
{% leaflet_js %}
|
{% leaflet_js %}
|
||||||
<script>
|
<script src="{% static 'js/map_minzoom.js' %}"></script>
|
||||||
ZoomHelpText = L.Control.extend({
|
<script src="{% static 'map/plugins/FormSaver.js' %}"></script>
|
||||||
options: {
|
<script src="{% static 'map/plugins/jquery.dirtyforms.min.js' %}"></script>
|
||||||
position: 'bottomleft'
|
<script src="{% static 'js/form_casestudy_drafts.js' %}"></script>
|
||||||
},
|
|
||||||
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. -->
|
<!-- Conditional logic for hiding and un-hiding fields. -->
|
||||||
<script>
|
<script>
|
||||||
@ -318,263 +183,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<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>
|
|
||||||
<script>
|
|
||||||
<!-- Jump to error -->
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
initDrafts()
|
||||||
|
|
||||||
|
// Jump to error
|
||||||
if ($('.has-error').length) {
|
if ($('.has-error').length) {
|
||||||
location.href = '#' + $('.has-error:first').attr('id');
|
location.href = '#' + $('.has-error:first').attr('id');
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
32
apps/map/templates/map/form-poi.html
Normal file
32
apps/map/templates/map/form-poi.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load compress crispy_forms_tags i18n leaflet_tags static %}
|
||||||
|
|
||||||
|
{% block page_title %}{% trans "Add a Point of Interest" %} - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
|
||||||
|
<div class="content--narrow">
|
||||||
|
|
||||||
|
<h1>{% trans "Add a Point of Interest" %}</h1>
|
||||||
|
<p class="subheading">{% trans "Points of interest are a way of adding information about or linked to a particular place. They could be a marker for further research, or just a link to information elsewhere." %}</p>
|
||||||
|
|
||||||
|
{% crispy form %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% leaflet_css %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ form.media }}
|
||||||
|
{% leaflet_js %}
|
||||||
|
<script src="{% static 'js/map_minzoom.js' %}"></script>
|
||||||
|
{% endblock %}
|
@ -7,17 +7,6 @@
|
|||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<style>
|
<style>
|
||||||
.form-selector {
|
|
||||||
max-width: 50em;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subhead {
|
|
||||||
font-size: 120%;
|
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -56,10 +45,10 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
||||||
<div class="form-selector">
|
<div class="content--narrow">
|
||||||
|
|
||||||
<h1>{% trans "Create a new entry" %}</h1>
|
<h1>{% trans "Create a new entry" %}</h1>
|
||||||
<p class="subhead">{% trans "What kind of entry do you want to create?" %}</p>
|
<p class="subheading">{% trans "What kind of entry do you want to create?" %}</p>
|
||||||
|
|
||||||
<a class="entry" href="{% url 'long-form' %}">
|
<a class="entry" href="{% url 'long-form' %}">
|
||||||
<i class="entry-icon ojuso-green fa fa-file-text-o"></i>
|
<i class="entry-icon ojuso-green fa fa-file-text-o"></i>
|
||||||
@ -80,8 +69,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<!--
|
|
||||||
<a class="entry" href="/">
|
<a class="entry" href="{% url 'point-of-interest-form' %}">
|
||||||
<i class="entry-icon ojuso-blue fa fa-thumb-tack"></i>
|
<i class="entry-icon ojuso-blue fa fa-thumb-tack"></i>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="entry-title">{% trans "Point of interest" %}</h3>
|
<h3 class="entry-title">{% trans "Point of interest" %}</h3>
|
||||||
@ -90,7 +79,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,59 +32,10 @@ html, body, #main {
|
|||||||
.popup-labels .label {
|
.popup-labels .label {
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hello {
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
overflow: auto;
|
|
||||||
z-index: 1000;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
background-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHZpZXdCb3g9IjEwMCA1IDIwMCAyNTUiPg0KICA8cGF0aCBkPSJNMjM4IDEyN2g1M2wtMjYtMTUtMjcgMTUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yOTQgMTIybC0yOS01MS0yOSA1MSAyOS0xNyAyOSAxNyIgZmlsbD0icmdiYSgxMDIsIDE5OCwgODcsIDAuNSkiLz4NCiAgPHBhdGggZD0iTTIyOCAxMjRsMzItNTZoLTY0bDMyIDU2IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xODYgNjNMMTUzIDdsLTMyIDU2aDY1bTExMSAxOTNsLTMyLTU2LTMyIDU2aDY0IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMjIzIDEyN2wtMzItNTYtMzMgNTZoNjUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yNjAgMTkxbC0zMi01Ni0zMiA1Nmg2NCIgZmlsbD0icmdiYSgyMjEsIDY4LCA1OCwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTUzIDEyNGwzMy01NmgtNjVsMzIgNTYiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjggMjUzbDMyLTU2aC02NGwzMiA1NiIgZmlsbD0icmdiYSgyNTIsIDIxMSwgMjksIDAuNSkiLz4NCiAgDQogIA0KICA8cGF0aCBkPSJNMTg2IDE5MWwtMzMtNTUtMzIgNTVoNjUiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIA0KICANCiAgPHBhdGggZD0iTTExOSAxODRsMjctNDYtMjcgMTV2MzEiIGZpbGw9InJnYmEoMTAyLCAxOTgsIDg3LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xOTMgMTg0bDI3LTQ2LTI3IDE1djMxIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjE3IDEzM2gtNTlsMzAgNTF2LTM0bDI5LTE3IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMTg4IDIwNWwtMjcgNDYgMjctMTZ2LTMwIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTY0IDI1Nmg1OWwtMzAtNTF2MzRsLTI5IDE3IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjUgMTJsLTI2IDQ2IDI2LTE2VjEyIiBmaWxsPSJyZ2JhKDI1MiwgMjExLCAyOSwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjAxIDYzaDU5bC0yOS01MXYzNGwtMzAgMTciIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQo8L3N2Zz4=');
|
|
||||||
background-position: center;
|
|
||||||
background-blend-mode: luminosity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hello--container {
|
|
||||||
max-width: 40em;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hello--text {
|
|
||||||
background-color: white;
|
|
||||||
margin-top: 5em;
|
|
||||||
margin-left: 1em;
|
|
||||||
margin-right: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
padding: 1em;
|
|
||||||
z-index: 1100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Top margin on small screns should be same as the rest */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hello--text {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hello--logo {
|
|
||||||
width: 200px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hello--logo, .hello--hide {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% trans "Ojuso Platform Map" %}{% endblock %}
|
|
||||||
|
|
||||||
{% block inner_content %}
|
{% block inner_content %}
|
||||||
<div id="main"></div>
|
<div id="main"></div>
|
||||||
<div id="modals"></div>
|
<div id="modals"></div>
|
||||||
@ -143,7 +94,7 @@ function getLabelClass(pos_or_neg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function popup(feature, layer) {
|
function caseStudyPopup(feature, layer) {
|
||||||
var str = '';
|
var str = '';
|
||||||
if(feature.properties.images.length > 0) {
|
if(feature.properties.images.length > 0) {
|
||||||
str = "<img src='"+feature.properties.images[0].file+"' width='100%'>";
|
str = "<img src='"+feature.properties.images[0].file+"' width='100%'>";
|
||||||
@ -157,8 +108,21 @@ function popup(feature, layer) {
|
|||||||
"<span class='label label-"+getLabelClass(feature.properties.positive_or_negative)+"'>"+ feature.properties.positive_or_negative_display+"</span>"+
|
"<span class='label label-"+getLabelClass(feature.properties.positive_or_negative)+"'>"+ feature.properties.positive_or_negative_display+"</span>"+
|
||||||
"</div>"+
|
"</div>"+
|
||||||
"<a class='btn btn-sm btn-primary' href='case-study/"+feature.properties.slug+"'>{% trans "View full case study" %}</a>")
|
"<a class='btn btn-sm btn-primary' href='case-study/"+feature.properties.slug+"'>{% trans "View full case study" %}</a>")
|
||||||
|
|
||||||
layer.bindPopup(str);
|
layer.bindPopup(str);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function poiPopup(feature, layer) {
|
||||||
|
str = (
|
||||||
|
"<div class='popup-head'>"+
|
||||||
|
"<h5>"+feature.properties.title+"</h5>" +
|
||||||
|
"</div>"+
|
||||||
|
"<p>"+feature.properties.synopsis+"</p>"+
|
||||||
|
"<p><a href='"+feature.properties.link+"'>Link</a></p>"
|
||||||
|
)
|
||||||
|
|
||||||
|
layer.bindPopup(str);
|
||||||
|
}
|
||||||
|
|
||||||
// This is called when the map is initialized
|
// This is called when the map is initialized
|
||||||
function main_app_init(map, options) {
|
function main_app_init(map, options) {
|
||||||
@ -167,7 +131,26 @@ function main_app_init(map, options) {
|
|||||||
// Pull data as GeoJSON and add to map with a modal
|
// Pull data as GeoJSON and add to map with a modal
|
||||||
$.getJSON('/api/case-studies/', function(data) {
|
$.getJSON('/api/case-studies/', function(data) {
|
||||||
L.geoJson(data, {
|
L.geoJson(data, {
|
||||||
onEachFeature: popup
|
onEachFeature: caseStudyPopup
|
||||||
|
}).addTo(map)
|
||||||
|
});
|
||||||
|
|
||||||
|
var geojsonMarkerOptions = {
|
||||||
|
radius: 4,
|
||||||
|
fillColor: "#ff7800",
|
||||||
|
color: "#000",
|
||||||
|
weight: 1,
|
||||||
|
opacity: 1,
|
||||||
|
fillOpacity: 0.8
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pull data as GeoJSON and add to map with a modal
|
||||||
|
$.getJSON('/api/points-of-interest/', function(data) {
|
||||||
|
L.geoJson(data, {
|
||||||
|
pointToLayer: function (feature, latlng) {
|
||||||
|
return L.circleMarker(latlng, geojsonMarkerOptions);
|
||||||
|
},
|
||||||
|
onEachFeature: poiPopup
|
||||||
}).addTo(map)
|
}).addTo(map)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
from django.views.i18n import JavaScriptCatalog
|
||||||
from djgeojson.views import GeoJSONLayerView
|
from djgeojson.views import GeoJSONLayerView
|
||||||
|
|
||||||
from .models import CaseStudy
|
from .models import CaseStudy
|
||||||
@ -11,12 +12,13 @@ urlpatterns = [
|
|||||||
url(r'^case-study/create/?$', views.Create.as_view(), name="create"),
|
url(r'^case-study/create/?$', views.Create.as_view(), name="create"),
|
||||||
url(r'^case-study/create/short/?$', views.ShortForm.as_view(), name='short-form'),
|
url(r'^case-study/create/short/?$', views.ShortForm.as_view(), name='short-form'),
|
||||||
url(r'^case-study/create/long/?$', views.LongForm.as_view(), name='long-form'),
|
url(r'^case-study/create/long/?$', views.LongForm.as_view(), name='long-form'),
|
||||||
|
url(r'^case-study/create/poi/?$', views.PointOfInterest.as_view(), name='point-of-interest-form'),
|
||||||
url(r'^case-study/create/success/?$', views.FormSuccess.as_view(), name='form-success'),
|
url(r'^case-study/create/success/?$', views.FormSuccess.as_view(), name='form-success'),
|
||||||
url(r'^case-study/draft/?$', views.Drafts.as_view(), name='drafts'),
|
url(r'^case-study/draft/?$', views.Drafts.as_view(), name='drafts'),
|
||||||
url(r'^case-study/(?P<slug>[-\w]+)/?$', views.CaseStudyDetail.as_view(), name='detail'),
|
url(r'^case-study/(?P<slug>[-\w]+)/?$', views.CaseStudyDetail.as_view(), name='detail'),
|
||||||
url(r'^map/?$', views.Map.as_view(), name='map'),
|
url(r'^map/?$', views.Map.as_view(), name='map'),
|
||||||
|
|
||||||
# API
|
# API
|
||||||
url(r'^data.geojson$', GeoJSONLayerView.as_view(model=CaseStudy, geometry_field='location'), name='data'),
|
url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalogue'),
|
||||||
url(r'^srs-autocomplete/$', views.SpatialRefSysAutocomplete.as_view(), name='srs-autocomplete'),
|
url(r'^srs-autocomplete/$', views.SpatialRefSysAutocomplete.as_view(), name='srs-autocomplete'),
|
||||||
]
|
]
|
||||||
|
@ -15,8 +15,8 @@ from dal import autocomplete
|
|||||||
|
|
||||||
from apps.files.models import File
|
from apps.files.models import File
|
||||||
|
|
||||||
from .models import CaseStudy, CaseStudyDraft, SpatialRefSys
|
from .models import CaseStudy, CaseStudyDraft, SpatialRefSys, PointOfInterest
|
||||||
from .forms import ShortCaseStudyForm, LongCaseStudyForm
|
from .forms import ShortCaseStudyForm, LongCaseStudyForm, PointOfInterest
|
||||||
|
|
||||||
|
|
||||||
NOTIFY_MESSAGE = """
|
NOTIFY_MESSAGE = """
|
||||||
@ -39,6 +39,14 @@ class Create(LoginRequiredMixin, TemplateView):
|
|||||||
template_name = "map/form-selector.html"
|
template_name = "map/form-selector.html"
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterest(LoginRequiredMixin, CreateView):
|
||||||
|
"""View for base case study form."""
|
||||||
|
template_name = 'map/form-poi.html'
|
||||||
|
success_url = '/case-study/create/success/'
|
||||||
|
model = PointOfInterest
|
||||||
|
form_class = PointOfInterest
|
||||||
|
|
||||||
|
|
||||||
class BaseForm(LoginRequiredMixin, CreateView):
|
class BaseForm(LoginRequiredMixin, CreateView):
|
||||||
"""View for base case study form."""
|
"""View for base case study form."""
|
||||||
template_name = 'map/form-case_study.html'
|
template_name = 'map/form-case_study.html'
|
||||||
|
@ -13,6 +13,36 @@
|
|||||||
background: red;
|
background: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* UTILITY CLASSES
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Colours */
|
||||||
|
.ojuso-red { color: #ea4639; }
|
||||||
|
.ojuso-green { color: #4ac95d; }
|
||||||
|
.ojuso-yellow { color: #f9db3c; }
|
||||||
|
.ojuso-blue { color: #008ad5; }
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* LAYOUT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Narrow content area wrapper */
|
||||||
|
.content--narrow {
|
||||||
|
max-width: 50em;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subheading, designed to sit on a paragraph after h1 */
|
||||||
|
.subheading {
|
||||||
|
font-size: 120%;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Page footer */
|
/* Page footer */
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
@ -42,8 +72,128 @@
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colour utility classes */
|
|
||||||
.ojuso-red { color: #ea4639; }
|
|
||||||
.ojuso-green { color: #4ac95d; }
|
/*
|
||||||
.ojuso-yellow { color: #f9db3c; }
|
* FORMS
|
||||||
.ojuso-blue { color: #008ad5; }
|
*/
|
||||||
|
|
||||||
|
/* Map widget - Minimum zoom help text */
|
||||||
|
.minzoomhelptext {
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: rgba(255, 255, 255, 80%);
|
||||||
|
padding: 2px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Case study draft functionality */
|
||||||
|
.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; }
|
||||||
|
|
||||||
|
|
||||||
|
/* File upload widget styles */
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HOME PAGE
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* This is the popup that you see when you first arrive on the site */
|
||||||
|
.hello {
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
background-image: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHZpZXdCb3g9IjEwMCA1IDIwMCAyNTUiPg0KICA8cGF0aCBkPSJNMjM4IDEyN2g1M2wtMjYtMTUtMjcgMTUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yOTQgMTIybC0yOS01MS0yOSA1MSAyOS0xNyAyOSAxNyIgZmlsbD0icmdiYSgxMDIsIDE5OCwgODcsIDAuNSkiLz4NCiAgPHBhdGggZD0iTTIyOCAxMjRsMzItNTZoLTY0bDMyIDU2IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xODYgNjNMMTUzIDdsLTMyIDU2aDY1bTExMSAxOTNsLTMyLTU2LTMyIDU2aDY0IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMjIzIDEyN2wtMzItNTYtMzMgNTZoNjUiIGZpbGw9InJnYmEoMjUyLCAyMTEsIDI5LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yNjAgMTkxbC0zMi01Ni0zMiA1Nmg2NCIgZmlsbD0icmdiYSgyMjEsIDY4LCA1OCwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTUzIDEyNGwzMy01NmgtNjVsMzIgNTYiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjggMjUzbDMyLTU2aC02NGwzMiA1NiIgZmlsbD0icmdiYSgyNTIsIDIxMSwgMjksIDAuNSkiLz4NCiAgDQogIA0KICA8cGF0aCBkPSJNMTg2IDE5MWwtMzMtNTUtMzIgNTVoNjUiIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQogIA0KICANCiAgPHBhdGggZD0iTTExOSAxODRsMjctNDYtMjcgMTV2MzEiIGZpbGw9InJnYmEoMTAyLCAxOTgsIDg3LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0xOTMgMTg0bDI3LTQ2LTI3IDE1djMxIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjE3IDEzM2gtNTlsMzAgNTF2LTM0bDI5LTE3IiBmaWxsPSJyZ2JhKDEwMiwgMTk4LCA4NywgMC41KSIvPg0KICA8cGF0aCBkPSJNMTg4IDIwNWwtMjcgNDYgMjctMTZ2LTMwIiBmaWxsPSJyZ2JhKDM3LCAxNDAsIDIxMiwgMC41KSIvPg0KICA8cGF0aCBkPSJNMTY0IDI1Nmg1OWwtMzAtNTF2MzRsLTI5IDE3IiBmaWxsPSJyZ2JhKDIyMSwgNjgsIDU4LCAwLjUpIi8+DQogIDxwYXRoIGQ9Ik0yMjUgMTJsLTI2IDQ2IDI2LTE2VjEyIiBmaWxsPSJyZ2JhKDI1MiwgMjExLCAyOSwgMC41KSIvPg0KICA8cGF0aCBkPSJNMjAxIDYzaDU5bC0yOS01MXYzNGwtMzAgMTciIGZpbGw9InJnYmEoMzcsIDE0MCwgMjEyLCAwLjUpIi8+DQo8L3N2Zz4=');
|
||||||
|
background-position: center;
|
||||||
|
background-blend-mode: luminosity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello--container {
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello--text {
|
||||||
|
background-color: white;
|
||||||
|
margin-top: 5em;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top margin on small screns should be same as the rest */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hello--text {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello--logo {
|
||||||
|
width: 200px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hello--logo, .hello--hide {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
243
assets/js/form_casestudy_drafts.js
Normal file
243
assets/js/form_casestudy_drafts.js
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
"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: django.gettext("You have unsaved changes. Click here to save a draft, which you can access next time you are here.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchStateSaving() {
|
||||||
|
this.changeState({
|
||||||
|
buttonText: django.gettext("Saving..."),
|
||||||
|
iconClasses: "fa fa-spinner fa-spin"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchStateSaveSuccess() {
|
||||||
|
this.changeState({
|
||||||
|
buttonText: django.gettext("Saved"),
|
||||||
|
detailsText: django.gettext("Saved successfully."),
|
||||||
|
iconClasses: "fa fa-check",
|
||||||
|
buttonClickable: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
switchStateSaveFailed(reason = "") {
|
||||||
|
this.changeState({
|
||||||
|
detailsText: django.gettext("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(django.gettext('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: django.gettext("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
|
72
assets/js/map_minzoom.js
Normal file
72
assets/js/map_minzoom.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
ZoomHelpText = L.Control.extend({
|
||||||
|
options: {
|
||||||
|
position: 'bottomleft'
|
||||||
|
},
|
||||||
|
onAdd: function (map) {
|
||||||
|
return L.DomUtil.create('div', 'minzoomhelptext')
|
||||||
|
},
|
||||||
|
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(
|
||||||
|
django.gettext("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(
|
||||||
|
django.gettext("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)
|
||||||
|
},
|
||||||
|
})
|
@ -127,7 +127,8 @@
|
|||||||
{# CDN Javascript #}
|
{# CDN Javascript #}
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha256-JmvOoLtYsmqlsWxa7mDSLMwa6dZ9rrIdtrrVYRnDRH0=" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js" integrity="sha256-JmvOoLtYsmqlsWxa7mDSLMwa6dZ9rrIdtrrVYRnDRH0=" crossorigin="anonymous"></script>
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||||
<script src="{% static "map/language.js" %}"></script>
|
<script src="{% url 'javascript-catalogue' %}"></script>
|
||||||
|
<script src="{% static 'map/language.js' %}"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -23,7 +23,7 @@ from rest_framework import routers, serializers, viewsets
|
|||||||
from rest_framework_gis import serializers as gis_serializers
|
from rest_framework_gis import serializers as gis_serializers
|
||||||
|
|
||||||
from apps.files.models import File
|
from apps.files.models import File
|
||||||
from apps.map.models import CaseStudy
|
from apps.map.models import CaseStudy, PointOfInterest
|
||||||
|
|
||||||
from .views import LanguageDropdownView
|
from .views import LanguageDropdownView
|
||||||
|
|
||||||
@ -71,9 +71,28 @@ class CaseStudyViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = CaseStudySerializer
|
serializer_class = CaseStudySerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestSerializer(gis_serializers.GeoFeatureModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PointOfInterest
|
||||||
|
geo_field = "location"
|
||||||
|
fields = (
|
||||||
|
'title',
|
||||||
|
'synopsis',
|
||||||
|
'link',
|
||||||
|
'slug'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PointOfInterestViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PointOfInterest.objects.approved()
|
||||||
|
serializer_class = PointOfInterestSerializer
|
||||||
|
|
||||||
|
|
||||||
apirouter = routers.DefaultRouter()
|
apirouter = routers.DefaultRouter()
|
||||||
#apirouter.register(r'users', UserViewSet)
|
#apirouter.register(r'users', UserViewSet)
|
||||||
apirouter.register(r'case-studies', CaseStudyViewSet)
|
apirouter.register(r'case-studies', CaseStudyViewSet)
|
||||||
|
apirouter.register(r'points-of-interest', PointOfInterestViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'api/', include(apirouter.urls)),
|
url(r'api/', include(apirouter.urls)),
|
||||||
|
Loading…
Reference in New Issue
Block a user