Compare commits

...

4 commits

Author SHA1 Message Date
Yohan Boniface
ac6e3c8d64 chore: align search form elements
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 17:05:08 +01:00
Yohan Boniface
cafd651560 feat: associate icons to tags
Co-authored-by: David Larlet <david@larlet.fr>
2025-03-28 16:50:05 +01:00
Yohan Boniface
76212dd6fa feat: basic filtering by tags 2025-03-28 16:50:05 +01:00
Yohan Boniface
2a52017f76 feat: add Map.tags and allow to edit from client
cf #2283
2025-03-28 16:50:05 +01:00
35 changed files with 198 additions and 14 deletions

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;
# 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:

View file

@ -14,6 +14,7 @@ def settings(request):
"UMAP_DEMO_SITE": djsettings.UMAP_DEMO_SITE,
"UMAP_HOST_INFOS": djsettings.UMAP_HOST_INFOS,
"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.forms.utils import ErrorList
from django.template.defaultfilters import slugify
from django.utils.translation import gettext_lazy as _
from .models import DataLayer, Map, Team
@ -92,7 +91,7 @@ class MapSettingsForm(forms.ModelForm):
return self.cleaned_data["center"]
class Meta:
fields = ("settings", "name", "center", "slug")
fields = ("settings", "name", "center", "slug", "tags")
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.contrib.auth.models import User
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.storage import storages
from django.core.signing import Signer
@ -236,6 +237,7 @@ class Map(NamedModel):
settings = models.JSONField(
blank=True, null=True, verbose_name=_("settings"), default=dict
)
tags = ArrayField(models.CharField(max_length=200), blank=True, default=list)
objects = models.Manager()
public = PublicManager()
@ -420,7 +422,8 @@ class Map(NamedModel):
return {
"iconUrl": {
"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
from django.conf.locale import LANG_INFO
from django.utils.translation import gettext_lazy as _
import umap as project_module
@ -290,6 +291,25 @@ UMAP_HOME_FEED = "latest"
UMAP_IMPORTERS = {}
UMAP_HOST_INFOS = {}
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_GZIP = True

View file

@ -16,6 +16,10 @@ input::-webkit-input-placeholder, ::-webkit-input-placeholder {
input:-moz-placeholder, :-moz-placeholder {
color: #a5a5a5;
}
.search-form {
display: flex;
align-items: baseline;
}
/* **************** */

View file

@ -206,6 +206,7 @@
line-height: initial;
height: initial;
width: auto;
padding: 0 var(--text-margin);
}
.umap-caption-bar-enabled {
--current-footer-height: var(--footer-height);

View file

@ -61,10 +61,7 @@ textarea {
select {
border: 1px solid #222;
width: 100%;
height: 28px;
line-height: 28px;
margin-top: 5px;
margin-bottom: var(--box-margin);
padding: var(--button-padding);
}
.dark select {
color: #efefef;

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',
slugKey: 'PropertyInput',
labelKey: 'PropertyInput',
tags: 'TagsEditor',
}
for (const [key, defaults] of Object.entries(SCHEMA)) {
const properties = Object.assign({}, defaults)
@ -152,6 +153,8 @@ export class MutatingForm extends Form {
} else if (properties.type === Number) {
if (properties.step) properties.handler = 'Range'
else properties.handler = 'IntInput'
} else if (properties.type === Array) {
properties.handler = 'CheckBoxes'
} else if (properties.choices) {
const text_length = properties.choices.reduce(
(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 {
getTemplate() {
return `<select name="${this.name}" data-ref=select></select>`
@ -1296,13 +1331,14 @@ Fields.ManageEditors = class extends BaseElement {
placeholder: translate("Type editor's username"),
}
this.autocomplete = new AjaxAutocompleteMultiple(this.container, options)
this._values = this.toHTML()
if (this._values)
this._values = this.toHTML() || []
if (this._values) {
for (let i = 0; i < this._values.length; i++)
this.autocomplete.displaySelected({
item: { value: this._values[i].id, label: this._values[i].name },
})
}
}
value() {
return this._values

View file

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

View file

@ -757,6 +757,12 @@ export default class Umap extends ServerStored {
const form = builder.build()
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 creditsFields = [
'properties.licence',
@ -1185,6 +1191,7 @@ export default class Umap extends ServerStored {
const formData = new FormData()
formData.append('name', this.properties.name)
formData.append('center', JSON.stringify(this.geometry()))
formData.append('tags', this.properties.tags || [])
formData.append('settings', JSON.stringify(geojson))
const uri = this.urls.get('map_save', { map_id: this.id })
const [data, _, error] = await this.server.post(uri, {}, formData)

View file

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

View file

@ -4,15 +4,23 @@
{% trans "Search maps" as default_placeholder %}
<div class="wrapper search_wrapper">
<div class="row">
<form action="{% firstof action search_url %}" method="get">
<div class="col two-third mwide">
<form class="search-form" action="{% firstof action search_url %}" method="get">
<div class="col half mwide">
<input name="q"
type="search"
placeholder="{% firstof placeholder default_placeholder %}"
aria-label="{% firstof placeholder default_placeholder %}"
value="{{ request.GET.q|default:"" }}" />
</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" />
</div>
</form>

View file

@ -226,3 +226,18 @@ def test_hover_tooltip_setting_should_be_persistent(live_server, map, page):
- text: always never on hover
""")
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")
response = client.get(url + "?q=Blé")
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:
def get_search_queryset(self, **kwargs):
q = self.request.GET.get("q")
tags = [t for t in self.request.GET.getlist("tags") if t]
qs = Map.objects.all()
if q:
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
query = SearchQuery(
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):