diff --git a/umap/forms.py b/umap/forms.py
index e982799e..b9503d76 100644
--- a/umap/forms.py
+++ b/umap/forms.py
@@ -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
diff --git a/umap/migrations/0027_map_tags.py b/umap/migrations/0027_map_tags.py
new file mode 100644
index 00000000..e55d45e2
--- /dev/null
+++ b/umap/migrations/0027_map_tags.py
@@ -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,
+ ),
+ ),
+ ]
diff --git a/umap/models.py b/umap/models.py
index 36404794..66851787 100644
--- a/umap/models.py
+++ b/umap/models.py
@@ -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},
}
diff --git a/umap/settings/base.py b/umap/settings/base.py
index ecb51326..ac1ff865 100644
--- a/umap/settings/base.py
+++ b/umap/settings/base.py
@@ -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,19 @@ UMAP_HOME_FEED = "latest"
UMAP_IMPORTERS = {}
UMAP_HOST_INFOS = {}
UMAP_LABEL_KEYS = ["name", "title"]
+UMAP_TAGS = (
+ ("art", _("Art and Culture")),
+ ("bike", _("Bike")),
+ ("environment", _("Environment")),
+ ("education", _("Education")),
+ ("food", _("Food and Agriculture")),
+ ("history", _("History")),
+ ("public", _("Public sector")),
+ ("sport", _("Sport and Leisure")),
+ ("travel", _("Travel")),
+ ("trekking", _("Trekking")),
+ ("tourism", _("Tourism")),
+)
UMAP_READONLY = env("UMAP_READONLY", default=False)
UMAP_GZIP = True
diff --git a/umap/static/umap/js/modules/form/builder.js b/umap/static/umap/js/modules/form/builder.js
index 88698d39..70d0926c 100644
--- a/umap/static/umap/js/modules/form/builder.js
+++ b/umap/static/umap/js/modules/form/builder.js
@@ -138,6 +138,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,
diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js
index 5a8be8d6..8d9903b2 100644
--- a/umap/static/umap/js/modules/form/fields.js
+++ b/umap/static/umap/js/modules/form/fields.js
@@ -324,6 +324,24 @@ Fields.CheckBox = class extends BaseElement {
}
}
+Fields.CheckBoxes = class extends BaseElement {
+ build() {
+ const initial = this.get() || []
+ for (const [value, label] of this.properties.choices) {
+ const tpl = ``
+ const [root, { input }] = Utils.loadTemplateWithRefs(tpl)
+ 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.Select = class extends BaseElement {
getTemplate() {
return ``
@@ -1296,12 +1314,13 @@ 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() {
diff --git a/umap/static/umap/js/modules/schema.js b/umap/static/umap/js/modules/schema.js
index 24a44d0b..1deac31b 100644
--- a/umap/static/umap/js/modules/schema.js
+++ b/umap/static/umap/js/modules/schema.js
@@ -516,6 +516,9 @@ export const SCHEMA = {
helpEntries: ['sync'],
default: false,
},
+ tags: {
+ type: Array,
+ },
team: {
type: Object,
},
diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js
index 82733692..426cc30a 100644
--- a/umap/static/umap/js/modules/umap.js
+++ b/umap/static/umap/js/modules/umap.js
@@ -755,6 +755,12 @@ export default class Umap {
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',
@@ -1168,6 +1174,7 @@ export default class Umap {
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)
diff --git a/umap/tests/integration/test_edit_map.py b/umap/tests/integration/test_edit_map.py
index 7bc6ca5f..a64542ae 100644
--- a/umap/tests/integration/test_edit_map.py
+++ b/umap/tests/integration/test_edit_map.py
@@ -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"]