Compare commits

...

18 commits

Author SHA1 Message Date
Yohan Boniface
76e8d7f185
Merge 5517e1f437 into be83eddbd0 2025-03-25 16:18:24 +01:00
Yohan Boniface
be83eddbd0
chore: bump ruff from 0.11.0 to 0.11.2 (#2587)
Some checks failed
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
2025-03-25 13:04:07 +01:00
dependabot[bot]
4df201107e
chore: bump ruff from 0.11.0 to 0.11.2
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.0 to 0.11.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.0...0.11.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 17:42:02 +00:00
Yohan Boniface
60f16cbc76
chore: remove old JS tests (#2585)
Some checks failed
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
Those are not run since more than one year, so let's trash.

fix #2260
2025-03-20 11:50:48 +01:00
Yohan Boniface
2fa88c36f8
chore: bump ruff from 0.9.10 to 0.11.0 (#2582)
Some checks are pending
Test & Docs / tests (postgresql, 3.10) (push) Waiting to run
Test & Docs / tests (postgresql, 3.12) (push) Waiting to run
Test & Docs / lint (push) Waiting to run
2025-03-19 08:22:32 +01:00
Yohan Boniface
47c5c0a2f0 chore: remove old JS tests
Those are not ran since more than one year, so let's trash.

fix #2260
2025-03-19 08:16:38 +01:00
dependabot[bot]
e548ec60f1
chore: bump ruff from 0.9.10 to 0.11.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.10 to 0.11.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.11.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 05:58:20 +00:00
Yohan Boniface
190acbfaf0
chore: bump mkdocs-material from 9.6.7 to 9.6.9 (#2580)
Some checks failed
Test & Docs / tests (postgresql, 3.10) (push) Has been cancelled
Test & Docs / tests (postgresql, 3.12) (push) Has been cancelled
Test & Docs / lint (push) Has been cancelled
2025-03-18 06:57:13 +01:00
Yohan Boniface
1370b1a0e8
chore: bump psycopg from 3.2.5 to 3.2.6 (#2581)
Some checks are pending
Test & Docs / tests (postgresql, 3.10) (push) Waiting to run
Test & Docs / tests (postgresql, 3.12) (push) Waiting to run
Test & Docs / lint (push) Waiting to run
2025-03-17 21:27:56 +01:00
dependabot[bot]
aa75b323c8
chore: bump psycopg from 3.2.5 to 3.2.6
Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.2.5 to 3.2.6.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.2.5...3.2.6)

---
updated-dependencies:
- dependency-name: psycopg
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 18:07:46 +00:00
dependabot[bot]
1c00545095
chore: bump mkdocs-material from 9.6.7 to 9.6.9
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.6.7 to 9.6.9.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.6.7...9.6.9)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 18:07:38 +00:00
Yohan Boniface
54a3aae912
chore: order importers by name instead of id (#2578) 2025-03-17 16:46:22 +01:00
Yohan Boniface
9d4069d9ae chore: order importers by name instead of id
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-17 16:44:23 +01:00
Yohan Boniface
167bab70c5
feat: add experimental BAN importer (#2565)
This importer takes a CSV as input, sends it to the BAN API, and then
paste it into the import textarea, so it can be imported as usual.



https://github.com/user-attachments/assets/c13f8580-5c09-4b35-b092-baac664a57a1
2025-03-17 16:39:55 +01:00
Yohan Boniface
d3ed46356d feat: add experimental BAN importer
This importer takes a CSV as input, sends it to the BAN API, and
then paste it into the import textarea, so it can be imported as usual.

Co-authored-by: David Larlet <david@larlet.fr>
2025-03-17 16:20:57 +01:00
Yohan Boniface
5517e1f437 feat: associate icons to tags
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-06 18:51:04 +01:00
Yohan Boniface
91b7b93bf4 feat: basic filtering by tags 2025-02-27 09:51:11 +01:00
Yohan Boniface
a7837aa54a feat: add Map.tags and allow to edit from client
cf #2283
2025-02-26 19:34:04 +01:00
52 changed files with 315 additions and 1813 deletions

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.14.3 pymdown-extensions==10.14.3
mkdocs-material==9.6.7 mkdocs-material==9.6.9
mkdocs-static-i18n==1.3.0 mkdocs-static-i18n==1.3.0

View file

@ -323,7 +323,10 @@ CREATE EXTENSION btree_gin;
ALTER TEXT SEARCH CONFIGURATION umapdict ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple; ALTER TEXT SEARCH CONFIGURATION umapdict ALTER MAPPING FOR hword, hword_part, word WITH unaccent, simple;
# Now create the index # Now create the index
CREATE INDEX IF NOT EXISTS search_idx ON umap_map USING GIN(to_tsvector('umapdict', COALESCE(name, ''::character varying)::text), share_status); CREATE INDEX IF NOT EXISTS search_idx ON umap_map USING GIN(to_tsvector('umapdict', COALESCE(name, ''::character varying)::text), share_status, tags);
# You should also create an index for tag filtering:
CREATE INDEX IF NOT EXISTS tags_idx ON umap_map USING GIN(share_status, tags);
``` ```
Then set: Then set:

View file

@ -1,5 +1,5 @@
# Force rtfd to use a recent version of mkdocs # Force rtfd to use a recent version of mkdocs
mkdocs==1.6.1 mkdocs==1.6.1
pymdown-extensions==10.14.3 pymdown-extensions==10.14.3
mkdocs-material==9.6.7 mkdocs-material==9.6.9
mkdocs-static-i18n==1.3.0 mkdocs-static-i18n==1.3.0

View file

@ -33,7 +33,7 @@ dependencies = [
"django-environ==0.12.0", "django-environ==0.12.0",
"django-probes==1.7.0", "django-probes==1.7.0",
"Pillow==11.1.0", "Pillow==11.1.0",
"psycopg==3.2.5", "psycopg==3.2.6",
"requests==2.32.3", "requests==2.32.3",
"rcssmin==1.2.1", "rcssmin==1.2.1",
"rjsmin==1.2.4", "rjsmin==1.2.4",
@ -44,10 +44,10 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"hatch==1.14.0", "hatch==1.14.0",
"ruff==0.9.10", "ruff==0.11.2",
"djlint==1.36.4", "djlint==1.36.4",
"mkdocs==1.6.1", "mkdocs==1.6.1",
"mkdocs-material==9.6.7", "mkdocs-material==9.6.9",
"mkdocs-static-i18n==1.3.0", "mkdocs-static-i18n==1.3.0",
"vermin==1.6.0", "vermin==1.6.0",
"pymdown-extensions==10.14.3", "pymdown-extensions==10.14.3",

View file

@ -14,6 +14,7 @@ def settings(request):
"UMAP_DEMO_SITE": djsettings.UMAP_DEMO_SITE, "UMAP_DEMO_SITE": djsettings.UMAP_DEMO_SITE,
"UMAP_HOST_INFOS": djsettings.UMAP_HOST_INFOS, "UMAP_HOST_INFOS": djsettings.UMAP_HOST_INFOS,
"UMAP_ALLOW_EDIT_PROFILE": djsettings.UMAP_ALLOW_EDIT_PROFILE, "UMAP_ALLOW_EDIT_PROFILE": djsettings.UMAP_ALLOW_EDIT_PROFILE,
"UMAP_TAGS": djsettings.UMAP_TAGS,
} }

View file

@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils.translation import gettext_lazy as _
from .models import DataLayer, Map, Team from .models import DataLayer, Map, Team
@ -92,7 +91,7 @@ class MapSettingsForm(forms.ModelForm):
return self.cleaned_data["center"] return self.cleaned_data["center"]
class Meta: class Meta:
fields = ("settings", "name", "center", "slug") fields = ("settings", "name", "center", "slug", "tags")
model = Map model = Map

View file

@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-02-26 16:18
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("umap", "0026_datalayer_modified_at_datalayer_share_status"),
]
operations = [
migrations.AddField(
model_name="map",
name="tags",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=200),
blank=True,
default=list,
size=None,
),
),
]

View file

@ -4,6 +4,7 @@ import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
from django.core.files.base import File from django.core.files.base import File
from django.core.files.storage import storages from django.core.files.storage import storages
from django.core.signing import Signer from django.core.signing import Signer
@ -236,6 +237,7 @@ class Map(NamedModel):
settings = models.JSONField( settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict blank=True, null=True, verbose_name=_("settings"), default=dict
) )
tags = ArrayField(models.CharField(max_length=200), blank=True, default=list)
objects = models.Manager() objects = models.Manager()
public = PublicManager() public = PublicManager()
@ -420,7 +422,8 @@ class Map(NamedModel):
return { return {
"iconUrl": { "iconUrl": {
"default": "%sumap/img/marker.svg" % settings.STATIC_URL, "default": "%sumap/img/marker.svg" % settings.STATIC_URL,
} },
"tags": {"choices": settings.UMAP_TAGS},
} }

View file

@ -6,6 +6,7 @@ from email.utils import parseaddr
import environ import environ
from django.conf.locale import LANG_INFO from django.conf.locale import LANG_INFO
from django.utils.translation import gettext_lazy as _
import umap as project_module import umap as project_module
@ -290,6 +291,25 @@ UMAP_HOME_FEED = "latest"
UMAP_IMPORTERS = {} UMAP_IMPORTERS = {}
UMAP_HOST_INFOS = {} UMAP_HOST_INFOS = {}
UMAP_LABEL_KEYS = ["name", "title"] UMAP_LABEL_KEYS = ["name", "title"]
UMAP_TAGS = (
("arts", _("Art and Culture")),
("cycling", _("Cycling")),
("business", _("Business")),
("environment", _("Environment")),
("education", _("Education")),
("food", _("Food and Agriculture")),
("geopolitics", _("Geopolitics")),
("health", _("Health")),
("hiking", _("Hiking")),
("history", _("History")),
("public", _("Public sector")),
("science", _("Science")),
("shopping", _("Shopping")),
("sport", _("Sport and Leisure")),
("travel", _("Travel")),
("transports", _("Transports")),
("tourism", _("Tourism")),
)
UMAP_READONLY = env("UMAP_READONLY", default=False) UMAP_READONLY = env("UMAP_READONLY", default=False)
UMAP_GZIP = True UMAP_GZIP = True

View file

@ -202,3 +202,15 @@ dt {
height: 100vh; height: 100vh;
opacity: 0.5; opacity: 0.5;
} }
.table-scrollable {
background-image: linear-gradient(to right, var(--background-color), var(--background-color)),
linear-gradient(to right, var(--background-color), var(--background-color)),
linear-gradient(to right, rgba(0, 0, 20, .50), rgba(255, 255, 255, 0)),
linear-gradient(to left, rgba(0, 0, 20, .50), rgba(255, 255, 255, 0));
background-position: left center, right center, left center, right center;
background-repeat: no-repeat;
background-size: 20px 100%, 20px 100%, 10px 100%, 10px 100%;
background-attachment: local, local, scroll, scroll;
display: block;
overflow-x: auto;
}

View file

@ -2,7 +2,7 @@
z-index: var(--zindex-dialog); z-index: var(--zindex-dialog);
margin: auto; margin: auto;
margin-top: 100px; margin-top: 100px;
width: 40vw; width: var(--dialog-width);
max-width: 100vw; max-width: 100vw;
max-height: 50vh; max-height: 50vh;
padding: 20px; padding: 20px;

View file

