Merge branch 'master' into dependabot/pip/django-5.0.1

This commit is contained in:
David Larlet 2024-01-30 14:01:55 -05:00 committed by GitHub
commit 5da9b67b2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2143 additions and 78 deletions

11
.eslintrc.json Normal file
View file

@ -0,0 +1,11 @@
{
"plugins": ["compat"],
"extends": ["plugin:compat/recommended"],
"env": {
"es6": true
},
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
}
}

View file

@ -57,6 +57,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python3 -m pip install -e .[test,dev] python3 -m pip install -e .[test,dev]
make installjs
- name: Run Lint - name: Run Lint
run: make lint run: make lint

View file

@ -20,6 +20,7 @@ format: ## Format the code and templates files
.PHONY: lint .PHONY: lint
lint: ## Lint the code and template files lint: ## Lint the code and template files
npx eslint umap/static/umap/ &&\
djlint umap/templates --lint &&\ djlint umap/templates --lint &&\
isort --check --profile black umap/ &&\ isort --check --profile black umap/ &&\
ruff format --check --target-version=py310 umap/ &&\ ruff format --check --target-version=py310 umap/ &&\

View file

@ -42,7 +42,7 @@
- `umap/templates/umap/map_table.html` - `umap/templates/umap/map_table.html`
- `umap/templates/umap/user_dashboard.html` - `umap/templates/umap/user_dashboard.html`
[See the diff](https://github.com/umap-project/umap/compare/1.12.2...1.13.0#diff-1311890945256dbddf0e59928c2e9d4f59fd6bcc6b1fd33719ef35f03e5168b4). [See the diff](https://github.com/umap-project/umap/compare/1.12.2...1.13.0#files_bucket).
## 1.12.2 - 2023-12-29 ## 1.12.2 - 2023-12-29

View file

@ -203,6 +203,13 @@ ready for production use (no backup, etc.)
Link to show on the header under the "Feedback and help" label. Link to show on the header under the "Feedback and help" label.
#### UMAP_HOME_FEED
Which feed to display on the home page. Three valid values:
- `"latest"`, which shows the latest maps (default)
- `"highlighted"`, which shows the maps that have been starred by a staff member
- `None`, which does not show any map on the home page
#### UMAP_MAPS_PER_PAGE #### UMAP_MAPS_PER_PAGE
How many maps to show in maps list, like search or home page. How many maps to show in maps list, like search or home page.

View file

@ -1,4 +1,6 @@
# How to make a release # Releases
## How to make a release
1. Run tests: 1. Run tests:
- `make test` - `make test`
@ -20,12 +22,35 @@
9. `make publish` 9. `make publish`
10. `make docker` 10. `make docker`
## Deploying instances ### Deploying instances
### OSMfr #### OSMfr
The process is manual for now, Yohan has one Makefile on his computer. The process is manual for now, Yohan has one Makefile on his computer.
### ANCT #### ANCT
Update the [Dockerfile](https://gitlab.com/incubateur-territoires/startups/donnees-et-territoires/umap-dsfr-moncomptepro/-/blob/main/Dockerfile?ref_type=heads) with correct version and put a tag `YYYY.MM.DD` in order to deploy it to production. Update the [Dockerfile](https://gitlab.com/incubateur-territoires/startups/donnees-et-territoires/umap-dsfr-moncomptepro/-/blob/main/Dockerfile?ref_type=heads) with correct version and put a tag `YYYY.MM.DD` in order to deploy it to production.
## When to make a release
We aim to support [Baseline](https://developer.mozilla.org/en-US/blog/baseline-evolution-on-mdn/) “Widely available” (implemented in major browsers within the last 30 months).
### Major (2.Y.Z)
* when we bump Django to a major version
* when we change how we store data (both in database and filesystem)
### Minor (X.3.Z)
* when we add new features
* when we improve an existing feature
* when we improve the usability
* when we change templates
If it's not a major nor a patch, it's a minor.
### Patch (X.Y.12)
* when there are bugfixes

1715
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,8 @@
}, },
"devDependencies": { "devDependencies": {
"chai": "^3.3.0", "chai": "^3.3.0",
"eslint": "^8.56.0",
"eslint-plugin-compat": "^4.2.0",
"happen": "~0.1.3", "happen": "~0.1.3",
"lebab": "^3.2.1", "lebab": "^3.2.1",
"mocha": "^10.2.0", "mocha": "^10.2.0",
@ -60,5 +62,8 @@
"simple-statistics": "^7.8.3", "simple-statistics": "^7.8.3",
"togpx": "^0.5.4", "togpx": "^0.5.4",
"tokml": "0.4.0" "tokml": "0.4.0"
} },
"browserslist": [
"> 0.5%, last 2 versions, Firefox ESR, not dead, not op_mini all"
]
} }

View file

@ -48,7 +48,7 @@ dev = [
"djlint==1.34.1", "djlint==1.34.1",
"mkdocs==1.5.3", "mkdocs==1.5.3",
"mkdocs-material==9.4.14", "mkdocs-material==9.4.14",
"vermin==1.5.2", "vermin==1.6.0",
"pymdown-extensions==10.4", "pymdown-extensions==10.4",
"isort==5.12", "isort==5.12",
] ]

View file

@ -216,7 +216,7 @@ class Map(NamedModel):
"umap_id": self.pk, "umap_id": self.pk,
"onLoadPanel": "none", "onLoadPanel": "none",
"captionBar": False, "captionBar": False,
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL,
"slideshow": {}, "slideshow": {},
} }
) )

View file

