maps: add option to include a search button (#88131)
gitea/combo/pipeline/head This commit looks good
Details
gitea/combo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
d964be219e
commit
7b66dca2ba
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.16 on 2024-03-13 19:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('maps', '0022_increase_extra_css_class'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='map',
|
||||||
|
name='include_search_button',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Include address search button'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -433,6 +433,7 @@ class Map(CellBase):
|
||||||
min_zoom = models.CharField(_('Minimal zoom level'), max_length=2, choices=ZOOM_LEVELS, default='0')
|
min_zoom = models.CharField(_('Minimal zoom level'), max_length=2, choices=ZOOM_LEVELS, default='0')
|
||||||
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, choices=ZOOM_LEVELS, default=19)
|
max_zoom = models.CharField(_('Maximal zoom level'), max_length=2, choices=ZOOM_LEVELS, default=19)
|
||||||
group_markers = models.BooleanField(_('Group markers in clusters'), default=False)
|
group_markers = models.BooleanField(_('Group markers in clusters'), default=False)
|
||||||
|
include_search_button = models.BooleanField(_('Include address search button'), default=False)
|
||||||
marker_behaviour_onclick = models.CharField(
|
marker_behaviour_onclick = models.CharField(
|
||||||
_('Marker behaviour on click'), max_length=32, default='none', choices=MARKER_BEHAVIOUR_ONCLICK
|
_('Marker behaviour on click'), max_length=32, default='none', choices=MARKER_BEHAVIOUR_ONCLICK
|
||||||
)
|
)
|
||||||
|
@ -449,6 +450,7 @@ class Map(CellBase):
|
||||||
'/jsi18n',
|
'/jsi18n',
|
||||||
'xstatic/leaflet.js',
|
'xstatic/leaflet.js',
|
||||||
'js/leaflet-gps.js',
|
'js/leaflet-gps.js',
|
||||||
|
'js/leaflet-search.js',
|
||||||
'js/combo.map.js',
|
'js/combo.map.js',
|
||||||
'xstatic/leaflet.markercluster.js',
|
'xstatic/leaflet.markercluster.js',
|
||||||
'xstatic/leaflet-gesture-handling.min.js',
|
'xstatic/leaflet-gesture-handling.min.js',
|
||||||
|
@ -464,6 +466,7 @@ class Map(CellBase):
|
||||||
fields = (
|
fields = (
|
||||||
'initial_state',
|
'initial_state',
|
||||||
'group_markers',
|
'group_markers',
|
||||||
|
'include_search_button',
|
||||||
'marker_behaviour_onclick',
|
'marker_behaviour_onclick',
|
||||||
)
|
)
|
||||||
return forms.models.modelform_factory(self.__class__, fields=fields)
|
return forms.models.modelform_factory(self.__class__, fields=fields)
|
||||||
|
@ -560,6 +563,7 @@ class Map(CellBase):
|
||||||
ctx['tiles_layers'] = self.get_tiles_layers()
|
ctx['tiles_layers'] = self.get_tiles_layers()
|
||||||
ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS
|
ctx['max_bounds'] = settings.COMBO_MAP_MAX_BOUNDS
|
||||||
ctx['group_markers'] = self.group_markers
|
ctx['group_markers'] = self.group_markers
|
||||||
|
ctx['include_search_button'] = self.include_search_button
|
||||||
ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick
|
ctx['marker_behaviour_onclick'] = self.marker_behaviour_onclick
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
|
@ -281,3 +281,61 @@ ul#id_icon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-top.leaflet-right {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-search {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
&.leaflet-control {
|
||||||
|
pointer-events: none;
|
||||||
|
&.open {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--control {
|
||||||
|
width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open &--control {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
&--input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--result-list {
|
||||||
|
padding-right: 0.7em;
|
||||||
|
background: white;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--result-item {
|
||||||
|
text-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: 1em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover, &.selected {
|
||||||
|
color: white;
|
||||||
|
background-color: #5897fb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -293,6 +293,17 @@ $(function() {
|
||||||
tooltipTitle: gettext('Display my position')});
|
tooltipTitle: gettext('Display my position')});
|
||||||
map.addControl(gps_control);
|
map.addControl(gps_control);
|
||||||
}
|
}
|
||||||
|
if (L.Control.Search && $map_widget.data('search-url')) {
|
||||||
|
var search_control = new L.Control.Search({
|
||||||
|
labels: {
|
||||||
|
hint: gettext('Search address'),
|
||||||
|
error: gettext('An error occured while fetching results'),
|
||||||
|
searching: gettext('Searching...'),
|
||||||
|
},
|
||||||
|
searchUrl: $map_widget.data('search-url')
|
||||||
|
});
|
||||||
|
map.addControl(search_control);
|
||||||
|
}
|
||||||
|
|
||||||
$map_widget[0].leaflet_map = map;
|
$map_widget[0].leaflet_map = map;
|
||||||
$(cell).removeClass('empty-cell');
|
$(cell).removeClass('empty-cell');
|
||||||
|
|
|
@ -0,0 +1,269 @@
|
||||||
|
/* global L, $ */
|
||||||
|
class SearchControl extends L.Control {
|
||||||
|
options = {
|
||||||
|
labels: {
|
||||||
|
hint: 'Search adresses',
|
||||||
|
error: 'An error occured while fetching results',
|
||||||
|
searching: 'Searching...'
|
||||||
|
},
|
||||||
|
position: 'topright',
|
||||||
|
searchUrl: '/api/geocoding',
|
||||||
|
maxResults: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (options) {
|
||||||
|
super()
|
||||||
|
L.Util.setOptions(this, options)
|
||||||
|
this._refreshTimeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd (map) {
|
||||||
|
this._map = map
|
||||||
|
this._container = L.DomUtil.create('div', 'leaflet-search')
|
||||||
|
this._resultLocations = []
|
||||||
|
this._selectedIndex = -1
|
||||||
|
|
||||||
|
this._buttonBar = L.DomUtil.create('div', 'leaflet-bar', this._container)
|
||||||
|
|
||||||
|
this._toggleButton = L.DomUtil.create('a', '', this._buttonBar)
|
||||||
|
this._toggleButton.href = '#'
|
||||||
|
this._toggleButton.role = 'button'
|
||||||
|
this._toggleButton.style.fontFamily = 'FontAwesome'
|
||||||
|
this._toggleButton.text = '\uf002'
|
||||||
|
this._toggleButton.title = this.options.labels.hint
|
||||||
|
this._toggleButton.setAttribute('aria-label', this.options.labels.hint)
|
||||||
|
|
||||||
|
this._control = L.DomUtil.create('div', 'leaflet-search--control', this._container)
|
||||||
|
this._control.style.visibility = 'collapse'
|
||||||
|
|
||||||
|
this._searchInput = L.DomUtil.create('input', 'leaflet-search--input', this._control)
|
||||||
|
this._searchInput.placeholder = this.options.labels.hint
|
||||||
|
|
||||||
|
this._feedback = L.DomUtil.create('div', '', this._control)
|
||||||
|
|
||||||
|
this._resultList = L.DomUtil.create('div', 'leaflet-search--result-list', this._control)
|
||||||
|
this._resultList.style.visibility = 'collapse'
|
||||||
|
this._resultList.tabIndex = 0
|
||||||
|
this._resultList.setAttribute('aria-role', 'list')
|
||||||
|
|
||||||
|
L.DomEvent
|
||||||
|
.on(this._container, 'click', L.DomEvent.stop, this)
|
||||||
|
.on(this._control, 'focusin', this._onControlFocusIn, this)
|
||||||
|
.on(this._control, 'focusout', this._onControlFocusOut, this)
|
||||||
|
.on(this._control, 'keydown', this._onControlKeyDown, this)
|
||||||
|
.on(this._toggleButton, 'click', this._onToggleButtonClick, this)
|
||||||
|
.on(this._searchInput, 'keydown', this._onSearchInputKeyDown, this)
|
||||||
|
.on(this._searchInput, 'input', this._onSearchInput, this)
|
||||||
|
.on(this._searchInput, 'mousemove', this._onSearchInputMove, this)
|
||||||
|
.on(this._searchInput, 'touchmove', this._onSearchInputMove, this)
|
||||||
|
.on(this._resultList, 'keydown', this._onResultListKeyDown, this)
|
||||||
|
|
||||||
|
return this._container
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove (map) {
|
||||||
|
}
|
||||||
|
|
||||||
|
_showControl () {
|
||||||
|
this._container.classList.add('open')
|
||||||
|
this._buttonBar.style.visibility = 'collapse'
|
||||||
|
this._control.style.removeProperty('visibility')
|
||||||
|
this._initialBounds = this._map.getBounds()
|
||||||
|
setTimeout(() => this._searchInput.focus(), 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideControl (resetBounds) {
|
||||||
|
this._container.classList.remove('open')
|
||||||
|
if (resetBounds) {
|
||||||
|
this._map.fitBounds(this._initialBounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._buttonBar.style.removeProperty('visibility')
|
||||||
|
this._control.style.visibility = 'collapse'
|
||||||
|
this._toggleButton.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
_onControlFocusIn (event) {
|
||||||
|
clearTimeout(this._hideTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onControlFocusOut (event) {
|
||||||
|
// need to debounce here because leaflet raises focusout then focusin when
|
||||||
|
// clicking on an already focused child element.
|
||||||
|
this._hideTimeout = setTimeout(() => this._hideControl(), 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSelectedLocation () {
|
||||||
|
if (this._selectedIndex === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._resultLocations[this._selectedIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusLocation (location) {
|
||||||
|
if (location.bounds !== undefined) {
|
||||||
|
this._map.fitBounds(location.bounds)
|
||||||
|
} else {
|
||||||
|
this._map.panTo(location.latlng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateLocation (location) {
|
||||||
|
this._focusLocation(location)
|
||||||
|
this._hideControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchInputMove (event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
_onControlKeyDown (event) {
|
||||||
|
if (event.keyCode === 27) { // escape
|
||||||
|
this._hideControl(true)
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (event.keyCode === 13) { // enter
|
||||||
|
const selectedLocation = this._getSelectedLocation()
|
||||||
|
if (selectedLocation) {
|
||||||
|
this._validateLocation(selectedLocation)
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onToggleButtonClick () {
|
||||||
|
this._showControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectIndex (index) {
|
||||||
|
for (const resultItem of this._resultList.children) {
|
||||||
|
resultItem.classList.remove('selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
this._selectedIndex = index
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
this._map.fitBounds(this._initialBounds)
|
||||||
|
this._searchInput.focus()
|
||||||
|
} else {
|
||||||
|
this._focusLocation(this._resultLocations[index])
|
||||||
|
const selectedElement = this._resultList.children[index]
|
||||||
|
selectedElement.classList.add('selected')
|
||||||
|
this._resultList.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchInputKeyDown (event) {
|
||||||
|
const results = this._resultLocations
|
||||||
|
if (results.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode === 38) {
|
||||||
|
this._selectIndex(results.length - 1)
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (event.keyCode === 40) {
|
||||||
|
this._selectIndex(0)
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearResults () {
|
||||||
|
while (this._resultList.lastElementChild) {
|
||||||
|
this._resultList.removeChild(this._resultList.lastElementChild)
|
||||||
|
}
|
||||||
|
this._resultList.style.visibility = 'collapse'
|
||||||
|
this._resultLocations = []
|
||||||
|
}
|
||||||
|
|
||||||
|
_fetchResults () {
|
||||||
|
const searchString = this._searchInput.value
|
||||||
|
|
||||||
|
if (!searchString) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._clearResults()
|
||||||
|
|
||||||
|
this._feedback.innerHTML = this.options.labels.searching
|
||||||
|
this._feedback.classList.remove('error')
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: this.options.searchUrl,
|
||||||
|
data: { q: searchString },
|
||||||
|
success: (data) => {
|
||||||
|
this._feedback.innerHTML = ''
|
||||||
|
this._resultLocations = []
|
||||||
|
const firstResults = data.slice(0, this.options.maxResults)
|
||||||
|
|
||||||
|
if (firstResults.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resultList.style.removeProperty('visibility')
|
||||||
|
|
||||||
|
for (const result of firstResults) {
|
||||||
|
const resultItem = L.DomUtil.create('div', 'leaflet-search--result-item', this._resultList)
|
||||||
|
resultItem.innerHTML = result.display_name
|
||||||
|
resultItem.title = result.display_name
|
||||||
|
resultItem.setAttribute('aria-role', 'list-item')
|
||||||
|
L.DomEvent.on(resultItem, 'click', this._onResultItemClick, this)
|
||||||
|
|
||||||
|
const itemLocation = {
|
||||||
|
latlng: L.latLng(result.lat, result.lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
const bbox = result.boundingbox
|
||||||
|
|
||||||
|
if (bbox !== undefined) {
|
||||||
|
itemLocation.bounds = L.latLngBounds(
|
||||||
|
L.latLng(bbox[0], bbox[2]),
|
||||||
|
L.latLng(bbox[1], bbox[3])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._resultLocations.push(itemLocation)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this._feedback.innerHTML = this.options.labels.error
|
||||||
|
this._feedback.classList.add('error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchInput () {
|
||||||
|
clearTimeout(this._refreshTimeout)
|
||||||
|
if (this._searchInput.value === '') {
|
||||||
|
this._clearResults()
|
||||||
|
} else {
|
||||||
|
this._refreshTimeout = setTimeout(() => this._fetchResults(), 250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onResultItemClick (event) {
|
||||||
|
const elementIndex = Array.prototype.indexOf.call(this._resultList.children, event.target)
|
||||||
|
this._selectIndex(elementIndex)
|
||||||
|
const selectedLocation = this._getSelectedLocation()
|
||||||
|
this._validateLocation(selectedLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onResultListKeyDown (event) {
|
||||||
|
const results = this._resultLocations
|
||||||
|
if (event.keyCode === 38) {
|
||||||
|
this._selectIndex(this._selectedIndex - 1)
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (event.keyCode === 40) {
|
||||||
|
if (this._selectedIndex === results.length - 1) {
|
||||||
|
this._selectIndex(-1)
|
||||||
|
} else {
|
||||||
|
this._selectIndex(this._selectedIndex + 1)
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(SearchControl.prototype, L.Mixin.Events)
|
||||||
|
|
||||||
|
L.Control.Search = SearchControl
|
|
@ -11,6 +11,11 @@
|
||||||
{% block map-include-geoloc-button %}
|
{% block map-include-geoloc-button %}
|
||||||
data-include-geoloc-button="true"
|
data-include-geoloc-button="true"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block map-include-search-button %}
|
||||||
|
{% if include_search_button %}
|
||||||
|
data-search-url="{% url 'mapcell-geocoding' %}"
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
{% if group_markers %}data-group-markers="1"{% endif %}
|
{% if group_markers %}data-group-markers="1"{% endif %}
|
||||||
data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}"
|
data-marker-behaviour-onclick="{{ cell.marker_behaviour_onclick }}"
|
||||||
{% if max_bounds.corner1.lat %}
|
{% if max_bounds.corner1.lat %}
|
||||||
|
|
|
@ -14,16 +14,15 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.conf.urls import re_path
|
from django.urls import include, path, re_path
|
||||||
from django.urls import include
|
|
||||||
|
|
||||||
from combo.urls_utils import decorated_includes, staff_required
|
from combo.urls_utils import decorated_includes, staff_required
|
||||||
|
|
||||||
from . import manager_views
|
from . import manager_views
|
||||||
from .views import GeojsonView
|
from .views import GeojsonView, geocoding_view
|
||||||
|
|
||||||
maps_manager_urls = [
|
maps_manager_urls = [
|
||||||
re_path('^$', manager_views.ManagerHomeView.as_view(), name='maps-manager-homepage'),
|
path('', manager_views.ManagerHomeView.as_view(), name='maps-manager-homepage'),
|
||||||
re_path(
|
re_path(
|
||||||
'^layers/add/(?P<kind>geojson|tiles)/$',
|
'^layers/add/(?P<kind>geojson|tiles)/$',
|
||||||
manager_views.LayerAddView.as_view(),
|
manager_views.LayerAddView.as_view(),
|
||||||
|
@ -68,4 +67,5 @@ urlpatterns = [
|
||||||
GeojsonView.as_view(),
|
GeojsonView.as_view(),
|
||||||
name='mapcell-geojson',
|
name='mapcell-geojson',
|
||||||
),
|
),
|
||||||
|
path('api/geocoding', geocoding_view, name='mapcell-geocoding'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,11 +15,16 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from django.http import HttpResponse, HttpResponseForbidden
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
|
|
||||||
|
from combo.utils import requests
|
||||||
|
|
||||||
from .models import Map
|
from .models import Map
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,3 +38,21 @@ class GeojsonView(View):
|
||||||
geojson = layer.get_geojson(request, options.properties)
|
geojson = layer.get_geojson(request, options.properties)
|
||||||
content_type = 'application/json'
|
content_type = 'application/json'
|
||||||
return HttpResponse(json.dumps(geojson), content_type=content_type)
|
return HttpResponse(json.dumps(geojson), content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
|
def geocoding_view(request, *args, **kwargs):
|
||||||
|
if 'q' not in request.GET:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if not Map.objects.filter(include_search_button=True).exists():
|
||||||
|
raise PermissionDenied()
|
||||||
|
q = request.GET['q']
|
||||||
|
url = settings.COMBO_MAP_GEOCODING_URL
|
||||||
|
if '?' in url:
|
||||||
|
url += '&'
|
||||||
|
else:
|
||||||
|
url += '?'
|
||||||
|
url += 'format=json&q=%s' % urllib.parse.quote(q)
|
||||||
|
url += '&accept-language=%s' % settings.LANGUAGE_CODE.split('-')[0]
|
||||||
|
return HttpResponse(
|
||||||
|
requests.get(url, without_user=True, remote_service=False).text, content_type='application/json'
|
||||||
|
)
|
||||||
|
|
|
@ -304,6 +304,9 @@ COMBO_MAP_ATTRIBUTION = _(
|
||||||
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
|
'<a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# geocoding service
|
||||||
|
COMBO_MAP_GEOCODING_URL = 'https://nominatim.entrouvert.org/search'
|
||||||
|
|
||||||
# send notifications about new invoices
|
# send notifications about new invoices
|
||||||
LINGO_INVOICE_NOTIFICATIONS_ENABLED = True
|
LINGO_INVOICE_NOTIFICATIONS_ENABLED = True
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import re
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import responses
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -748,3 +749,29 @@ def test_duplicate(layer):
|
||||||
assert options.map_layer == layer
|
assert options.map_layer == layer
|
||||||
assert options.opacity == 0.5
|
assert options.opacity == 0.5
|
||||||
assert options.properties == 'a, b'
|
assert options.properties == 'a, b'
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_geocoding(app):
|
||||||
|
page = Page(title='xxx', slug='test_map_cell', template_name='standard')
|
||||||
|
page.save()
|
||||||
|
cell = Map(page=page, placeholder='content', order=0, title='Map')
|
||||||
|
cell.save()
|
||||||
|
|
||||||
|
resp = app.get('/test_map_cell/')
|
||||||
|
assert not resp.pyquery('[data-search-url]')
|
||||||
|
cell.include_search_button = True
|
||||||
|
cell.save()
|
||||||
|
resp = app.get('/test_map_cell/')
|
||||||
|
search_url = resp.pyquery('[data-search-url]').attr('data-search-url')
|
||||||
|
app.get(search_url, status=400)
|
||||||
|
responses.get(
|
||||||
|
settings.COMBO_MAP_GEOCODING_URL, json=[{'display_name': 'address', 'lat': '1', 'lon': '2'}]
|
||||||
|
)
|
||||||
|
resp = app.get(search_url + '?q=test')
|
||||||
|
assert resp.json == [{'display_name': 'address', 'lat': '1', 'lon': '2'}]
|
||||||
|
assert responses.calls[0].request.path_url == '/search?format=json&q=test&accept-language=en'
|
||||||
|
|
||||||
|
cell.include_search_button = False
|
||||||
|
cell.save()
|
||||||
|
app.get(search_url + '?q=test', status=403)
|
||||||
|
|
Loading…
Reference in New Issue