@ -55,3 +55,10 @@
.importers ul .datasets:before { .importers ul .datasets:before {
background-image: url(../img/importers/datasets.svg); background-image: url(../img/importers/datasets.svg);
} }
.importer.banfr h3:before,
.importers ul .banfr:before {
background-image: url(../img/importers/banfr.svg);
}
.importer table {
width: calc(var(--dialog-width) - 2 * var(--box-margin));
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="none"><path fill="#bebebe" d="M40 0H10C4.477 0 0 4.477 0 10v30c0 5.523 4.477 10 10 10h30c5.523 0 10-4.477 10-10V10c0-5.523-4.477-10-10-10z" style="stroke-width:1"/><path fill="#bfbfbf" fill-opacity=".9" fill-rule="evenodd" d="M29.023 20.012v6.053a1.226 1.226 0 0 0 2.451 0v-6.053a1.226 1.226 0 0 0-2.451 0z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#E1000F" fill-opacity=".9" fill-rule="evenodd" d="m24.393 18.047 5.242 3.027a1.226 1.226 0 0 0 1.225-2.123l-5.241-3.027a1.226 1.226 0 0 0-1.226 2.123z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#000091" fill-opacity=".9" fill-rule="evenodd" d="m29.635 25.004-5.242 3.026a1.226 1.226 0 0 0 1.226 2.123l5.241-3.027a1.226 1.226 0 0 0-1.225-2.122z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#bfbfbf" fill-opacity=".9" fill-rule="evenodd" d="M20.99 26.065v-6.053a1.226 1.226 0 0 0-2.451 0v6.053a1.226 1.226 0 0 0 2.451 0z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#000091" fill-opacity=".9" fill-rule="evenodd" d="m20.377 21.074 5.242-3.027a1.226 1.226 0 0 0-1.226-2.123l-5.241 3.027a1.226 1.226 0 0 0 1.225 2.123z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#E1000F" fill-opacity=".9" fill-rule="evenodd" d="m14.934 27.326 4.84-.035a1.226 1.226 0 0 0-.018-2.452l-4.84.035a1.226 1.226 0 0 0 .018 2.452z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#bfbfbf" fill-opacity=".9" fill-rule="evenodd" d="M18.62 14.22A9.977 9.977 0 0 1 25 11.924c2.424 0 4.648.864 6.38 2.298a1.226 1.226 0 0 0 1.564-1.888A12.417 12.417 0 0 0 25 9.471a12.417 12.417 0 0 0-7.944 2.862 1.226 1.226 0 0 0 1.564 1.888z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#000091" fill-opacity=".9" fill-rule="evenodd" d="M16.022 25.565c-.622-1.309-1.035-2.553-1.035-3.628a9.998 9.998 0 0 1 3.632-7.716 1.225 1.225 0 1 0-1.563-1.888 12.443 12.443 0 0 0-4.52 9.604c0 1.385.473 2.997 1.271 4.679a1.226 1.226 0 0 0 2.215-1.051z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#E1000F" fill-opacity=".9" fill-rule="evenodd" d="M31.38 14.221a9.997 9.997 0 0 1 3.633 7.716c0 1.682-.967 3.75-2.248 5.846-3.235 5.298-8.629 10.652-8.629 10.652a1.224 1.224 0 1 0 1.728 1.737s5.62-5.586 8.993-11.11c1.551-2.541 2.607-5.087 2.607-7.125 0-3.861-1.759-7.318-4.52-9.604a1.226 1.226 0 0 0-1.564 1.888z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/><path fill="#000091" fill-opacity=".9" fill-rule="evenodd" d="M25.864 38.435s-3.874-3.85-7.03-8.252a1.227 1.227 0 0 0-1.992 1.427c3.275 4.57 7.294 8.562 7.294 8.562.48.478 1.257.475 1.734-.006a1.224 1.224 0 0 0-.006-1.731z" clip-rule="evenodd" style="fill:#323737;fill-opacity:1;stroke-width:1"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M2 1S1 1 1 2v5.158C1 8.888 1.354 11 4.5 11H5V8L2.5 9s0-2.5 2.5-2.5V5c0-.708.087-1.32.5-1.775.381-.42 1.005-1.258 2.656-.471L9 3.303V2s0-1-1-1c-.708 0-1.978 1-3 1S2.787 1 2 1zm1 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 1S6 4 6 5v5c0 2 1 4 4 4s4-2 4-4V5c0-1-1-1-1-1-.708 0-1.978 1-3 1S7.787 4 7 4zm1 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm4 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm-4.5 4h5s0 2.5-2.5 2.5S7.5 10 7.5 10z"/></svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M4.5 4V2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2H12a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1.5Zm1-2v2h4V2h-4Z"/></svg>

After

Width:  |  Height:  |  Size: 205 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2zM8.145 2.994a.5.5 0 0 0-.348.143l-2.64 2.5a.5.5 0 0 0 .042.763L7 7.75v2.75c-.01.676 1.01.676 1 0v-3a.5.5 0 0 0-.2-.4l-.767-.577 1.818-1.72.749.998A.5.5 0 0 0 10 6h1.5c.676.01.676-1.01 0-1h-1.25L9.5 4l-.6-.8a.5.5 0 0 0-.384-.206h-.371zM3 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm9 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM3 8a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm9 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>

After

Width:  |  Height:  |  Size: 481 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M10 0v15H0V0h10zM9 1H1v13h8V1zM2.5 4h5c.5 0 .5 1 0 1h-5C2 5 2 4 2.5 4zm0 2h5c.5 0 .5 1 0 1h-5C2 7 2 6 2.5 6zm0 2h5c.5 0 .5 1 0 1h-5C2 9 2 8 2.5 8zm0 2h5c.5 0 .5 1 0 1h-5c-.5 0-.5-1 0-1zM11 13c.5.5 2.5.5 3 0 0 0-1 2-1.5 2S11 13 11 13zm0-10c0 .5 3 .5 3 0v9c0 .5-3 .5-3 0V3zm1.5-3C11 0 11 .5 11 1v1c0 .5 3 .5 3 0V1c0-.5 0-1-1.5-1z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M2.456 8.613c-.338.598-.955 1.69.137 2.418.343.227.728.384 1.131.462.307.045.323.518-.038.507-.385-.02-2.26-.193-2.561-1.6-.156-.82.02-1.557.504-2.355l.697-1.233-1.306-.743L4.5 4v4l-1.306-.694-.738 1.307zM6.7 2.034c1.155-.628 1.823.43 2.191 1.007l.806 1.263-1.266.808L12 6.986l-.197-4.026-1.264.807-.76-1.189c-.522-.746-.904-1.297-1.835-1.545C6.307.72 5.301 2.619 5.311 2.607c-.164.287.216.54.451.21.258-.32.577-.586.938-.783zm6.594 6.187c-.088-.19-.549-.141-.419.267.131.39.184.8.157 1.21C12.939 11.01 11.684 11 11 11H9.5V9.5l-3.5 2 3.488 2.025L9.493 12H11c.89.015 1.6-.176 2.2-.713 1.2-1.061.094-3.066.094-3.066z"/></svg>

After

Width:  |  Height:  |  Size: 695 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="m3.5 0-1 5.5c-.146.805 1.782 1.181 1.75 2L4 14c-.038 1 1 1 1 1s1.038 0 1-1l-.25-6.5c-.031-.818 1.733-1.18 1.75-2L6.5 0H6l.25 4-.75.5L5.25 0h-.5L4.5 4.5 3.75 4 4 0h-.5zM12 0c-.736 0-1.964.655-2.455 1.637C9.135 2.373 9 4.018 9 5v2.5c0 .818 1.09 1 1.5 1L10 14c-.09.996 1 1 1 1s1 0 1-1V0z"/></svg>

After

Width:  |  Height:  |  Size: 365 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M6.65 2C5.43 2 4.48 3.38 4.11 3.82a.49.49 0 0 0-.11.32v4.4a.44.44 0 0 0 .72.36 3 3 0 0 1 1.93-1.17C8.06 7.73 8.6 9 10.07 9a5.28 5.28 0 0 0 2.73-1.09.49.49 0 0 0 .2-.4V2.45a.44.44 0 0 0-.62-.45 5.75 5.75 0 0 1-2.31 1.06C8.6 3.08 8.12 2 6.65 2zM2.5 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM3 4v9.48a.5.5 0 0 1-1 0V4a.5.5 0 0 1 1 0z"/></svg>

After

Width:  |  Height:  |  Size: 400 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M7 1c-.6 0-1 .4-1 1v4H2c-.6 0-1 .4-1 1v1c0 .6.4 1 1 1h4v4c0 .6.4 1 1 1h1c.6 0 1-.4 1-1V9h4c.6 0 1-.4 1-1V7c0-.6-.4-1-1-1H9V2c0-.6-.4-1-1-1H7z"/></svg>

After

Width:  |  Height:  |  Size: 222 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" baseProfile="tiny" overflow="inherit" version="1.2" viewBox="0 0 50 50"><path d="M6.43 21.55c-.22.45-.37.94-.46 1.47-.04.26-.06.53-.06.79l.19 12.13-4.2 9.02c-.14.27-.25.58-.3.9-.23 1.49.8 2.88 2.3 3.11 1.16.18 2.27-.41 2.8-1.39l4.63-9.86c.1-.23.18-.47.22-.73l.03-.37-.04-7.5 7.11 3.09 1.15 7.33a2.732 2.732 0 0 0 2.26 2.11c1.5.22 2.89-.81 3.12-2.28.04-.25.04-.51.01-.75l-1.4-8.84c-.17-.85-.74-1.59-1.53-1.96l-6.35-2.81L19.96 18l2.01 2.54c.21.23.47.42.77.54l7.65 2.23a2.14 2.14 0 0 0 2.45-1.29 2.14 2.14 0 0 0-1.18-2.8l-.11-.04-6.64-1.95-5-5.98A5.079 5.079 0 0 0 17 9.71c-2.03-.3-3.96.66-4.97 2.3l-5.56 9.57zm21.94 17.38-.48 3.63-13.16 3.19.13 2.25h32.09c1.14 0 2.06-.91 2.06-2.04l-.04-43.07-4.4-1.02-2.53 11.32-4.22 1.77-3.74 10.44 3.56 7.99-1 3.07-8.26 2.48zM19.44 9.15c2.26 0 4.1-1.83 4.1-4.08S21.7.99 19.45.99s-4.1 1.83-4.1 4.08 1.84 4.08 4.1 4.08zm-8.15.64c.31-.55.13-1.27-.43-1.59L8.88 7.05c-.57-.31-1.28-.13-1.61.43l-6.11 10.5c-.31.55-.13 1.26.43 1.59l1.99 1.14c.56.32 1.27.13 1.59-.42l6.11-10.5z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M7.5 0 1 3.5V4h13v-.5L7.5 0zM2 5v5l-1 1.6V13h13v-1.4L13 10V5H2zm2 1h1v5.5H4V6zm3 0h1v5.5H7V6zm3 0h1v5.5h-1V6z"/></svg>

After

Width:  |  Height:  |  Size: 190 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9 14v-3H6v3H1V6.5c0-.28.22-.5.5-.5H4v-.5c0-.28.22-.5.5-.5H7V1l2-1 2 1 2-1v3l-2 1-2-1-1 .5V5h2.5c.28 0 .5.22.5.5V6h2.5c.28 0 .5.22.5.5V14H9Zm3-6v2h1V8h-1Zm-2 0v2h1V8h-1Zm0 3v2h1v-2h-1Zm2 0v2h1v-2h-1Zm-8 0v2h1v-2H4Zm-2 0v2h1v-2H2Zm6-3v2h1V8H8ZM6 8v2h1V8H6ZM4 8v2h1V8H4ZM2 8v2h1V8H2Z"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="m8.34 6.15 3.62 8.15a.501.501 0 1 1-.92.4L9.84 12H5.16l-1.2 2.7a.501.501 0 1 1-.92-.4l3.19-7.17-1.74.81-1.27-2.71 6.79-3.17 1.27 2.71-2.94 1.38Zm-.84.58L5.6 11h3.8L7.5 6.73Zm2.76-5.34L12.98.12l1.69 3.63-2.72 1.27-1.69-3.63Zm-10 5.77 2.72-1.27.84 1.81L1.1 8.97.26 7.16Z"/></svg>

After

Width:  |  Height:  |  Size: 346 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M13.33 5H11.5l-.39-2.33A2 2 0 0 0 9.7 1.18 3.76 3.76 0 0 0 8.62 1H6.38a3.76 3.76 0 0 0-1.08.18 2 2 0 0 0-1.41 1.49L3.5 5H1.67a.5.5 0 0 0-.48.65l1.88 6.3A1.5 1.5 0 0 0 4.5 13h6a1.5 1.5 0 0 0 1.42-1.05l1.88-6.3a.5.5 0 0 0-.47-.65zM4.52 5l.36-2.17a.91.91 0 0 1 .74-.7c.246-.078.502-.121.76-.13h2.24c.261.008.52.051.77.13a.91.91 0 0 1 .74.7L10.48 5h-6z"/></svg>

After

Width:  |  Height:  |  Size: 429 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M11.968 10.227a3.812 3.812 0 0 1-1.913.984L3.768 4.934a4.028 4.028 0 0 1 1.005-1.902C7.03.774 9.98.224 12.378 2.622s1.848 5.348-.41 7.605Zm-6.987 1.61a3.842 3.842 0 0 1 1.168-.559A4.533 4.533 0 0 1 8 11.445L3.546 7a4.413 4.413 0 0 1 .157 1.922 3.664 3.664 0 0 1-.521 1.116C2.11 11.301 1.05 11.765 1.05 12.226a1.838 1.838 0 0 0 1.724 1.724c.46 0 .918-1.013 2.207-2.113Z"/></svg>

After

Width:  |  Height:  |  Size: 449 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="m5.36 1.67-.01 4.02a4.452 4.452 0 0 0-1.1-.11c-.37.1-.74.63-1.1.76a4.202 4.202 0 0 1 2.21-4.67Zm2.41-.64L9.8 4.48a3.183 3.183 0 0 1 .84-.61c.36-.1.94.17 1.34.11a4.202 4.202 0 0 0-4.21-2.95ZM1 13h13c-.66-.66-2.64-1.11-4.34-1.33l-1.87-7c.52-.05 1.15.03 1.53 0l-2.11-3.6H7.2a6.174 6.174 0 0 0-.7.14 4.38 4.38 0 0 0-.64.22l-.01 4.15c.35-.17.84-.54 1.3-.74l1.8 6.74c-.58-.05-1.09-.08-1.45-.08C6.03 11.5 2 12 1 13Z"/></svg>

After

Width:  |  Height:  |  Size: 489 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M8 1v1h1.5l1.41 1.41c.38.38.59.89.59 1.42V11c0 1.1-.9 2-2 2h-4c-1.1 0-2-.9-2-2V4.83c0-.53.21-1.04.59-1.42L5.5 2H7V1H5.5C5.22 1 5 .78 5 .5s.22-.5.5-.5h4c.28 0 .5.22.5.5s-.22.5-.5.5H8ZM6.25 13.5 5.5 15H4l.75-1.5h1.5Zm4 0L11 15H9.5l-.75-1.5h1.5ZM8.5 12h1c.55 0 1-.45 1-1v-1c-1.1 0-2 .9-2 2Zm-2 0c0-1.1-.9-2-2-2v1c0 .55.45 1 1 1h1Zm-2-6.5v3c0 .28.22.5.5.5h5c.28 0 .5-.22.5-.5v-3c0-.28-.22-.5-.5-.5H5c-.28 0-.5.22-.5.5Zm1-2c0 .28.22.5.5.5h3c.28 0 .5-.22.5-.5S9.28 3 9 3H6c-.28 0-.5.22-.5.5Z"/></svg>

After

Width:  |  Height:  |  Size: 563 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M3.98 36h12.04a.98.98 0 0 0 .98-.98V17a.98.98 0 0 0-1.8-.54L3.17 34.48A.98.98 0 0 0 3.98 36ZM20 36h23.99A1 1 0 0 0 45 34.98C44.58 16.6 32.84.76 20.03 0A.99.99 0 0 0 19 1v34a1 1 0 0 0 1 1ZM46 39H2a2 2 0 0 0-2 2 7 7 0 0 0 7 7h34a7 7 0 0 0 7-7 2 2 0 0 0-2-2Z"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View file

@ -141,6 +141,7 @@ export class MutatingForm extends Form {
facetKey: 'PropertyInput', facetKey: 'PropertyInput',
slugKey: 'PropertyInput', slugKey: 'PropertyInput',
labelKey: 'PropertyInput', labelKey: 'PropertyInput',
tags: 'TagsEditor',
} }
for (const [key, defaults] of Object.entries(SCHEMA)) { for (const [key, defaults] of Object.entries(SCHEMA)) {
const properties = Object.assign({}, defaults) const properties = Object.assign({}, defaults)
@ -152,6 +153,8 @@ export class MutatingForm extends Form {
} else if (properties.type === Number) { } else if (properties.type === Number) {
if (properties.step) properties.handler = 'Range' if (properties.step) properties.handler = 'Range'
else properties.handler = 'IntInput' else properties.handler = 'IntInput'
} else if (properties.type === Array) {
properties.handler = 'CheckBoxes'
} else if (properties.choices) { } else if (properties.choices) {
const text_length = properties.choices.reduce( const text_length = properties.choices.reduce(
(acc, [_, label]) => acc + label.length, (acc, [_, label]) => acc + label.length,

View file

@ -324,6 +324,41 @@ Fields.CheckBox = class extends BaseElement {
} }
} }
Fields.CheckBoxes = class extends BaseElement {
getInputTemplate(value, label) {
return `<label><input type=checkbox value="${value}" name="${this.name}" data-ref=input />${label}</label>`
}
build() {
const initial = this.get() || []
for (const [value, label] of this.properties.choices) {
const [root, { input }] = Utils.loadTemplateWithRefs(
this.getInputTemplate(value, label)
)
this.container.appendChild(root)
input.checked = initial.includes(value)
input.addEventListener('change', () => this.sync())
}
super.build()
}
value() {
return Array.from(this.root.querySelectorAll('input:checked')).map((el) => el.value)
}
}
Fields.TagsEditor = class extends Fields.CheckBoxes {
getInputTemplate(value, label) {
const path = SCHEMA.iconUrl.default.replace('marker.svg', `tags/${value}.svg`)
return `
<label>
<input type=checkbox value="${value}" name="${this.name}" data-ref=input />
<img class="tag-icon" src="${path}" alt="" /> ${label}
</label>
`
}
}
Fields.Select = class extends BaseElement { Fields.Select = class extends BaseElement {
getTemplate() { getTemplate() {
return `<select name="${this.name}" data-ref=select></select>` return `<select name="${this.name}" data-ref=select></select>`
@ -1296,12 +1331,13 @@ Fields.ManageEditors = class extends BaseElement {
placeholder: translate("Type editor's username"), placeholder: translate("Type editor's username"),
} }
this.autocomplete = new AjaxAutocompleteMultiple(this.container, options) this.autocomplete = new AjaxAutocompleteMultiple(this.container, options)
this._values = this.toHTML() this._values = this.toHTML() || []
if (this._values) if (this._values) {
for (let i = 0; i < this._values.length; i++) for (let i = 0; i < this._values.length; i++)
this.autocomplete.displaySelected({ this.autocomplete.displaySelected({
item: { value: this._values[i].id, label: this._values[i].name }, item: { value: this._values[i].id, label: this._values[i].name },
}) })
}
} }
value() { value() {

View file

@ -94,6 +94,9 @@ export default class Importer extends Utils.WithTemplate {
case 'datasets': case 'datasets':
import('./importers/datasets.js').then(register) import('./importers/datasets.js').then(register)
break break
case 'banfr':
import('./importers/banfr.js').then(register)
break
} }
} }
} }
@ -173,7 +176,7 @@ export default class Importer extends Utils.WithTemplate {
showImporters() { showImporters() {
if (!this.IMPORTERS.length) return if (!this.IMPORTERS.length) return
const [element, { grid }] = Utils.loadTemplateWithRefs(GRID_TEMPLATE) const [element, { grid }] = Utils.loadTemplateWithRefs(GRID_TEMPLATE)
for (const plugin of this.IMPORTERS.sort((a, b) => (a.id > b.id ? 1 : -1))) { for (const plugin of this.IMPORTERS.sort((a, b) => (a.name > b.name ? 1 : -1))) {
const button = Utils.loadTemplate( const button = Utils.loadTemplate(
`<li><button type="button" class="${plugin.id}">${plugin.name}</button></li>` `<li><button type="button" class="${plugin.id}">${plugin.name}</button></li>`
) )

View file

@ -0,0 +1,93 @@
import { DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
import { BaseAjax, SingleMixin } from '../autocomplete.js'
import * as Utils from '../utils.js'
import { AutocompleteCommunes } from './communesfr.js'
import { translate } from '../i18n.js'
import { uMapAlert as Alert } from '../../components/alerts/alert.js'
const TEMPLATE = `
<div>
<h3>Géocodage dadresses en France</h3>
<p>Géocoder un fichier CSV avec la base adresse nationale.</p>
<fieldset class="formbox">
<legend>Choisir un fichier CSV (encodé en UTF-8)</legend>
<input type=file name=file data-ref=csvFile accept=".csv" />
</fieldset>
<fieldset class="formbox">
<legend>Aperçu des données</legend>
<table class="table-scrollable" data-ref=table></table>
</fieldset>
<fieldset class="formbox">
<legend>Sélectionner les colonnes à utiliser</legend>
<span data-ref="columns"></span>
</fieldset>
</div>
`
export class Importer {
constructor(umap, options) {
this._umap = umap
this.name = options.name || 'Géocodage FR'
this.id = 'banfr'
}
async open(importer) {
let data
const [container, { table, columns, csvFile }] =
Utils.loadTemplateWithRefs(TEMPLATE)
csvFile.addEventListener('change', () => {
const reader = new FileReader()
reader.onload = (evt) => {
data = evt.target.result
const rows = csv2geojson.auto(data).slice(0, 5)
const cols = Object.keys(rows[0])
table.innerHTML = ''
columns.innerHTML = ''
const tr = document.createElement('tr')
for (const column of cols) {
tr.appendChild(Utils.loadTemplate(`<th>${column}</th>`))
columns.appendChild(
Utils.loadTemplate(
`<label><input type="checkbox" value="${column}" /> ${column}</label>`
)
)
}
table.appendChild(tr)
for (const row of rows) {
const tr = document.createElement('tr')
for (const column of cols) {
tr.appendChild(Utils.loadTemplate(`<td>${row[column]}</td>`))
}
table.appendChild(tr)
}
}
reader.readAsText(csvFile.files[0])
})
const confirm = async (form) => {
const formData = new FormData()
formData.append('data', csvFile.files[0])
for (const option of columns.querySelectorAll('input:checked')) {
formData.append('columns', option.value)
}
const response = await this._umap.request.post(
'https://api-adresse.data.gouv.fr/search/csv/',
{},
formData
)
if (response?.ok) {
importer.raw = await response.text()
importer.format = 'csv'
}
}
importer.dialog
.open({
template: container,
className: `${this.id} importer dark`,
cancel: false,
accept: translate('Geocode'),
})
.then(confirm)
}
}

View file

@ -500,6 +500,9 @@ export const SCHEMA = {
helpEntries: ['sync'], helpEntries: ['sync'],
default: false, default: false,
}, },
tags: {
type: Array,
},
tilelayer: { tilelayer: {
type: Object, type: Object,
impacts: ['background'], impacts: ['background'],

View file

@ -757,6 +757,12 @@ export default class Umap extends ServerStored {
const form = builder.build() const form = builder.build()
container.appendChild(form) container.appendChild(form)
const tags = DomUtil.createFieldset(container, translate('Tags'))
const tagsFields = ['properties.tags']
const tagsBuilder = new MutatingForm(this, tagsFields, {
umap: this,
})
tags.appendChild(tagsBuilder.build())
const credits = DomUtil.createFieldset(container, translate('Credits')) const credits = DomUtil.createFieldset(container, translate('Credits'))
const creditsFields = [ const creditsFields = [
'properties.licence', 'properties.licence',
@ -1185,6 +1191,7 @@ export default class Umap extends ServerStored {
const formData = new FormData() const formData = new FormData()
formData.append('name', this.properties.name) formData.append('name', this.properties.name)
formData.append('center', JSON.stringify(this.geometry())) formData.append('center', JSON.stringify(this.geometry()))
formData.append('tags', this.properties.tags || [])
formData.append('settings', JSON.stringify(geojson)) formData.append('settings', JSON.stringify(geojson))
const uri = this.urls.get('map_save', { map_id: this.id }) const uri = this.urls.get('map_save', { map_id: this.id })
const [data, _, error] = await this.server.post(uri, {}, formData) const [data, _, error] = await this.server.post(uri, {}, formData)

View file

@ -955,6 +955,20 @@ a.umap-control-caption,
display: block; display: block;
} }
/* **** */
/* Tags */
/* **** */
.tag-icon {
width: 20px;
height: 20px;
margin-bottom: -4px;
margin-right: 3px;
}
.dark .tag-icon {
filter: invert(1);
}
/* *************************** */ /* *************************** */
/* Overriding leaflet defaults */ /* Overriding leaflet defaults */
/* *************************** */ /* *************************** */

View file

@ -1,21 +0,0 @@
{
"globals": {
"describe": true,
"happen": true,
"assert": true,
"before": true,
"after": true,
"it": true,
"sinon": true,
"enableEdit": true,
"disableEdit": true,
"changeInputValue": true,
"resetMap": true,
"initMap": true,
"clickCancel": true,
"map": true,
"qs": true,
"qsa": true,
"qst": true
}
}

View file

@ -1,463 +0,0 @@
describe('U.DataLayer', () => {
let path = '/map/99/datalayer/update/62/',
map,
datalayer
before(async () => {
fetchMock.mock(/\/datalayer\/62\/\?.*/, JSON.stringify(RESPONSES.datalayer62_GET))
fetchMock.sticky('/map/99/update/settings/', { id: 99 })
this.options = {
umap_id: 99,
}
MAP = map = initMap({ umap_id: 99 })
const datalayer_options = defaultDatalayerData()
await map.initDataLayers([datalayer_options])
datalayer = map.getDataLayerByUmapId(62)
enableEdit()
})
after(() => {
fetchMock.restore()
resetMap()
})
describe('#init()', () => {
it('should be added in datalayers index', () => {
assert.notEqual(map.datalayers_index.indexOf(datalayer), -1)
})
})
describe('#edit()', () => {
var editButton, form, input, forceButton
it('row in control should be active', () => {
assert.notOk(
qs('.leaflet-control-browse #browse_data_toggle_' + L.stamp(datalayer) + '.off')
)
})
it('should have edit button', () => {
editButton = qs('#browse_data_toggle_' + L.stamp(datalayer) + ' .layer-edit')
assert.ok(editButton)
})
it('should have toggle visibility element', () => {
assert.ok(qs('.leaflet-control-browse i.layer-toggle'))
})
it('should exist only one datalayer', () => {
assert.equal(qsa('.leaflet-control-browse i.layer-toggle').length, 1)
})
it('should build a form on edit button click', () => {
happen.click(editButton)
form = qs('form.umap-form')
input = qs('form.umap-form input[name="name"]')
assert.ok(form)
assert.ok(input)
})
it('should update name on input change', () => {
var new_name = 'This is a new name'
input.value = new_name
happen.once(input, { type: 'input' })
assert.equal(datalayer.options.name, new_name)
})
it('should have made datalayer dirty', () => {
assert.ok(datalayer.isDirty)
assert.notEqual(map.dirty_datalayers.indexOf(datalayer), -1)
})
it('should have made Map dirty', () => {
assert.ok(map.isDirty)
})
it('should call datalayer.save on save button click', (done) => {
const postDatalayer = fetchMock.post(path, () => {
return defaultDatalayerData()
})
clickSave()
window.setTimeout(() => {
assert(fetchMock.called(path))
done()
}, 500)
})
it('should show alert if server respond 412', (done) => {
cleanAlert()
fetchMock.restore()
fetchMock.post(path, 412)
happen.click(editButton)
input = qs('form.umap-form input[name="name"]')
input.value = 'a new name'
happen.once(input, { type: 'input' })
clickSave()
window.setTimeout(() => {
assert(L.DomUtil.hasClass(map._container, 'umap-alert'))
assert.notEqual(map.dirty_datalayers.indexOf(datalayer), -1)
const forceButton = qs('#umap-alert-container .umap-action')
assert.ok(forceButton)
done()
}, 500)
})
it('should save anyway on force save button click', (done) => {
const forceButton = qs('#umap-alert-container .umap-action')
fetchMock.restore()
fetchMock.post(path, defaultDatalayerData)
happen.click(forceButton)
window.setTimeout(() => {
assert.notOk(qs('#umap-alert-container .umap-action'))
assert(fetchMock.called(path))
assert.equal(map.dirty_datalayers.indexOf(datalayer), -1)
done()
}, 500)
})
})
describe('#save() new', () => {
let newLayerButton, form, input, newDatalayer, editButton, manageButton
it('should have a manage datalayers action', () => {
enableEdit()
manageButton = qs('.manage-datalayers')
assert.ok(manageButton)
happen.click(manageButton)
})
it('should have a new layer button', () => {
newLayerButton = qs('.panel.right.on .add-datalayer')
assert.ok(newLayerButton)
})
it('should build a form on new layer button click', () => {
happen.click(newLayerButton)
form = qs('form.umap-form')
input = qs('form.umap-form input[name="name"]')
assert.ok(form)
assert.ok(input)
})
it('should have an empty name', () => {
assert.notOk(input.value)
})
it('should have created a new datalayer', () => {
assert.equal(map.datalayers_index.length, 2)
newDatalayer = map.datalayers_index[1]
})
it('should have made Map dirty', () => {
assert.ok(map.isDirty)
})
it('should update name on input change', () => {
var new_name = 'This is a new name'
input.value = new_name
happen.once(input, { type: 'input' })
assert.equal(newDatalayer.options.name, new_name)
})
it('should set umap_id on save callback', async () => {
assert.notOk(newDatalayer.umap_id)
fetchMock.post('/map/99/datalayer/create/', defaultDatalayerData({ id: 63 }))
clickSave()
return new Promise((resolve) => {
window.setTimeout(() => {
assert.equal(newDatalayer.umap_id, 63)
resolve()
}, 1000)
})
})
it('should have unset map dirty', () => {
assert.notOk(map.isDirty)
})
it('should have edit button', () => {
editButton = qs('#browse_data_toggle_' + L.stamp(newDatalayer) + ' .layer-edit')
assert.ok(editButton)
})
it('should call update if we edit again', async () => {
happen.click(editButton)
assert.notOk(map.isDirty)
input = qs('form.umap-form input[name="name"]')
input.value = "a new name again but we don't care which"
happen.once(input, { type: 'input' })
assert.ok(map.isDirty)
var response = () => {
return defaultDatalayerData({ pk: 63 })
}
var spy = sinon.spy(response)
fetchMock.post('/map/99/datalayer/update/63/', spy)
return new Promise((resolve) => {
clickSave()
window.setTimeout(() => {
assert.ok(spy.calledOnce)
resolve()
}, 1000)
})
})
})
describe('#iconClassChange()', () => {
it('should change icon class', () => {
happen.click(qs('[data-id="' + datalayer._leaflet_id + '"] .layer-edit'))
changeSelectValue(
qs('form#datalayer-advanced-properties select[name=iconClass]'),
'Circle'
)
assert.notOk(qs('div.umap-div-icon'))
assert.ok(qs('div.umap-circle-icon'))
happen.click(
qs('form#datalayer-advanced-properties .umap-field-iconClass .undefine')
)
assert.notOk(qs('div.umap-circle-icon'))
assert.ok(qs('div.umap-div-icon'))
clickCancel()
})
})
describe('#show/hide', () => {
it('should hide features on hide', () => {
assert.ok(qs('div.umap-div-icon'))
assert.ok(qs('path[fill="none"]'))
datalayer.hide()
assert.notOk(qs('div.umap-div-icon'))
assert.notOk(qs('path[fill="none"]'))
})
it('should show features on show', () => {
assert.notOk(qs('div.umap-div-icon'))
assert.notOk(qs('path[fill="none"]'))
datalayer.show()
assert.ok(qs('div.umap-div-icon'))
assert.ok(qs('path[fill="none"]'))
})
})
describe('#clone()', () => {
it('should clone everything but the id and the name', () => {
enableEdit()
var clone = datalayer.clone()
assert.notOk(clone.umap_id)
assert.notEqual(clone.options.name, datalayer.name)
assert.ok(clone.options.name)
assert.equal(clone.options.color, datalayer.options.color)
assert.equal(clone.options.stroke, datalayer.options.stroke)
clone._delete()
clickSave()
})
})
describe('#restore()', () => {
var oldConfirm,
newConfirm = () => {
return true
}
before(() => {
oldConfirm = window.confirm
window.confirm = newConfirm
})
after(() => {
window.confirm = oldConfirm
})
it('should restore everything', (done) => {
enableEdit()
var geojson = L.Util.CopyJSON(RESPONSES.datalayer62_GET)
geojson.features.push({
geometry: {
type: 'Point',
coordinates: [-1.274658203125, 50.57634993749885],
},
type: 'Feature',
id: 1807,
properties: { _umap_options: {}, name: 'new point from restore' },
})
geojson._umap_options.color = 'Chocolate'
fetchMock.get('/datalayer/62/olderversion.geojson', geojson)
sinon.spy(window, 'confirm')
datalayer.restore('olderversion.geojson')
window.setTimeout(() => {
assert(window.confirm.calledOnce)
window.confirm.restore()
assert.equal(datalayer.umap_id, 62)
assert.ok(datalayer.isDirty)
assert.equal(datalayer._index.length, 4)
assert.ok(qs('path[fill="Chocolate"]'))
done()
}, 1000)
})
it('should revert anything on cancel click', () => {
clickCancel()
assert.equal(datalayer._index.length, 3)
assert.notOk(qs('path[fill="Chocolate"]'))
})
})
describe('#smart-options()', () => {
let poly, marker
before(() => {
datalayer.eachLayer(function (layer) {
if (!poly && layer instanceof L.Polygon) {
poly = layer
}
if (!marker && layer instanceof L.Marker) {
marker = layer
}
})
})
it('should parse color variable', () => {
let icon = qs('div.umap-div-icon .icon_container')
poly.properties.mycolor = 'DarkGoldenRod'
marker.properties.mycolor = 'DarkRed'
marker.properties._umap_options.color = undefined
assert.notOk(qs('path[fill="DarkGoldenRod"]'))
assert.equal(icon.style.backgroundColor, 'olivedrab')
datalayer.options.color = '{mycolor}'
datalayer.options.fillColor = '{mycolor}'
datalayer.indexProperties(poly)
datalayer.indexProperties(marker)
datalayer.redraw()
icon = qs('div.umap-div-icon .icon_container')
assert.equal(icon.style.backgroundColor, 'darkred')
assert.ok(qs('path[fill="DarkGoldenRod"]'))
})
})
describe('#facet-search()', () => {
before(async () => {
fetchMock.get(/\/datalayer\/63\/\?.*/, RESPONSES.datalayer63_GET)
map.options.facetKey = 'name'
await map.initDataLayers([RESPONSES.datalayer63_GET._umap_options])
})
it('should not impact non browsable layer', () => {
assert.ok(qs('path[fill="SteelBlue"]'))
})
it('should allow advanced filter', () => {
map.openFacet()
assert.ok(qs('div.umap-facet-search'))
// This one if from the normal datalayer
// it's name is "test", so it should be hidden
// by the filter
assert.ok(qs('path[fill="none"]'))
happen.click(qs('input[data-value="name poly"]'))
assert.notOk(qs('path[fill="none"]'))
// This one comes from a non browsable layer
// so it should not be affected by the filter
assert.ok(qs('path[fill="SteelBlue"]'))
happen.click(qs('input[data-value="name poly"]')) // Undo
})
it('should allow to control facet label', () => {
map.options.facetKey = 'name|Nom'
map.openFacet()
assert.ok(qs('div.umap-facet-search h5'))
assert.equal(qs('div.umap-facet-search h5').textContent, 'Nom')
})
})
describe('#zoomEnd', () => {
it('should honour the fromZoom option', () => {
map.setZoom(6, { animate: false })
assert.ok(qs('path[fill="none"]'))
datalayer.options.fromZoom = 6
map.setZoom(5, { animate: false })
assert.notOk(qs('path[fill="none"]'))
map.setZoom(6, { animate: false })
assert.ok(qs('path[fill="none"]'))
})
it('should honour the toZoom option', () => {
map.setZoom(6, { animate: false })
assert.ok(qs('path[fill="none"]'))
datalayer.options.toZoom = 6
map.setZoom(7, { animate: false })
assert.notOk(qs('path[fill="none"]'))
map.setZoom(6, { animate: false })
assert.ok(qs('path[fill="none"]'))
})
})
describe('#displayOnLoad', () => {
before(() => {
fetchMock.get(/\/datalayer\/64\/\?.*/, RESPONSES.datalayer64_GET)
})
beforeEach(async () => {
await map.initDataLayers([RESPONSES.datalayer64_GET._umap_options])
datalayer = map.getDataLayerByUmapId(64)
map.setZoom(10, { animate: false })
})
afterEach(() => {
datalayer._delete()
})
it('should not display layer at load', () => {
assert.notOk(qs('path[fill="AliceBlue"]'))
})
it('should display on click', (done) => {
happen.click(qs(`[data-id='${L.stamp(datalayer)}'] .layer-toggle`))
window.setTimeout(() => {
assert.ok(qs('path[fill="AliceBlue"]'))
done()
}, 500)
})
it('should not display on zoom', (done) => {
map.setZoom(9, { animate: false })
window.setTimeout(() => {
assert.notOk(qs('path[fill="AliceBlue"]'))
done()
}, 500)
})
})
describe('#delete()', () => {
let deleteLink,
deletePath = '/map/99/datalayer/delete/62/'
before(() => {
datalayer = map.getDataLayerByUmapId(62)
})
it('should have a delete link in update form', () => {
enableEdit()
happen.click(qs('#browse_data_toggle_' + L.stamp(datalayer) + ' .layer-edit'))
deleteLink = qs('button.delete_datalayer_button')
assert.ok(deleteLink)
})
it('should delete features on datalayer delete', () => {
happen.click(deleteLink)
assert.notOk(qs('div.icon_container'))
})
it('should have set map dirty', () => {
assert.ok(map.isDirty)
})
it('should delete layer control row on delete', () => {
assert.notOk(
qs('.leaflet-control-browse #browse_data_toggle_' + L.stamp(datalayer))
)
})
it('should be removed from map.datalayers_index', () => {
assert.equal(map.datalayers_index.indexOf(datalayer), -1)
})
it('should be removed from map.datalayers', () => {
assert.notOk(map.datalayers[L.stamp(datalayer)])
})
it('should be visible again on edit cancel', () => {
clickCancel()
assert.ok(qs('div.icon_container'))
})
})
})

View file

@ -1,131 +0,0 @@
describe('U.FeatureMixin', function () {
let map, datalayer
before(async () => {
await fetchMock.mock(
/\/datalayer\/62\/\?.*/,
JSON.stringify(RESPONSES.datalayer62_GET)
)
this.options = {
umap_id: 99,
}
MAP = map = initMap({ umap_id: 99 })
const datalayer_options = defaultDatalayerData()
await map.initDataLayers([datalayer_options])
datalayer = map.getDataLayerByUmapId(62)
})
after(function () {
fetchMock.restore()
resetMap()
})
describe('#utils()', function () {
var poly, marker
function setFeatures(datalayer) {
datalayer.eachLayer(function (layer) {
if (!poly && layer instanceof L.Polygon) {
poly = layer
}
if (!marker && layer instanceof L.Marker) {
marker = layer
}
})
}
it('should generate a valid geojson', function () {
setFeatures(datalayer)
assert.ok(poly)
assert.deepEqual(poly.toGeoJSON().geometry, {
type: 'Polygon',
coordinates: [
[
[11.25, 53.585984],
[10.151367, 52.975108],
[12.689209, 52.167194],
[14.084473, 53.199452],
[12.634277, 53.618579],
[11.25, 53.585984],
[11.25, 53.585984],
],
],
})
// Ensure original latlngs has not been modified
assert.equal(poly.getLatLngs()[0].length, 6)
})
it('should remove empty _umap_options from exported geojson', function () {
setFeatures(datalayer)
assert.ok(poly)
assert.deepEqual(poly.toGeoJSON().properties, { name: 'name poly' })
assert.ok(marker)
assert.deepEqual(marker.toGeoJSON().properties, {
_umap_options: { color: 'OliveDrab' },
name: 'test',
})
})
})
describe('#properties()', function () {
it('should rename property', function () {
var poly = datalayer._lineToLayer({}, [
[0, 0],
[0, 1],
[0, 2],
])
poly.properties.prop1 = 'xxx'
poly.renameProperty('prop1', 'prop2')
assert.equal(poly.properties.prop2, 'xxx')
assert.ok(typeof poly.properties.prop1 === 'undefined')
})
it('should not create property when renaming', function () {
var poly = datalayer._lineToLayer({}, [
[0, 0],
[0, 1],
[0, 2],
])
delete poly.properties.prop2 // Make sure it doesn't exist
poly.renameProperty('prop1', 'prop2')
assert.ok(typeof poly.properties.prop2 === 'undefined')
})
it('should delete property', function () {
var poly = datalayer._lineToLayer({}, [
[0, 0],
[0, 1],
[0, 2],
])
poly.properties.prop = 'xxx'
assert.equal(poly.properties.prop, 'xxx')
poly.deleteProperty('prop')
assert.ok(typeof poly.properties.prop === 'undefined')
})
})
describe('#matchFilter()', function () {
var poly
it('should filter on properties', function () {
poly = datalayer._lineToLayer({}, [
[0, 0],
[0, 1],
[0, 2],
])
poly.properties.name = 'mooring'
assert.ok(poly.matchFilter('moo', ['name']))
assert.notOk(poly.matchFilter('foo', ['name']))
})
it('should be case unsensitive', function () {
assert.ok(poly.matchFilter('Moo', ['name']))
})
it('should match also in the middle of a string', function () {
assert.ok(poly.matchFilter('oor', ['name']))
})
it('should handle multiproperties', function () {
poly.properties.city = 'Teulada'
assert.ok(poly.matchFilter('eul', ['name', 'city', 'foo']))
})
})
})

View file

@ -1,37 +0,0 @@
describe('U.Map', () => {
let map, datalayer
before(async () => {
await fetchMock.mock(
/\/datalayer\/62\/\?.*/,
JSON.stringify(RESPONSES.datalayer62_GET)
)
this.options = {
umap_id: 99,
}
map = initMap({ umap_id: 99 })
const datalayer_options = defaultDatalayerData()
await map.initDataLayers([datalayer_options])
datalayer = map.getDataLayerByUmapId(62)
})
after(() => {
fetchMock.restore()
clickCancel()
resetMap()
})
describe('#localizeUrl()', function () {
it('should replace known variables', function () {
assert.equal(
map.localizeUrl('http://example.org/{zoom}'),
'http://example.org/' + map.getZoom()
)
})
it('should keep unknown variables', function () {
assert.equal(
map.localizeUrl('http://example.org/{unkown}'),
'http://example.org/{unkown}'
)
})
})
})

View file

@ -1,126 +0,0 @@
describe('U.Marker', () => {
let map, datalayer
before(async () => {
const datalayer_response = JSON.parse(JSON.stringify(RESPONSES.datalayer62_GET)) // Copy.
datalayer_response._umap_options.iconClass = 'Drop'
await fetchMock.mock(/\/datalayer\/62\/\?.*/, datalayer_response)
this.options = {
umap_id: 99,
}
MAP = map = initMap({ umap_id: 99 })
const datalayer_options = defaultDatalayerData()
await map.initDataLayers([datalayer_options])
datalayer = map.getDataLayerByUmapId(62)
})
after(() => {
fetchMock.restore()
resetMap()
})
describe('#iconClassChange()', () => {
it('should change icon class', () => {
enableEdit()
happen.click(qs('div.umap-drop-icon'))
happen.click(qs('ul.leaflet-inplace-toolbar a.umap-toggle-edit'))
changeSelectValue(
qs(
'form#umap-feature-shape-properties .umap-field-iconClass select[name=iconClass]'
),
'Circle'
)
assert.notOk(qs('div.umap-drop-icon'))
assert.ok(qs('div.umap-circle-icon'))
happen.click(
qs('form#umap-feature-shape-properties .umap-field-iconClass .undefine')
)
assert.notOk(qs('div.umap-circle-icon'))
assert.ok(qs('div.umap-drop-icon'))
clickCancel()
})
})
describe('#iconSymbolChange()', () => {
it('should change icon symbol', () => {
enableEdit()
happen.click(qs('div.umap-drop-icon'))
happen.click(qs('ul.leaflet-inplace-toolbar a.umap-toggle-edit'))
changeInputValue(
qs(
'form#umap-feature-shape-properties .umap-field-iconUrl input[name=iconUrl]'
),
'1'
)
assert.equal(qs('div.umap-drop-icon span').textContent, '1')
changeInputValue(
qs(
'form#umap-feature-shape-properties .umap-field-iconUrl input[name=iconUrl]'
),
'{name}'
)
assert.equal(qs('div.umap-drop-icon span').textContent, 'test')
clickCancel()
})
})
describe('#iconClassChange()', () => {
it('should change icon class', () => {
enableEdit()
happen.click(qs('div.umap-drop-icon'))
happen.click(qs('ul.leaflet-inplace-toolbar a.umap-toggle-edit'))
changeSelectValue(
qs(
'form#umap-feature-shape-properties .umap-field-iconClass select[name=iconClass]'
),
'Circle'
)
assert.notOk(qs('div.umap-drop-icon'))
assert.ok(qs('div.umap-circle-icon'))
happen.click(
qs('form#umap-feature-shape-properties .umap-field-iconClass .undefine')
)
assert.notOk(qs('div.umap-circle-icon'))
assert.ok(qs('div.umap-drop-icon'))
clickCancel()
})
})
describe('#clone', () => {
it('should clone marker', () => {
var layer = new U.Marker(map, [10, 20], {
datalayer: datalayer,
}).addTo(datalayer)
assert.equal(datalayer._index.length, 4)
other = layer.clone()
assert.ok(map.hasLayer(other))
assert.equal(datalayer._index.length, 5)
// Must not be the same reference
assert.notEqual(layer._latlng, other._latlng)
assert.equal(L.Util.formatNum(layer._latlng.lat), other._latlng.lat)
assert.equal(L.Util.formatNum(layer._latlng.lng), other._latlng.lng)
})
})
describe('#edit()', function (done) {
it('should allow changing coordinates manually', () => {
var layer = new U.Marker(map, [10, 20], {
datalayer: datalayer,
}).addTo(datalayer)
enableEdit()
layer.edit()
changeInputValue(qs('form.umap-form input[name="lat"]'), '54.43')
assert.equal(layer._latlng.lat, 54.43)
})
it('should not allow invalid latitude nor longitude', () => {
var layer = new U.Marker(map, [10, 20], {
datalayer: datalayer,
}).addTo(datalayer)
enableEdit()
layer.edit()
changeInputValue(qs('form.umap-form input[name="lat"]'), '5443')
assert.equal(layer._latlng.lat, 10)
changeInputValue(qs('form.umap-form input[name="lng"]'), '5443')
assert.equal(layer._latlng.lng, 20)
})
})
})

View file

@ -1,111 +0,0 @@
describe('U.Polygon', function () {
var p2ll, map, datalayer
before(function () {
map = initMap({ umap_id: 99 })
enableEdit()
p2ll = function (x, y) {
return map.containerPointToLatLng([x, y])
}
datalayer = map.createDataLayer()
datalayer.connectToMap()
})
after(function () {
clickCancel()
resetMap()
})
afterEach(function () {
datalayer.empty()
})
describe('#isMulti()', function () {
it('should return false for basic Polygon', function () {
var layer = new U.Polygon(
map,
[
[1, 2],
[3, 4],
[5, 6],
],
{ datalayer: datalayer }
)
assert.notOk(layer.isMulti())
})
it('should return false for nested basic Polygon', function () {
var latlngs = [[[p2ll(100, 150), p2ll(150, 200), p2ll(200, 100)]]],
layer = new U.Polygon(map, latlngs, { datalayer: datalayer })
assert.notOk(layer.isMulti())
})
it('should return false for simple Polygon with hole', function () {
var layer = new U.Polygon(
map,
[
[
[1, 2],
[3, 4],
[5, 6],
],
[
[7, 8],
[9, 10],
[11, 12],
],
],
{ datalayer: datalayer }
)
assert.notOk(layer.isMulti())
})
it('should return true for multi Polygon', function () {
var latLngs = [
[
[
[1, 2],
[3, 4],
[5, 6],
],
],
[
[
[7, 8],
[9, 10],
[11, 12],
],
],
]
var layer = new U.Polygon(map, latLngs, { datalayer: datalayer })
assert.ok(layer.isMulti())
})
it('should return true for multi Polygon with hole', function () {
var latLngs = [
[
[
[10, 20],
[30, 40],
[50, 60],
],
],
[
[
[0, 10],
[10, 10],
[10, 0],
],
[
[2, 3],
[2, 4],
[3, 4],
],
],
]
var layer = new U.Polygon(map, latLngs, { datalayer: datalayer })
assert.ok(layer.isMulti())
})
})
})

View file

@ -1,286 +0,0 @@
describe('U.Polyline', function () {
var p2ll, map
before(function () {
this.map = map = initMap({ umap_id: 99 })
enableEdit()
p2ll = function (x, y) {
return map.containerPointToLatLng([x, y])
}
this.datalayer = this.map.createDataLayer()
this.datalayer.connectToMap()
})
after(function () {
clickCancel()
resetMap()
})
afterEach(function () {
this.datalayer.empty()
})
describe('#isMulti()', function () {
it('should return false for basic Polyline', function () {
var layer = new U.Polyline(
this.map,
[
[1, 2],
[3, 4],
[5, 6],
],
{ datalayer: this.datalayer }
)
assert.notOk(layer.isMulti())
})
it('should return false for nested basic Polyline', function () {
var layer = new U.Polyline(
this.map,
[
[
[1, 2],
[3, 4],
[5, 6],
],
],
{ datalayer: this.datalayer }
)
assert.notOk(layer.isMulti())
})
it('should return true for multi Polyline', function () {
var latLngs = [
[
[
[1, 2],
[3, 4],
[5, 6],
],
],
[
[
[7, 8],
[9, 10],
[11, 12],
],
],
]
var layer = new U.Polyline(this.map, latLngs, { datalayer: this.datalayer })
assert.ok(layer.isMulti())
})
})
describe('#contextmenu', function () {
afterEach(function () {
// Make sure contextmenu is hidden.
happen.once(document, { type: 'keydown', keyCode: 27 })
})
describe('#in edit mode', function () {
it('should allow to remove shape when multi', function () {
var latlngs = [
[p2ll(100, 100), p2ll(100, 200)],
[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.equal(qst('Remove shape from the multi'), 1)
})
it('should not allow to remove shape when not multi', function () {
var latlngs = [[p2ll(100, 100), p2ll(100, 200)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Remove shape from the multi'))
})
it('should not allow to isolate shape when not multi', function () {
var latlngs = [[p2ll(100, 100), p2ll(100, 200)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Extract shape to separate feature'))
})
it('should allow to isolate shape when multi', function () {
var latlngs = [
[p2ll(100, 150), p2ll(100, 200)],
[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.ok(qst('Extract shape to separate feature'))
})
it('should not allow to transform to polygon when multi', function () {
var latlngs = [
[p2ll(100, 150), p2ll(100, 200)],
[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Transform to polygon'))
})
it('should allow to transform to polygon when not multi', function () {
var latlngs = [p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.equal(qst('Transform to polygon'), 1)
})
it('should not allow to transfer shape when not editedFeature', function () {
var layer = new U.Polyline(this.map, [p2ll(100, 150), p2ll(100, 200)], {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Transfer shape to edited feature'))
})
it('should not allow to transfer shape when editedFeature is not a line', function () {
var layer = new U.Polyline(this.map, [p2ll(100, 150), p2ll(100, 200)], {
datalayer: this.datalayer,
}).addTo(this.datalayer),
other = new U.Polygon(
this.map,
[p2ll(200, 300), p2ll(300, 200), p2ll(200, 100)],
{ datalayer: this.datalayer }
).addTo(this.datalayer)
other.edit()
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Transfer shape to edited feature'))
})
it('should allow to transfer shape when another line is edited', function () {
var layer = new U.Polyline(
this.map,
[p2ll(100, 150), p2ll(100, 200), p2ll(200, 100)],
{ datalayer: this.datalayer }
).addTo(this.datalayer),
other = new U.Polyline(this.map, [p2ll(200, 300), p2ll(300, 200)], {
datalayer: this.datalayer,
}).addTo(this.datalayer)
other.edit()
happen.once(layer._path, { type: 'contextmenu' })
assert.equal(qst('Transfer shape to edited feature'), 1)
other.remove()
layer.remove()
})
it('should allow to merge lines when multi', function () {
var latlngs = [
[p2ll(100, 100), p2ll(100, 200)],
[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.equal(qst('Merge lines'), 1)
})
it('should not allow to merge lines when not multi', function () {
var latlngs = [[p2ll(100, 100), p2ll(100, 200)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
happen.once(layer._path, { type: 'contextmenu' })
assert.notOk(qst('Merge lines'))
})
it('should allow to split lines when clicking on vertex', function () {
var latlngs = [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
layer.enableEdit()
happen.at('contextmenu', 350, 400)
assert.equal(qst('Split line'), 1)
})
it('should not allow to split lines when clicking on first vertex', function () {
var latlngs = [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
layer.enableEdit()
happen.at('contextmenu', 300, 350)
assert.equal(qst('Delete this feature'), 1) // Make sure we have clicked on the vertex.
assert.notOk(qst('Split line'))
})
it('should not allow to split lines when clicking on last vertex', function () {
var latlngs = [[p2ll(300, 350), p2ll(350, 400), p2ll(400, 300)]],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
layer.enableEdit()
happen.at('contextmenu', 400, 300)
assert.equal(qst('Delete this feature'), 1) // Make sure we have clicked on the vertex.
assert.notOk(qst('Split line'))
})
})
})
describe('#mergeShapes', function () {
it('should remove duplicated join point when merging', function () {
var latlngs = [
[
[0, 0],
[0, 1],
],
[
[0, 1],
[0, 2],
],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
layer.mergeShapes()
layer.disableEdit() // Remove vertex from latlngs to compare them.
assert.deepEqual(layer.getLatLngs(), [
L.latLng([0, 0]),
L.latLng([0, 1]),
L.latLng([0, 2]),
])
assert(this.map.isDirty)
})
it('should revert candidate if first point is closer', function () {
var latlngs = [
[
[0, 0],
[0, 1],
],
[
[0, 2],
[0, 1],
],
],
layer = new U.Polyline(this.map, latlngs, {
datalayer: this.datalayer,
}).addTo(this.datalayer)
layer.mergeShapes()
layer.disableEdit()
assert.deepEqual(layer.getLatLngs(), [
L.latLng([0, 0]),
L.latLng([0, 1]),
L.latLng([0, 2]),
])
})
})
})

View file

@ -1,28 +0,0 @@
describe('L.Util', function () {
describe('#TextColorFromBackgroundColor', function () {
it('should output white for black', function () {
document.body.style.backgroundColor = 'black'
assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#ffffff')
})
it('should output white for brown', function () {
document.body.style.backgroundColor = 'brown'
assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#ffffff')
})
it('should output black for white', function () {
document.body.style.backgroundColor = 'white'
assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000')
})
it('should output black for tan', function () {
document.body.style.backgroundColor = 'tan'
assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000')
})
it('should output black by default', function () {
document.body.style.backgroundColor = 'transparent'
assert.equal(L.DomUtil.TextColorFromBackgroundColor(document.body), '#000000')
})
})
})

View file

@ -1,455 +0,0 @@
window.assert = chai.assert
window.expect = chai.expect
var qs = function (selector, element) {
return (element || document).querySelector(selector)
}
var qsa = function (selector) {
return document.querySelectorAll(selector)
}
var qst = function (text, parent) {
// find element by its text content
var r = document.evaluate(
"descendant::*[contains(text(),'" + text + "')]",
parent || qs('#map'),
null,
XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
null
),
count = 0
while (r.iterateNext()) console.log(++count)
return count
}
happen.at = function (what, x, y, props) {
this.once(
document.elementFromPoint(x, y),
L.Util.extend(
{
type: what,
clientX: x,
clientY: y,
screenX: x,
screenY: y,
which: 1,
button: 0,
},
props || {}
)
)
}
var resetMap = function () {
var mapElement = qs('#map')
mapElement.innerHTML = 'Done'
delete mapElement._leaflet_id
document.body.className = ''
}
var enableEdit = function () {
happen.click(qs('div.leaflet-control-edit-enable button'))
}
var disableEdit = function () {
happen.click(qs('.leaflet-control-edit-disable'))
}
var clickSave = function () {
happen.click(qs('.leaflet-control-edit-save'))
}
var clickCancel = function () {
var _confirm = window.confirm
window.confirm = function (text) {
return true
}
happen.click(qs('button.leaflet-control-edit-cancel'))
happen.once(document.body, { type: 'keypress', keyCode: 13 })
window.confirm = _confirm
}
var changeInputValue = function (input, value) {
input.value = value
happen.once(input, { type: 'input' })
happen.once(input, { type: 'blur' })
}
var changeSelectValue = function (path_or_select, value) {
if (typeof path_or_select === 'string') path_or_select = qs(path_or_select)
var found = false
for (var i = 0; i < path_or_select.length; i++) {
if (path_or_select.options[i].value === value) {
path_or_select.options[i].selected = true
found = true
}
}
happen.once(path_or_select, { type: 'change' })
if (!found)
throw new Error('Value ' + value + 'not found in select ' + path_or_select)
return path_or_select
}
var cleanAlert = function () {
L.DomUtil.removeClass(qs('#map'), 'umap-alert')
L.DomUtil.get('umap-alert-container').innerHTML = ''
UI_ALERT_ID = null // Prevent setTimeout to be called
}
var defaultDatalayerData = function (custom) {
var _default = {
iconClass: 'Default',
name: 'Elephants',
displayOnLoad: true,
id: 62,
pictogram_url: null,
weight: null,
fillColor: '',
color: '',
stroke: true,
smoothFactor: null,
dashArray: '',
fill: true,
}
return L.extend({}, _default, custom)
}
function initMap(options) {
default_options = {
type: 'Feature',
properties: {
umap_id: 42,
datalayers: [],
urls: {
map: '/map/{slug}_{pk}',
datalayer_view: '/datalayer/{pk}/',
map_update: '/map/{map_id}/update/settings/',
map_old_url: '/map/{username}/{slug}/',
map_clone: '/map/{map_id}/update/clone/',
map_short_url: '/m/{pk}/',
map_anonymous_edit_url: '/map/anonymous-edit/{signature}',
map_new: '/map/new/',
datalayer_update: '/map/{map_id}/datalayer/update/{pk}/',
map_delete: '/map/{map_id}/update/delete/',
map_create: '/map/create/',
logout: '/logout/',
datalayer_create: '/map/{map_id}/datalayer/create/',
login_popup_end: '/login/popupd/',
login: '/login/',
datalayer_delete: '/map/{map_id}/datalayer/delete/{pk}/',
datalayer_versions: '/map/{map_id}/datalayer/{pk}/versions/',
datalayer_version: '/datalayer/{pk}/{name}',
pictogram_list_json: '/pictogram/json/',
map_update_permissions: '/map/{map_id}/update/permissions/',
map_download: '/map/{map_id}/download/',
},
default_iconUrl: '../src/img/marker.svg',
zoom: 6,
share_statuses: [
[1, 'Tout le monde (public)'],
[2, 'Quiconque a le lien'],
[3, 'Éditeurs uniquement'],
],
tilelayers: [
{
attribution: '\u00a9 OSM Contributors',
name: 'OpenStreetMap',
url_template: 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
minZoom: 0,
maxZoom: 18,
id: 1,
selected: true,
},
{
attribution: 'HOT and friends',
name: 'HOT OSM-fr server',
url_template: 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
rank: 99,
minZoom: 0,
maxZoom: 20,
id: 2,
},
],
tilelayer: {
attribution: 'HOT and friends',
name: 'HOT OSM-fr server',
url_template: 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
rank: 99,
minZoom: 0,
maxZoom: 20,
id: 2,
},
licences: {
'No licence set': {
url: '',
name: 'No licence set',
},
'Licence ouverte/Open Licence': {
url: 'http://www.data.gouv.fr/Licence-Ouverte-Open-Licence',
name: 'Licence ouverte/Open Licence',
},
'WTFPL': {
url: 'http://www.wtfpl.net/',
name: 'WTFPL',
},
'ODbl': {
url: 'http://opendatacommons.org/licenses/odbl/',
name: 'ODbl',
},
},
name: 'name of the map',
description: 'The description of the map',
locale: 'en',
editMode: 'advanced',
moreControl: true,
scaleControl: true,
miniMap: false,
datalayersControl: true,
displayCaptionOnLoad: false,
displayPopupFooter: false,
displayDataBrowserOnLoad: false,
permissions: {
share_status: 1,
owner: {
id: 1,
name: 'ybon',
url: '/en/user/ybon/',
},
editors: [],
},
user: {
id: 1,
name: 'foofoo',
url: '/en/me',
},
},
}
options = options || {}
options.properties = L.extend({}, default_options.properties, options)
options.geometry = {
type: 'Point',
coordinates: [5.0592041015625, 52.05924589011585],
}
return new U.Map('map', options)
}
var RESPONSES = {
datalayer62_GET: {
crs: null,
type: 'FeatureCollection',
_umap_options: defaultDatalayerData(),
features: [
{
geometry: {
type: 'Point',
coordinates: [-0.274658203125, 52.57634993749885],
},
type: 'Feature',
id: 1807,
properties: { _umap_options: { color: 'OliveDrab' }, name: 'test' },
},
{
geometry: {
type: 'LineString',
coordinates: [
[-0.5712890625, 54.47642158429295],
[0.439453125, 54.610254981579146],
[1.724853515625, 53.44880683542759],
[4.163818359375, 53.98839506479995],
[5.306396484375, 53.533778184257805],
[6.591796875, 53.70971358510174],
[7.042236328124999, 53.35055131839989],
],
},
type: 'Feature',
id: 20,
properties: { _umap_options: { fill: false, opacity: 0.6 }, name: 'test' },
},
{
geometry: {
type: 'Polygon',
coordinates: [
[
[11.25, 53.585983654559804],
[10.1513671875, 52.9751081817353],
[12.689208984375, 52.16719363541221],
[14.084472656249998, 53.199451902831555],
[12.63427734375, 53.61857936489517],
[11.25, 53.585983654559804],
[11.25, 53.585983654559804],
],
],
},
type: 'Feature',
id: 76,
properties: { name: 'name poly' },
},
],
},
// This one is non browsable
datalayer63_GET: {
crs: null,
type: 'FeatureCollection',
_umap_options: defaultDatalayerData({ id: 63, browsable: false }),
features: [
{
geometry: {
type: 'Polygon',
coordinates: [
[
[5.545478, 45.068383],
[5.545907, 45.067277],
[5.548439, 45.067565],
[5.552516, 45.06752],
[5.553288, 45.068217],
[5.549405, 45.069247],
[5.548224, 45.071005],
[5.545907, 45.071096],
[5.545478, 45.068383],
],
],
},
type: 'Feature',
id: 76,
properties: { name: 'non browsable 1' },
},
{
type: 'Feature',
properties: {
_umap_options: {
color: 'SteelBlue',
},
name: 'non browsable 2',
},
geometry: {
type: 'Polygon',
coordinates: [
[
[5.550542, 45.071717],
[5.548182, 45.071051],
[5.549426, 45.069232],
[5.553331, 45.068171],
[5.554812, 45.070869],
[5.553396, 45.072384],
[5.550542, 45.071717],
],
],
},
},
],
},
// This one is not shown at load
datalayer64_GET: {
crs: null,
type: 'FeatureCollection',
_umap_options: defaultDatalayerData({
name: 'hidden',
id: 64,
displayOnLoad: false,
}),
features: [
{
geometry: {
type: 'Polygon',
coordinates: [
[
[5.545478, 45.068383],
[5.545907, 45.067277],
[5.548439, 45.067565],
[5.552516, 45.06752],
[5.553288, 45.068217],
[5.549405, 45.069247],
[5.548224, 45.071005],
[5.545907, 45.071096],
[5.545478, 45.068383],
],
],
},
type: 'Feature',
id: 76,
properties: { name: 'not shown at load 1' },
},
{
type: 'Feature',
properties: {
_umap_options: {
color: 'AliceBlue',
},
name: 'not shown at load 2',
},
geometry: {
type: 'Polygon',
coordinates: [
[
[5.550542, 45.071717],
[5.548182, 45.071051],
[5.549426, 45.069232],
[5.553331, 45.068171],
[5.554812, 45.070869],
[5.553396, 45.072384],
[5.550542, 45.071717],
],
],
},
},
],
},
}
var kml_example =
'<?xml version="1.0" encoding="UTF-8"?>' +
'<kml xmlns="http://www.opengis.net/kml/2.2">' +
'<Placemark>' +
'<name>Simple point</name>' +
'<description>Here is a simple description.</description>' +
'<Point>' +
'<coordinates>-122.0822035425683,37.42228990140251,0</coordinates>' +
'</Point>' +
'</Placemark>' +
'<Placemark>' +
'<name>Simple path</name>' +
'<description>Simple description</description>' +
'<LineString>' +
'<coordinates>-112.2550785337791,36.07954952145647,2357 -112.2549277039738,36.08117083492122,2357 -112.2552505069063,36.08260761307279,2357</coordinates>' +
'</LineString>' +
'</Placemark>' +
'<Placemark>' +
'<name>Simple polygon</name>' +
'<description>A description.</description>' +
'<Polygon>' +
'<outerBoundaryIs>' +
'<LinearRing>' +
'<coordinates>' +
' -77.05788457660967,38.87253259892824,100 ' +
' -77.05465973756702,38.87291016281703,100 ' +
' -77.05315536854791,38.87053267794386,100 ' +
' -77.05788457660967,38.87253259892824,100 ' +
'</coordinates>' +
'</LinearRing>' +
'</outerBoundaryIs>' +
'</Polygon>' +
'</Placemark>' +
'</kml>'
var gpx_example =
'<gpx' +
' version="1.1"' +
' creator="GPSBabel - http://www.gpsbabel.org"' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xmlns="http://www.topografix.com/GPX/1/1"' +
' xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">' +
' <wpt lat="45.44283" lon="-121.72904"><ele>1374</ele><name>Simple Point</name><desc>Simple description</desc></wpt>' +
' <trk>' +
' <name>Simple path</name>' +
' <desc>Simple description</desc>' +
' <trkseg>' +
' <trkpt lat="45.4431641" lon="-121.7295456"></trkpt>' +
' <trkpt lat="45.4428615" lon="-121.7290800"></trkpt>' +
' <trkpt lat="45.4425697" lon="-121.7279085"></trkpt>' +
' </trkseg>' +
' </trk>' +
'</gpx>'
var csv_example =
'Foo,Latitude,Longitude,title,description\n' +
'bar,41.34,122.86,a point somewhere,the description of this point'
// Make Sinon log readable
sinon.format = function (what) {
if (typeof what === 'object') {
return JSON.stringify(what, null, 4)
} else if (typeof what === 'undefined') {
return ''
} else {
return what.toString()
}
}

View file

@ -1,139 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Umap front Tests</title>
<meta charset="utf-8" />
<script type="module" src="../js/modules/leaflet-configure.js" defer></script>
<script type="module" src="../js/modules/global.js" defer></script>
<script src="../vendors/editable/Path.Drag.js" defer></script>
<script src="../vendors/editable/Leaflet.Editable.js" defer></script>
<script src="../vendors/hash/leaflet-hash.js" defer></script>
<script src="../vendors/editinosm/Leaflet.EditInOSM.js" defer></script>
<script src="../vendors/minimap/Control.MiniMap.min.js" defer></script>
<script src="../vendors/csv2geojson/csv2geojson.js" defer></script>
<script src="../vendors/togeojson/togeojson.umd.js" defer></script>
<script src="../vendors/osmtogeojson/osmtogeojson.js" defer></script>
<script src="../vendors/loading/Control.Loading.js" defer></script>
<script src="../vendors/markercluster/leaflet.markercluster.js" defer></script>
<script src="../vendors/contextmenu/leaflet.contextmenu.min.js" defer></script>
<script src="../vendors/photon/leaflet.photon.js" defer></script>
<script src="../vendors/georsstogeojson/GeoRSSToGeoJSON.js" defer></script>
<script src="../vendors/heat/leaflet-heat.js" defer></script>
<script src="../vendors/fullscreen/Leaflet.fullscreen.min.js" defer></script>
<script src="../vendors/toolbar/leaflet.toolbar.js" defer></script>
<script src="../vendors/formbuilder/Leaflet.FormBuilder.js" defer></script>
<script src="../vendors/measurable/Leaflet.Measurable.js" defer></script>
<script src="../vendors/togpx/togpx.js" defer></script>
<script src="../vendors/iconlayers/iconLayers.js" defer></script>
<script src="../vendors/tokml/tokml.js" defer></script>
<script src="../vendors/locatecontrol/L.Control.Locate.min.js" defer></script>
<script src="../vendors/colorbrewer/colorbrewer.js" defer></script>
<script src="../vendors/simple-statistics/simple-statistics.min.js" defer></script>
<script src="../js/umap.core.js" defer></script>
<script src="../js/umap.autocomplete.js" defer></script>
<script src="../js/umap.popup.js" defer></script>
<script src="../js/umap.forms.js" defer></script>
<script src="../js/umap.icon.js" defer></script>
<script src="../js/umap.features.js" defer></script>
<script src="../js/umap.permissions.js" defer></script>
<script src="../js/umap.datalayer.permissions.js" defer></script>
<script src="../js/umap.layer.js" defer></script>
<script src="../js/umap.controls.js" defer></script>
<script src="../js/umap.slideshow.js" defer></script>
<script src="../js/umap.tableeditor.js" defer></script>
<script src="../js/umap.importer.js" defer></script>
<script src="../js/umap.share.js" defer></script>
<script src="../js/umap.js" defer></script>
<script src="../js/umap.ui.js" defer></script>
<script src="../js/components/fragment.js" defer></script>
<link rel="stylesheet" href="../vendors/leaflet/leaflet.css" />
<link rel="stylesheet" href="../vendors/markercluster/MarkerCluster.css" />
<link rel="stylesheet" href="../vendors/markercluster/MarkerCluster.Default.css" />
<link rel="stylesheet" href="../vendors/editinosm/Leaflet.EditInOSM.css" />
<link rel="stylesheet" href="../vendors/minimap/Control.MiniMap.min.css" />
<link rel="stylesheet" href="../vendors/contextmenu/leaflet.contextmenu.min.css" />
<link rel="stylesheet" href="../vendors/toolbar/leaflet.toolbar.css" />
<link rel="stylesheet" href="../vendors/measurable/Leaflet.Measurable.css" />
<link rel="stylesheet" href="../vendors/fullscreen/leaflet.fullscreen.css" />
<link rel="stylesheet" href="../vendors/locatecontrol/L.Control.Locate.min.css" />
<link rel="stylesheet" href="../vendors/iconlayers/iconLayers.css" />
<link rel="stylesheet" href="../../umap/vars.css" />
<link rel="stylesheet" href="../../umap/font.css" />
<link rel="stylesheet" href="../../umap/base.css" />
<link rel="stylesheet" href="../../umap/content.css" />
<link rel="stylesheet" href="../../umap/nav.css" />
<link rel="stylesheet" href="../../umap/map.css" />
<link rel="stylesheet" href="../../umap/theme.css" />
<script src="../../../../node_modules/sinon/pkg/sinon.js"></script>
<script src="../../../../node_modules/mocha/mocha.js"></script>
<script src="../../../../node_modules/chai/chai.js"></script>
<script src="../../../../node_modules/happen/happen.js"></script>
<link rel="stylesheet" href="../../../../node_modules/mocha/mocha.css" />
<script type="module">
import fetchMock from '../../../../node_modules/fetch-mock/esm/client.js';
window.fetchMock = fetchMock
</script>
<script type="text/javascript">
mocha.setup({
ui: 'bdd',
bail: window.location.search.indexOf('failfast') !== -1,
ignoreLeaks: true,
})
chai.config.includeStack = true
</script>
<script src="./_pre.js" defer></script>
<script src="./Map.js" defer></script>
<script src="./Feature.js" defer></script>
<script src="./Marker.js" defer></script>
<script src="./Polyline.js" defer></script>
<script src="./Polygon.js" defer></script>
<script src="./Util.js" defer></script>
<script type="module" src="./URLs.js" defer></script>
<style type="text/css">
#mocha {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 10000;
background-color: white;
box-shadow: 0px 0px 8px 0px black;
overflow-y: auto;
display: none;
}
#mocha-stats {
position: absolute;
}
</style>
</head>
<body>
<div id="mocha"></div>
<div id="map"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
var runner = (window.mochaPhantomJS || window.mocha).run(function (failures) {
if (window.location.search.indexOf('debug') === -1)
qs('#mocha').style.display = 'block'
console.log(failures)
})
if (window.location.search.indexOf('debug') !== -1) {
runner.on('fail', function (test, err) {
console.log(test.title, test.err)
console.log(test.err.expected, test.err.actual)
console.log(test.err.stack)
})
sinon.log = function (message) {
console.log(message)
}
}
})
</script>
</body>
</html>

View file

@ -44,6 +44,7 @@
--small-box-padding: 4px; --small-box-padding: 4px;
--box-margin: 14px; --box-margin: 14px;
--text-margin: 7px; --text-margin: 7px;
--dialog-width: 40vw;
/* z-indexes (leaflet CSS sets the map at 400 by default) */ /* z-indexes (leaflet CSS sets the map at 400 by default) */
--zindex-alert: 500; --zindex-alert: 500;

View file

@ -5,14 +5,22 @@
<div class="wrapper search_wrapper"> <div class="wrapper search_wrapper">
<div class="row"> <div class="row">
<form action="{% firstof action search_url %}" method="get"> <form action="{% firstof action search_url %}" method="get">
<div class="col two-third mwide"> <div class="col half mwide">
<input name="q" <input name="q"
type="search" type="search"
placeholder="{% firstof placeholder default_placeholder %}" placeholder="{% firstof placeholder default_placeholder %}"
aria-label="{% firstof placeholder default_placeholder %}" aria-label="{% firstof placeholder default_placeholder %}"
value="{{ request.GET.q|default:"" }}" /> value="{{ request.GET.q|default:"" }}" />
</div> </div>
<div class="col third mwide"> <div class="col quarter mwide">
<select name="tags">
<option value="">{% trans "Any category" %}</option>
{% for value, label in UMAP_TAGS %}
<option value="{{ value }}" {% if request.GET.tags == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col quarter mwide">
<input type="submit" value="{% trans "Search" %}" class="neutral" /> <input type="submit" value="{% trans "Search" %}" class="neutral" />
</div> </div>
</form> </form>

View file

@ -226,3 +226,18 @@ def test_hover_tooltip_setting_should_be_persistent(live_server, map, page):
- text: always never on hover - text: always never on hover
""") """)
expect(page.locator(".umap-field-showLabel input[value=null]")).to_be_checked() expect(page.locator(".umap-field-showLabel input[value=null]")).to_be_checked()
def test_can_edit_map_tags(live_server, map, page):
map.settings["properties"]["tags"] = ["art"]
map.edit_status = Map.ANONYMOUS
map.save()
page.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
page.get_by_role("button", name="Edit map name and caption").click()
page.get_by_text("Tags").click()
expect(page.get_by_label("Art and Culture")).to_be_checked()
page.get_by_label("Bike").check()
with page.expect_response(re.compile("./update/settings/.*")):
page.get_by_role("button", name="Save").click()
saved = Map.objects.get(pk=map.pk)
assert saved.tags == ["art", "bike"]

View file

@ -486,3 +486,27 @@ def test_cannot_search_deleted_map(client, map):
url = reverse("search") url = reverse("search")
response = client.get(url + "?q=Blé") response = client.get(url + "?q=Blé")
assert "Blé dur" not in response.content.decode() assert "Blé dur" not in response.content.decode()
@pytest.mark.django_db
def test_filter_by_tag(client, map):
# Very basic search, that do not deal with accent nor case.
# See install.md for how to have a smarter dict + index.
map.name = "Blé dur"
map.tags = ["bike"]
map.save()
url = reverse("search")
response = client.get(url + "?tags=bike")
assert "Blé dur" in response.content.decode()
@pytest.mark.django_db
def test_can_combine_search_and_filter(client, map):
# Very basic search, that do not deal with accent nor case.
# See install.md for how to have a smarter dict + index.
map.name = "Blé dur"
map.tags = ["bike"]
map.save()
url = reverse("search")
response = client.get(url + "?q=dur&tags=bike")
assert "Blé dur" in response.content.decode()

View file

@ -334,12 +334,18 @@ class TeamMaps(PaginatorMixin, DetailView):
class SearchMixin: class SearchMixin:
def get_search_queryset(self, **kwargs): def get_search_queryset(self, **kwargs):
q = self.request.GET.get("q") q = self.request.GET.get("q")
tags = [t for t in self.request.GET.getlist("tags") if t]
qs = Map.objects.all()
if q: if q:
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION) vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
query = SearchQuery( query = SearchQuery(
q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch" q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch"
) )
return Map.objects.annotate(search=vector).filter(search=query) qs = qs.annotate(search=vector).filter(search=query)
if tags:
qs = qs.filter(tags__contains=tags)
if q or tags:
return qs
class Search(PaginatorMixin, TemplateView, PublicMapsMixin, SearchMixin): class Search(PaginatorMixin, TemplateView, PublicMapsMixin, SearchMixin):