@ -34,10 +34,7 @@ if path:
for key in dir(d): for key in dir(d):
if key.isupper(): if key.isupper():
value = getattr(d, key) value = getattr(d, key)
if key.startswith("LEAFLET_STORAGE"): if key == "UMAP_CUSTOM_TEMPLATES":
# Retrocompat pre 1.0, remove me in 1.1.
globals()["UMAP" + key[15:]] = value
elif key == "UMAP_CUSTOM_TEMPLATES":
if "DIRS" in globals()["TEMPLATES"][0]: if "DIRS" in globals()["TEMPLATES"][0]:
globals()["TEMPLATES"][0]["DIRS"].insert(0, value) globals()["TEMPLATES"][0]["DIRS"].insert(0, value)
else: else:

View file

@ -255,6 +255,7 @@ DATABASES = {"default": env.db(default="postgis://localhost:5432/umap")}
UMAP_DEFAULT_SHARE_STATUS = None UMAP_DEFAULT_SHARE_STATUS = None
UMAP_DEFAULT_EDIT_STATUS = None UMAP_DEFAULT_EDIT_STATUS = None
UMAP_DEFAULT_FEATURES_HAVE_OWNERS = False UMAP_DEFAULT_FEATURES_HAVE_OWNERS = False
UMAP_HOME_FEED = "latest"
UMAP_READONLY = env("UMAP_READONLY", default=False) UMAP_READONLY = env("UMAP_READONLY", default=False)
UMAP_GZIP = True UMAP_GZIP = True

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" id="circle" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15">
<path d="M14,7.5c0,3.5899-2.9101,6.5-6.5,6.5S1,11.0899,1,7.5S3.9101,1,7.5,1S14,3.9101,14,7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

View file

