Merge ac6e3c8d64
into 8292608365
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
23
umap/migrations/0027_map_tags.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -16,6 +16,10 @@ input::-webkit-input-placeholder, ::-webkit-input-placeholder {
|
||||||
input:-moz-placeholder, :-moz-placeholder {
|
input:-moz-placeholder, :-moz-placeholder {
|
||||||
color: #a5a5a5;
|
color: #a5a5a5;
|
||||||
}
|
}
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* **************** */
|
/* **************** */
|
||||||
|
|
|
@ -206,6 +206,7 @@
|
||||||
line-height: initial;
|
line-height: initial;
|
||||||
height: initial;
|
height: initial;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
padding: 0 var(--text-margin);
|
||||||
}
|
}
|
||||||
.umap-caption-bar-enabled {
|
.umap-caption-bar-enabled {
|
||||||
--current-footer-height: var(--footer-height);
|
--current-footer-height: var(--footer-height);
|
||||||
|
|
|
@ -61,10 +61,7 @@ textarea {
|
||||||
select {
|
select {
|
||||||
border: 1px solid #222;
|
border: 1px solid #222;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 28px;
|
padding: var(--button-padding);
|
||||||
line-height: 28px;
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-bottom: var(--box-margin);
|
|
||||||
}
|
}
|
||||||
.dark select {
|
.dark select {
|
||||||
color: #efefef;
|
color: #efefef;
|
||||||
|
|
1
umap/static/umap/img/tags/arts.svg
Normal 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 |
1
umap/static/umap/img/tags/business.svg
Normal 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 |
1
umap/static/umap/img/tags/cycling.svg
Normal 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 |
1
umap/static/umap/img/tags/education.svg
Normal 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 |
1
umap/static/umap/img/tags/environment.svg
Normal 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 |
1
umap/static/umap/img/tags/food.svg
Normal 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 |
1
umap/static/umap/img/tags/geopolitics.svg
Normal 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 |
1
umap/static/umap/img/tags/health.svg
Normal 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 |
1
umap/static/umap/img/tags/hiking.svg
Normal 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 |
1
umap/static/umap/img/tags/history.svg
Normal 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 |
1
umap/static/umap/img/tags/public.svg
Normal 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 |
1
umap/static/umap/img/tags/science.svg
Normal 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 |
1
umap/static/umap/img/tags/shopping.svg
Normal 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 |
1
umap/static/umap/img/tags/sport.svg
Normal 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 |
1
umap/static/umap/img/tags/tourism.svg
Normal 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 |
1
umap/static/umap/img/tags/transports.svg
Normal 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 |
1
umap/static/umap/img/tags/travel.svg
Normal 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 |
|
@ -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,
|
||||||
|
|
|
@ -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,13 +1331,14 @@ 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() {
|
||||||
return this._values
|
return this._values
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -935,6 +935,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 */
|
||||||
/* *************************** */
|
/* *************************** */
|
||||||
|
|
|
@ -4,15 +4,23 @@
|
||||||
{% trans "Search maps" as default_placeholder %}
|
{% trans "Search maps" as default_placeholder %}
|
||||||
<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 class="search-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>
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|