Merge branch 'master' into dependabot/pip/django-5.0.1
This commit is contained in:
commit
5da9b67b2a
36 changed files with 2143 additions and 78 deletions
11
.eslintrc.json
Normal file
11
.eslintrc.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"plugins": ["compat"],
|
||||||
|
"extends": ["plugin:compat/recommended"],
|
||||||
|
"env": {
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module"
|
||||||
|
}
|
||||||
|
}
|
1
.github/workflows/test-docs.yml
vendored
1
.github/workflows/test-docs.yml
vendored
|
@ -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
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -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/ &&\
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
1715
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
4
umap/static/umap/img/marker.svg
Normal file
4
umap/static/umap/img/marker.svg
Normal 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 |
|
@ -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 },
|
||||||
|
|
|
@ -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, '')
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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') },
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)'],
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
):
|
):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
39
umap/tests/integration/test_querystring.py
Normal file
39
umap/tests/integration/test_querystring.py
Normal 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()
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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,))
|
||||||
|
|
Loading…
Reference in a new issue