@ -110,13 +110,13 @@ L.U.Browser = L.Class.extend({
const formContainer = L.DomUtil.create('div', '', container) const formContainer = L.DomUtil.create('div', '', container)
const dataContainer = L.DomUtil.create('div', 'umap-browse-features', container) const dataContainer = L.DomUtil.create('div', 'umap-browse-features', container)
const appendAll = () => { const rebuildHTML = () => {
dataContainer.innerHTML = '' dataContainer.innerHTML = ''
this.map.eachBrowsableDataLayer((datalayer) => { this.map.eachBrowsableDataLayer((datalayer) => {
this.addDatalayer(datalayer, dataContainer) this.addDatalayer(datalayer, dataContainer)
}) })
} }
const resetLayers = () => { const redrawDataLayers = () => {
this.map.eachBrowsableDataLayer((datalayer) => { this.map.eachBrowsableDataLayer((datalayer) => {
datalayer.resetLayer(true) datalayer.resetLayer(true)
}) })
@ -129,16 +129,16 @@ L.U.Browser = L.Class.extend({
makeDirty: false, makeDirty: false,
callback: (e) => { callback: (e) => {
if (e.helper.field === 'options.inBbox') { if (e.helper.field === 'options.inBbox') {
if (this.options.inBbox) this.map.on('moveend', appendAll) if (this.options.inBbox) this.map.on('moveend', rebuildHTML)
else this.map.off('moveend', appendAll) else this.map.off('moveend', rebuildHTML)
} }
appendAll() redrawDataLayers()
resetLayers() rebuildHTML()
}, },
}) })
formContainer.appendChild(builder.build()) formContainer.appendChild(builder.build())
appendAll() rebuildHTML()
this.map.ui.openPanel({ this.map.ui.openPanel({
data: { html: container }, data: { html: container },

View file

@ -1192,7 +1192,7 @@ L.U.AttributionControl = L.Control.Attribution.extend({
'', '',
container, container,
`${L._('Powered by uMap')}`, `${L._('Powered by uMap')}`,
'https://github.com/umap-project/umap/' 'https://umap-project.org/'
) )
} }
L.DomUtil.createLink('attribution-toggle', this._container, '') L.DomUtil.createLink('attribution-toggle', this._container, '')

View file

@ -548,6 +548,7 @@ L.U.Help = L.Class.extend({
label.title = label.textContent = L._('Close') label.title = label.textContent = L._('Close')
this.content = L.DomUtil.create('div', 'umap-help-content', this.box) this.content = L.DomUtil.create('div', 'umap-help-content', this.box)
this.isMacOS = /mac/i.test( this.isMacOS = /mac/i.test(
// eslint-disable-next-line compat/compat -- Fallback available.
navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform
) )
}, },

View file

@ -16,6 +16,7 @@ L.U.Importer = L.Class.extend({
{ type: 'file', multiple: 'multiple', autofocus: true }, { type: 'file', multiple: 'multiple', autofocus: true },
this.fileBox this.fileBox
) )
this.map.ui.once('panel:closed', () => (this.fileInput.value = null))
this.urlInput = L.DomUtil.element( this.urlInput = L.DomUtil.element(
'input', 'input',
{ type: 'text', placeholder: L._('Provide an URL here') }, { type: 'text', placeholder: L._('Provide an URL here') },

View file

@ -122,6 +122,11 @@ L.U.Map.include({
`${this.HIDDABLE_CONTROLS[i]}Control` `${this.HIDDABLE_CONTROLS[i]}Control`
) )
} }
// Specific case for datalayersControl
// which accept "expanded" value, on top of true/false/null
if (L.Util.queryString('datalayersControl') === 'expanded') {
L.Util.setFromQueryString(this.options, 'datalayersControl')
}
this.datalayersOnLoad = L.Util.queryString('datalayers') this.datalayersOnLoad = L.Util.queryString('datalayers')
this.options.onLoadPanel = L.Util.queryString( this.options.onLoadPanel = L.Util.queryString(
'onLoadPanel', 'onLoadPanel',

View file

@ -194,6 +194,7 @@ L.U.Layer.Choropleth = L.FeatureGroup.extend({
let mode = this.datalayer.options.choropleth.mode, let mode = this.datalayer.options.choropleth.mode,
classes = +this.datalayer.options.choropleth.classes || 5, classes = +this.datalayer.options.choropleth.classes || 5,
breaks breaks
classes = Math.min(classes, values.length)
if (mode === 'manual') { if (mode === 'manual') {
const manualBreaks = this.datalayer.options.choropleth.breaks const manualBreaks = this.datalayer.options.choropleth.breaks
if (manualBreaks) { if (manualBreaks) {

View file

@ -1322,10 +1322,10 @@ a.add-datalayer:hover,
margin: 5px; margin: 5px;
} }
a[href^='mailto']::before { a[href^='mailto']::before {
content: '🖃 '; content: '✉︎ ';
} }
a[href^='tel']::before { a[href^='tel']::before {
content: '🕿 '; content: '☎︎ ';
} }
address span { address span {
padding-right: 5px; padding-right: 5px;

View file

@ -7,7 +7,6 @@
"after": true, "after": true,
"it": true, "it": true,
"sinon": true, "sinon": true,
"qs": true,
"enableEdit": true, "enableEdit": true,
"disableEdit": true, "disableEdit": true,
"changeInputValue": true, "changeInputValue": true,

View file

@ -132,7 +132,7 @@ function initMap(options) {
map_update_permissions: '/map/{map_id}/update/permissions/', map_update_permissions: '/map/{map_id}/update/permissions/',
map_download: '/map/{map_id}/download/', map_download: '/map/{map_id}/download/',
}, },
default_iconUrl: '../src/img/marker.png', default_iconUrl: '../src/img/marker.svg',
zoom: 6, zoom: 6,
share_statuses: [ share_statuses: [
[1, 'Tout le monde (public)'], [1, 'Tout le monde (public)'],

View file

@ -10,7 +10,9 @@
</div> </div>
{% endif %} {% endif %}
<div class="wrapper"> <div class="wrapper">
{% if maps %}
<h2 class="section">{% blocktrans %}Get inspired, browse maps{% endblocktrans %}</h2> <h2 class="section">{% blocktrans %}Get inspired, browse maps{% endblocktrans %}</h2>
<div class="map_list row">{% include "umap/map_list.html" %}</div> <div class="map_list row">{% include "umap/map_list.html" %}</div>
{% endif %}
</div> </div>
{% endblock maincontent %} {% endblock maincontent %}

View file

@ -7,9 +7,17 @@
map_detail map_detail
{% endblock body_class %} {% endblock body_class %}
{% block extra_head %} {% block extra_head %}
{% if preconnect_domains %}
{% for domain in preconnect_domains %}
<link rel="preconnect" href="{{ domain }}" />
{% endfor %}
{% endif %}
{% umap_css %} {% umap_css %}
{% umap_js locale=locale %} {% umap_js locale=locale %}
{% if object.share_status != object.PUBLIC %}<meta name="robots" content="noindex">{% endif %} {% if object.share_status != object.PUBLIC %}<meta name="robots" content="noindex">{% endif %}
<link rel="alternate" type="application/json+oembed"
href="{{ oembed_absolute_uri }}?url={{ absolute_uri|urlencode }}&format=json"
title="{{ map.name }} oEmbed URL" />
{% endblock extra_head %} {% endblock extra_head %}
{% block content %} {% block content %}
{% block map_init %} {% block map_init %}

View file

@ -81,7 +81,7 @@ class MapFactory(factory.django.DjangoModelFactory):
"attribution": "\xa9 OSM Contributors", "attribution": "\xa9 OSM Contributors",
"maxZoom": 18, "maxZoom": 18,
"minZoom": 0, "minZoom": 0,
"url_template": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", "url_template": "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
}, },
"tilelayersControl": True, "tilelayersControl": True,
"zoom": 7, "zoom": 7,

View file

@ -13,12 +13,12 @@ DATALAYER_DATA = {
"features": [ "features": [
{ {
"type": "Feature", "type": "Feature",
"properties": {"name": "one point in france"}, "properties": {"name": "one point in france", "foo": "point"},
"geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]}, "geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]},
}, },
{ {
"type": "Feature", "type": "Feature",
"properties": {"name": "one polygon in greenland"}, "properties": {"name": "one polygon in greenland", "foo": "polygon"},
"geometry": { "geometry": {
"type": "Polygon", "type": "Polygon",
"coordinates": [ "coordinates": [
@ -34,7 +34,7 @@ DATALAYER_DATA = {
}, },
{ {
"type": "Feature", "type": "Feature",
"properties": {"name": "one line in new zeland"}, "properties": {"name": "one line in new zeland", "foo": "line"},
"geometry": { "geometry": {
"type": "LineString", "type": "LineString",
"coordinates": [ "coordinates": [
@ -72,14 +72,28 @@ def test_data_browser_should_be_open(live_server, page, bootstrap, map):
def test_data_browser_should_be_filterable(live_server, page, bootstrap, map): def test_data_browser_should_be_filterable(live_server, page, bootstrap, map):
page.goto(f"{live_server.url}{map.get_absolute_url()}") page.goto(f"{live_server.url}{map.get_absolute_url()}")
markers = page.locator(".leaflet-marker-icon") markers = page.locator(".leaflet-marker-icon")
paths = page.locator(".leaflet-overlay-pane path")
expect(markers).to_have_count(1) expect(markers).to_have_count(1)
el = page.locator("input[name='filter']") expect(paths).to_have_count(2)
expect(el).to_be_visible() filter_ = page.locator("input[name='filter']")
el.type("poly") expect(filter_).to_be_visible()
filter_.type("poly")
expect(page.get_by_text("one point in france")).to_be_hidden() expect(page.get_by_text("one point in france")).to_be_hidden()
expect(page.get_by_text("one line in new zeland")).to_be_hidden() expect(page.get_by_text("one line in new zeland")).to_be_hidden()
expect(page.get_by_text("one polygon in greenland")).to_be_visible() expect(page.get_by_text("one polygon in greenland")).to_be_visible()
expect(markers).to_have_count(0) # Hidden by filter expect(markers).to_have_count(0) # Hidden by filter
expect(paths).to_have_count(1) # Only polygon
# Empty the filter
filter_.fill("")
filter_.blur()
expect(markers).to_have_count(1)
expect(paths).to_have_count(2)
filter_.type("point")
expect(page.get_by_text("one point in france")).to_be_visible()
expect(page.get_by_text("one line in new zeland")).to_be_hidden()
expect(page.get_by_text("one polygon in greenland")).to_be_hidden()
expect(markers).to_have_count(1)
expect(paths).to_have_count(0)
def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map): def test_data_browser_can_show_only_visible_features(live_server, page, bootstrap, map):
@ -131,3 +145,25 @@ def test_data_browser_bbox_limit_should_be_dynamic(live_server, page, bootstrap,
expect(page.get_by_text("one point in france")).to_be_visible() expect(page.get_by_text("one point in france")).to_be_visible()
expect(page.get_by_text("one polygon in greenland")).to_be_visible() expect(page.get_by_text("one polygon in greenland")).to_be_visible()
expect(page.get_by_text("one line in new zeland")).to_be_hidden() expect(page.get_by_text("one line in new zeland")).to_be_hidden()
def test_data_browser_with_variable_in_name(live_server, page, bootstrap, map):
# Include a variable
map.settings["properties"]["labelKey"] = "{name} ({foo})"
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(page.get_by_text("one point in france (point)")).to_be_visible()
expect(page.get_by_text("one line in new zeland (line)")).to_be_visible()
expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_visible()
filter_ = page.locator("input[name='filter']")
expect(filter_).to_be_visible()
filter_.type("foobar") # Hide all
expect(page.get_by_text("one point in france (point)")).to_be_hidden()
expect(page.get_by_text("one line in new zeland (line)")).to_be_hidden()
expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_hidden()
# Empty back the filter
filter_.fill("")
filter_.blur()
expect(page.get_by_text("one point in france (point)")).to_be_visible()
expect(page.get_by_text("one line in new zeland (line)")).to_be_visible()
expect(page.get_by_text("one polygon in greenland (polygon)")).to_be_visible()

View file

@ -1,9 +1,14 @@
import re
from time import sleep
from playwright.sync_api import expect from playwright.sync_api import expect
from umap.models import DataLayer from umap.models import DataLayer
from ..base import DataLayerFactory, MapFactory from ..base import DataLayerFactory, MapFactory
DATALAYER_UPDATE = re.compile(r".*/datalayer/update/.*")
def test_collaborative_editing_create_markers(context, live_server, tilelayer): def test_collaborative_editing_create_markers(context, live_server, tilelayer):
# Let's create a new map with an empty datalayer # Let's create a new map with an empty datalayer
@ -31,11 +36,17 @@ def test_collaborative_editing_create_markers(context, live_server, tilelayer):
map_el_p1.click(position={"x": 200, "y": 200}) map_el_p1.click(position={"x": 200, "y": 200})
expect(marker_pane_p1).to_have_count(1) expect(marker_pane_p1).to_have_count(1)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click() save_p1.click()
# Prefent two layers to be saved on the same second, as we compare them based
# on time in case of conflict. FIXME do not use time for comparison.
sleep(1)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).settings == {
"browsable": True, "browsable": True,
"displayOnLoad": True, "displayOnLoad": True,
"name": "test datalayer", "name": "test datalayer",
"editMode": "advanced",
"inCaption": True,
} }
# Now navigate to this map from another tab # Now navigate to this map from another tab
@ -60,7 +71,9 @@ def test_collaborative_editing_create_markers(context, live_server, tilelayer):
map_el_p2.click(position={"x": 220, "y": 220}) map_el_p2.click(position={"x": 220, "y": 220})
expect(marker_pane_p2).to_have_count(2) expect(marker_pane_p2).to_have_count(2)
with page_two.expect_response(DATALAYER_UPDATE):
save_p2.click() save_p2.click()
sleep(1)
# No change after the save # No change after the save
expect(marker_pane_p2).to_have_count(2) expect(marker_pane_p2).to_have_count(2)
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).settings == {
@ -75,6 +88,7 @@ def test_collaborative_editing_create_markers(context, live_server, tilelayer):
create_marker_p1.click() create_marker_p1.click()
map_el_p1.click(position={"x": 150, "y": 150}) map_el_p1.click(position={"x": 150, "y": 150})
expect(marker_pane_p1).to_have_count(2) expect(marker_pane_p1).to_have_count(2)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click() save_p1.click()
# Should now get the other marker too # Should now get the other marker too
expect(marker_pane_p1).to_have_count(3) expect(marker_pane_p1).to_have_count(3)
@ -92,7 +106,9 @@ def test_collaborative_editing_create_markers(context, live_server, tilelayer):
create_marker_p1.click() create_marker_p1.click()
map_el_p1.click(position={"x": 180, "y": 150}) map_el_p1.click(position={"x": 180, "y": 150})
expect(marker_pane_p1).to_have_count(4) expect(marker_pane_p1).to_have_count(4)
with page_one.expect_response(DATALAYER_UPDATE):
save_p1.click() save_p1.click()
sleep(1)
# Should now get the other marker too # Should now get the other marker too
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).settings == {
"browsable": True, "browsable": True,
@ -110,7 +126,9 @@ def test_collaborative_editing_create_markers(context, live_server, tilelayer):
create_marker_p2.click() create_marker_p2.click()
map_el_p2.click(position={"x": 250, "y": 150}) map_el_p2.click(position={"x": 250, "y": 150})
expect(marker_pane_p2).to_have_count(3) expect(marker_pane_p2).to_have_count(3)
with page_two.expect_response(DATALAYER_UPDATE):
save_p2.click() save_p2.click()
sleep(1)
# Should now get the other markers too # Should now get the other markers too
assert DataLayer.objects.get(pk=datalayer.pk).settings == { assert DataLayer.objects.get(pk=datalayer.pk).settings == {
"browsable": True, "browsable": True,

View file

@ -61,7 +61,7 @@ def test_umap_export(map, live_server, datalayer, page):
"attribution": "© OSM Contributors", "attribution": "© OSM Contributors",
"maxZoom": 18, "maxZoom": 18,
"minZoom": 0, "minZoom": 0,
"url_template": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", "url_template": "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
}, },
"tilelayersControl": True, "tilelayersControl": True,
"zoom": 7, "zoom": 7,

View file

@ -11,8 +11,9 @@ def test_umap_import_from_file(live_server, datalayer, page):
button = page.get_by_title("Import data") button = page.get_by_title("Import data")
expect(button).to_be_visible() expect(button).to_be_visible()
button.click() button.click()
file_input = page.locator("input[type='file']")
with page.expect_file_chooser() as fc_info: with page.expect_file_chooser() as fc_info:
page.locator("input[type='file']").click() file_input.click()
file_chooser = fc_info.value file_chooser = fc_info.value
path = Path(__file__).parent.parent / "fixtures/display_on_load.umap" path = Path(__file__).parent.parent / "fixtures/display_on_load.umap"
file_chooser.set_files(path) file_chooser.set_files(path)
@ -23,6 +24,10 @@ def test_umap_import_from_file(live_server, datalayer, page):
expect(layers).to_have_count(3) expect(layers).to_have_count(3)
nonloaded = page.locator(".umap-browse-datalayers li.off") nonloaded = page.locator(".umap-browse-datalayers li.off")
expect(nonloaded).to_have_count(1) expect(nonloaded).to_have_count(1)
assert file_input.input_value()
# Close the import panel
page.keyboard.press("Escape")
assert not file_input.input_value()
def test_umap_import_geojson_from_textarea(live_server, datalayer, page): def test_umap_import_geojson_from_textarea(live_server, datalayer, page):

View file

@ -12,6 +12,35 @@ from ..base import DataLayerFactory
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
def test_preconnect_for_tilelayer(map, page, live_server, tilelayer):
page.goto(f"{live_server.url}{map.get_absolute_url()}")
meta = page.locator('link[rel="preconnect"]')
expect(meta).to_have_count(1)
expect(meta).to_have_attribute("href", "//a.tile.openstreetmap.fr")
# Add custom tilelayer
map.settings["properties"]["tilelayer"] = {
"name": "OSM Piano FR",
"maxZoom": 20,
"minZoom": 0,
"attribution": "test",
"url_template": "https://a.piano.tiles.quaidorsay.fr/fr{r}/{z}/{x}/{y}.png",
}
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(meta).to_have_attribute("href", "//a.piano.tiles.quaidorsay.fr")
# Add custom tilelayer with variable in domain, should create a preconnect
map.settings["properties"]["tilelayer"] = {
"name": "OSM Piano FR",
"maxZoom": 20,
"minZoom": 0,
"attribution": "test",
"url_template": "https://{s}.piano.tiles.quaidorsay.fr/fr{r}/{z}/{x}/{y}.png",
}
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(meta).to_have_count(0)
def test_default_view_latest_without_datalayer_should_use_default_center( def test_default_view_latest_without_datalayer_should_use_default_center(
map, live_server, datalayer, page map, live_server, datalayer, page
): ):

View file

@ -46,7 +46,7 @@ def test_can_change_picto_at_map_level(map, live_server, page, pictos):
marker = page.locator(".umap-div-icon img") marker = page.locator(".umap-div-icon img")
expect(marker).to_have_count(1) expect(marker).to_have_count(1)
# Should have default img # Should have default img
expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") expect(marker).to_have_attribute("src", "/static/umap/img/marker.svg")
edit_settings = page.get_by_title("Edit map properties") edit_settings = page.get_by_title("Edit map properties")
expect(edit_settings).to_be_visible() expect(edit_settings).to_be_visible()
edit_settings.click() edit_settings.click()
@ -66,7 +66,7 @@ def test_can_change_picto_at_map_level(map, live_server, page, pictos):
symbols.click() symbols.click()
expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg") expect(marker).to_have_attribute("src", "/uploads/pictogram/star.svg")
undefine.click() undefine.click()
expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") expect(marker).to_have_attribute("src", "/static/umap/img/marker.svg")
def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos): def test_can_change_picto_at_datalayer_level(map, live_server, page, pictos):
@ -147,7 +147,7 @@ def test_can_use_remote_url_as_picto(map, live_server, page, pictos):
marker = page.locator(".umap-div-icon img") marker = page.locator(".umap-div-icon img")
expect(marker).to_have_count(1) expect(marker).to_have_count(1)
# Should have default img # Should have default img
expect(marker).to_have_attribute("src", "/static/umap/img/marker.png") expect(marker).to_have_attribute("src", "/static/umap/img/marker.svg")
edit_settings = page.get_by_title("Edit map properties") edit_settings = page.get_by_title("Edit map properties")
expect(edit_settings).to_be_visible() expect(edit_settings).to_be_visible()
edit_settings.click() edit_settings.click()

View file

@ -0,0 +1,39 @@
import pytest
from playwright.sync_api import expect
pytestmark = pytest.mark.django_db
def test_scale_control(map, live_server, datalayer, page):
control = page.locator(".leaflet-control-scale")
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(control).to_be_visible()
page.goto(f"{live_server.url}{map.get_absolute_url()}?scaleControl=false")
expect(control).to_be_hidden()
def test_datalayers_control(map, live_server, datalayer, page):
control = page.locator(".umap-browse-toggle")
box = page.locator(".umap-browse-datalayers")
more = page.get_by_title("More controls")
page.goto(f"{live_server.url}{map.get_absolute_url()}")
expect(control).to_be_visible()
expect(box).to_be_hidden()
page.goto(f"{live_server.url}{map.get_absolute_url()}?datalayersControl=true")
expect(control).to_be_visible()
expect(box).to_be_hidden()
page.goto(f"{live_server.url}{map.get_absolute_url()}?datalayersControl=null")
expect(control).to_be_hidden()
expect(more).to_be_visible()
more.click()
expect(control).to_be_visible()
expect(box).to_be_hidden()
page.goto(f"{live_server.url}{map.get_absolute_url()}?datalayersControl=false")
expect(control).to_be_hidden()
expect(more).to_be_visible()
more.click()
expect(control).to_be_hidden()
expect(box).to_be_hidden()
page.goto(f"{live_server.url}{map.get_absolute_url()}?datalayersControl=expanded")
expect(control).to_be_hidden()
expect(box).to_be_visible()

View file

@ -19,7 +19,7 @@ def post_data():
return { return {
"name": "name", "name": "name",
"center": '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa "center": '{"type":"Point","coordinates":[13.447265624999998,48.94415123418794]}', # noqa
"settings": '{"type":"Feature","geometry":{"type":"Point","coordinates":[5.0592041015625,52.05924589011585]},"properties":{"tilelayer":{"maxZoom":20,"url_template":"http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png","minZoom":0,"attribution":"HOT and friends"},"licence":"","description":"","name":"test enrhûmé","tilelayersControl":true,"displayDataBrowserOnLoad":false,"displayPopupFooter":true,"displayCaptionOnLoad":false,"miniMap":true,"moreControl":true,"scaleControl":true,"zoomControl":true,"datalayersControl":true,"zoom":8}}', # noqa "settings": '{"type":"Feature","geometry":{"type":"Point","coordinates":[5.0592041015625,52.05924589011585]},"properties":{"tilelayer":{"maxZoom":20,"url_template":"http://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png","minZoom":0,"attribution":"HOT and friends"},"licence":"","description":"","name":"test enrhûmé","tilelayersControl":true,"displayDataBrowserOnLoad":false,"displayPopupFooter":true,"displayCaptionOnLoad":false,"miniMap":true,"moreControl":true,"scaleControl":true,"zoomControl":true,"datalayersControl":true,"zoom":8}}', # noqa
} }
@ -275,7 +275,7 @@ def test_owner_cannot_access_map_with_share_status_blocked(client, map):
assert response.status_code == 403 assert response.status_code == 403
def test_non_editor_cannot_access_map_if_share_status_private(client, map, user): # noqa def test_non_editor_cannot_access_map_if_share_status_private(client, map, user):
url = reverse("map", args=(map.slug, map.pk)) url = reverse("map", args=(map.slug, map.pk))
map.share_status = map.PRIVATE map.share_status = map.PRIVATE
map.save() map.save()
@ -346,14 +346,14 @@ def test_anonymous_create(cookieclient, post_data):
@pytest.mark.usefixtures("allow_anonymous") @pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_update_without_cookie_fails(client, anonymap, post_data): # noqa def test_anonymous_update_without_cookie_fails(client, anonymap, post_data):
url = reverse("map_update", kwargs={"map_id": anonymap.pk}) url = reverse("map_update", kwargs={"map_id": anonymap.pk})
response = client.post(url, post_data) response = client.post(url, post_data)
assert response.status_code == 403 assert response.status_code == 403
@pytest.mark.usefixtures("allow_anonymous") @pytest.mark.usefixtures("allow_anonymous")
def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data): # noqa def test_anonymous_update_with_cookie_should_work(cookieclient, anonymap, post_data):
url = reverse("map_update", kwargs={"map_id": anonymap.pk}) url = reverse("map_update", kwargs={"map_id": anonymap.pk})
# POST only mendatory fields # POST only mendatory fields
name = "new map name" name = "new map name"
@ -420,7 +420,7 @@ def test_bad_anonymous_edit_url_should_return_403(cookieclient, anonymap):
@pytest.mark.usefixtures("allow_anonymous") @pytest.mark.usefixtures("allow_anonymous")
def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed( def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(
client, anonymap, user client, anonymap, user
): # noqa ):
assert Map.objects.count() == 1 assert Map.objects.count() == 1
url = reverse("map_clone", kwargs={"map_id": anonymap.pk}) url = reverse("map_clone", kwargs={"map_id": anonymap.pk})
anonymap.edit_status = anonymap.OWNER anonymap.edit_status = anonymap.OWNER
@ -434,7 +434,7 @@ def test_clone_anonymous_map_should_not_be_possible_if_user_is_not_allowed(
@pytest.mark.usefixtures("allow_anonymous") @pytest.mark.usefixtures("allow_anonymous")
def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap): # noqa def test_clone_map_should_be_possible_if_edit_status_is_anonymous(client, anonymap):
assert Map.objects.count() == 1 assert Map.objects.count() == 1
url = reverse("map_clone", kwargs={"map_id": anonymap.pk}) url = reverse("map_clone", kwargs={"map_id": anonymap.pk})
anonymap.edit_status = anonymap.ANONYMOUS anonymap.edit_status = anonymap.ANONYMOUS
@ -624,7 +624,7 @@ def test_download(client, map, datalayer):
"attribution": "© OSM Contributors", "attribution": "© OSM Contributors",
"maxZoom": 18, "maxZoom": 18,
"minZoom": 0, "minZoom": 0,
"url_template": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png", "url_template": "https://a.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
}, },
"tilelayersControl": True, "tilelayersControl": True,
"zoom": 7, "zoom": 7,
@ -675,3 +675,63 @@ def test_download_my_map(client, map, datalayer):
# Test response is a json # Test response is a json
j = json.loads(response.content.decode()) j = json.loads(response.content.decode())
assert j["type"] == "umap" assert j["type"] == "umap"
@pytest.mark.parametrize("share_status", [Map.PRIVATE, Map.BLOCKED, Map.OPEN])
def test_oembed_shared_status_map(client, map, datalayer, share_status):
map.share_status = share_status
map.save()
url = f"{reverse('map_oembed')}?url=http://testserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 403
def test_oembed_no_url_map(client, map, datalayer):
url = reverse("map_oembed")
response = client.get(url)
assert response.status_code == 404
def test_oembed_wrong_format_map(client, map, datalayer):
url = (
f"{reverse('map_oembed')}"
f"?url=http://testserver{map.get_absolute_url()}&format=xml"
)
response = client.get(url)
assert response.status_code == 501
def test_oembed_wrong_domain_map(client, map, datalayer):
url = f"{reverse('map_oembed')}?url=http://BADserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 404
def test_oembed_map(client, map, datalayer):
url = f"{reverse('map_oembed')}?url=http://testserver{map.get_absolute_url()}"
response = client.get(url)
assert response.status_code == 200
j = json.loads(response.content.decode())
assert j["type"] == "rich"
assert j["version"] == "1.0"
assert j["width"] == 800
assert j["height"] == 300
assert j["html"] == (
'<iframe width="100%" height="300px" frameborder="0" allowfullscreen '
f'allow="geolocation" src="//testserver/en/map/test-map_{map.id}"></iframe>'
f'<p><a href="//testserver/en/map/test-map_{map.id}">See full screen</a></p>'
)
def test_oembed_link(client, map, datalayer):
response = client.get(map.get_absolute_url())
assert response.status_code == 200
assert (
'<link rel="alternate" type="application/json+oembed"'
in response.content.decode()
)
assert (
'href="http://testserver/map/oembed/'
f'?url=http%3A//testserver/en/map/test-map_{map.id}&format=json"'
) in response.content.decode()
assert 'title="test map oEmbed URL" />' in response.content.decode()

View file

@ -1,6 +1,6 @@
import json import json
import socket import socket
from datetime import date, datetime, timedelta from datetime import datetime, timedelta
import pytest import pytest
from django.conf import settings from django.conf import settings
@ -10,6 +10,7 @@ from django.urls import reverse
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from umap import VERSION from umap import VERSION
from umap.models import Map, Star
from umap.views import validate_url from umap.views import validate_url
from .base import MapFactory, UserFactory from .base import MapFactory, UserFactory
@ -391,3 +392,51 @@ def test_webmanifest(client):
}, },
] ]
} }
@pytest.mark.django_db
def test_home_feed(client, settings, user, tilelayer):
settings.UMAP_HOME_FEED = "latest"
staff = UserFactory(username="Staff", is_staff=True)
starred = MapFactory(
owner=user, name="A public map starred by staff", share_status=Map.PUBLIC
)
MapFactory(
owner=user, name="A public map not starred by staff", share_status=Map.PUBLIC
)
non_staff = MapFactory(
owner=user, name="A public map starred by non staff", share_status=Map.PUBLIC
)
private = MapFactory(
owner=user, name="A private map starred by staff", share_status=Map.PRIVATE
)
reserved = MapFactory(
owner=user, name="A reserved map starred by staff", share_status=Map.OPEN
)
Star.objects.create(by=staff, map=starred)
Star.objects.create(by=staff, map=private)
Star.objects.create(by=staff, map=reserved)
Star.objects.create(by=user, map=non_staff)
response = client.get(reverse("home"))
content = response.content.decode()
assert "A public map starred by staff" in content
assert "A public map not starred by staff" in content
assert "A public map starred by non staff" in content
assert "A private map starred by staff" not in content
assert "A reserved map starred by staff" not in content
settings.UMAP_HOME_FEED = "highlighted"
response = client.get(reverse("home"))
content = response.content.decode()
assert "A public map starred by staff" in content
assert "A public map not starred by staff" not in content
assert "A public map starred by non staff" not in content
assert "A private map starred by staff" not in content
assert "A reserved map starred by staff" not in content
settings.UMAP_HOME_FEED = None
response = client.get(reverse("home"))
content = response.content.decode()
assert "A public map starred by staff" not in content
assert "A public map not starred by staff" not in content
assert "A public map starred by non staff" not in content
assert "A private map starred by staff" not in content
assert "A reserved map starred by staff" not in content

View file

@ -41,6 +41,7 @@ urlpatterns = [
), ),
re_path(r"^i18n/", include("django.conf.urls.i18n")), re_path(r"^i18n/", include("django.conf.urls.i18n")),
re_path(r"^agnocomplete/", include("agnocomplete.urls")), re_path(r"^agnocomplete/", include("agnocomplete.urls")),
re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
re_path( re_path(
r"^map/(?P<map_id>\d+)/download/", r"^map/(?P<map_id>\d+)/download/",
can_view_map(views.MapDownload.as_view()), can_view_map(views.MapDownload.as_view()),

View file

@ -18,20 +18,23 @@ from django.contrib.auth import logout as do_logout
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.contrib.postgres.search import SearchQuery, SearchVector from django.contrib.postgres.search import SearchQuery, SearchVector
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.core.signing import BadSignature, Signer from django.core.signing import BadSignature, Signer
from django.core.validators import URLValidator, ValidationError from django.core.validators import URLValidator, ValidationError
from django.http import ( from django.http import (
Http404,
HttpResponse, HttpResponse,
HttpResponseBadRequest, HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseForbidden,
HttpResponsePermanentRedirect, HttpResponsePermanentRedirect,
HttpResponseRedirect, HttpResponseRedirect,
HttpResponseServerError,
) )
from django.middleware.gzip import re_accepts_gzip from django.middleware.gzip import re_accepts_gzip
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import resolve, reverse, reverse_lazy
from django.utils.encoding import smart_bytes from django.utils.encoding import smart_bytes
from django.utils.http import http_date from django.utils.http import http_date
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
@ -118,13 +121,26 @@ class PublicMapsMixin(object):
maps = qs.order_by("-modified_at") maps = qs.order_by("-modified_at")
return maps return maps
def get_highlighted_maps(self):
staff = User.objects.filter(is_staff=True)
stars = Star.objects.filter(by__in=staff).values("map")
qs = Map.public.filter(pk__in=stars)
maps = qs.order_by("-modified_at")
return maps
class Home(PaginatorMixin, TemplateView, PublicMapsMixin): class Home(PaginatorMixin, TemplateView, PublicMapsMixin):
template_name = "umap/home.html" template_name = "umap/home.html"
list_template_name = "umap/map_list.html" list_template_name = "umap/map_list.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
if settings.UMAP_HOME_FEED is None:
maps = []
elif settings.UMAP_HOME_FEED == "highlighted":
maps = self.get_highlighted_maps()
else:
maps = self.get_public_maps() maps = self.get_public_maps()
maps = self.paginate(maps, settings.UMAP_MAPS_PER_PAGE)
demo_map = None demo_map = None
if hasattr(settings, "UMAP_DEMO_PK"): if hasattr(settings, "UMAP_DEMO_PK"):
@ -140,8 +156,6 @@ class Home(PaginatorMixin, TemplateView, PublicMapsMixin):
except Map.DoesNotExist: except Map.DoesNotExist:
pass pass
maps = self.paginate(maps, settings.UMAP_MAPS_PER_PAGE)
return { return {
"maps": maps, "maps": maps,
"demo_map": demo_map, "demo_map": demo_map,
@ -421,6 +435,21 @@ class MapDetailMixin:
model = Map model = Map
pk_url_kwarg = "map_id" pk_url_kwarg = "map_id"
def set_preconnect(self, properties, context):
# Try to extract the tilelayer domain, in order to but a preconnect meta.
url_template = properties.get("tilelayer", {}).get("url_template")
# Not explicit tilelayer set, take the first of the list, which will be
# used by frontend too.
if not url_template:
tilelayers = properties.get("tilelayers")
if tilelayers:
url_template = tilelayers[0].get("url_template")
if url_template:
domain = urlparse(url_template).netloc
# Do not try to preconnect on domains with variables
if domain and "{" not in domain:
context["preconnect_domains"] = [f"//{domain}"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = self.request.user user = self.request.user
@ -428,7 +457,7 @@ class MapDetailMixin:
"urls": _urls_for_js(), "urls": _urls_for_js(),
"tilelayers": TileLayer.get_list(), "tilelayers": TileLayer.get_list(),
"editMode": self.edit_mode, "editMode": self.edit_mode,
"default_iconUrl": "%sumap/img/marker.png" % settings.STATIC_URL, # noqa "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, # noqa
"umap_id": self.get_umap_id(), "umap_id": self.get_umap_id(),
"starred": self.is_starred(), "starred": self.is_starred(),
"licences": dict((l.name, l.json) for l in Licence.objects.all()), "licences": dict((l.name, l.json) for l in Licence.objects.all()),
@ -473,6 +502,7 @@ class MapDetailMixin:
map_settings["properties"].update(properties) map_settings["properties"].update(properties)
map_settings["properties"]["datalayers"] = self.get_datalayers() map_settings["properties"]["datalayers"] = self.get_datalayers()
context["map_settings"] = json.dumps(map_settings, indent=settings.DEBUG) context["map_settings"] = json.dumps(map_settings, indent=settings.DEBUG)
self.set_preconnect(map_settings["properties"], context)
return context return context
def get_datalayers(self): def get_datalayers(self):
@ -525,6 +555,16 @@ class PermissionsMixin:
class MapView(MapDetailMixin, PermissionsMixin, DetailView): class MapView(MapDetailMixin, PermissionsMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["oembed_absolute_uri"] = self.request.build_absolute_uri(
reverse("map_oembed")
)
context["absolute_uri"] = self.request.build_absolute_uri(
self.object.get_absolute_url()
)
return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
canonical = self.get_canonical_url() canonical = self.get_canonical_url()
@ -606,6 +646,52 @@ class MapDownload(DetailView):
return response return response
class MapOEmbed(View):
def get(self, request, *args, **kwargs):
data = {"type": "rich", "version": "1.0"}
format_ = request.GET.get("format", "json")
if format_ != "json":
response = HttpResponseServerError("Only `json` format is implemented.")
response.status_code = 501
return response
url = request.GET.get("url")
if not url:
raise Http404("Missing `url` parameter.")
parsed_url = urlparse(url)
netloc = parsed_url.netloc
allowed_hosts = settings.ALLOWED_HOSTS
if parsed_url.hostname not in allowed_hosts and allowed_hosts != ["*"]:
raise Http404("Host not allowed.")
url_path = parsed_url.path
view, args, kwargs = resolve(url_path)
if "slug" not in kwargs or "map_id" not in kwargs:
raise Http404("Invalid URL path.")
map_ = Map.objects.get(id=kwargs["map_id"], slug=kwargs["slug"])
if map_.share_status != Map.PUBLIC:
raise PermissionDenied("This map is not public.")
map_url = map_.get_absolute_url()
label = _("See full screen")
height = 300
data["height"] = height
width = 800
data["width"] = width
# TODISCUSS: do we keep width=100% by default for the iframe?
html = (
f'<iframe width="100%" height="{height}px" '
f'frameborder="0" allowfullscreen allow="geolocation" '
f'src="//{netloc}{map_url}"></iframe>'
f'<p><a href="//{netloc}{map_url}">{label}</a></p>'
)
data["html"] = html
return simple_json_response(**data)
class MapViewGeoJSON(MapView): class MapViewGeoJSON(MapView):
def get_canonical_url(self): def get_canonical_url(self):
return reverse("map_geojson", args=(self.object.pk,)) return reverse("map_geojson", args=(self.object.pk,))