diff --git a/docs/config/settings.md b/docs/config/settings.md index 5211f7b5..c6b103f6 100644 --- a/docs/config/settings.md +++ b/docs/config/settings.md @@ -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: diff --git a/umap/context_processors.py b/umap/context_processors.py index fcabcfb4..6064a152 100644 --- a/umap/context_processors.py +++ b/umap/context_processors.py @@ -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, } 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 265c52eb..3547a2c8 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..e741b5b8 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,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 diff --git a/umap/static/umap/img/tags/arts.svg b/umap/static/umap/img/tags/arts.svg new file mode 100644 index 00000000..bfbb0a4e --- /dev/null +++ b/umap/static/umap/img/tags/arts.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/business.svg b/umap/static/umap/img/tags/business.svg new file mode 100644 index 00000000..3d013b87 --- /dev/null +++ b/umap/static/umap/img/tags/business.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/cycling.svg b/umap/static/umap/img/tags/cycling.svg new file mode 100644 index 00000000..b2758b26 --- /dev/null +++ b/umap/static/umap/img/tags/cycling.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/education.svg b/umap/static/umap/img/tags/education.svg new file mode 100644 index 00000000..a1275493 --- /dev/null +++ b/umap/static/umap/img/tags/education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/environment.svg b/umap/static/umap/img/tags/environment.svg new file mode 100644 index 00000000..b2cc25b0 --- /dev/null +++ b/umap/static/umap/img/tags/environment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/food.svg b/umap/static/umap/img/tags/food.svg new file mode 100644 index 00000000..c99d0a0d --- /dev/null +++ b/umap/static/umap/img/tags/food.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/geopolitics.svg b/umap/static/umap/img/tags/geopolitics.svg new file mode 100644 index 00000000..c78ca4d3 --- /dev/null +++ b/umap/static/umap/img/tags/geopolitics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/health.svg b/umap/static/umap/img/tags/health.svg new file mode 100644 index 00000000..1a85bc33 --- /dev/null +++ b/umap/static/umap/img/tags/health.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/hiking.svg b/umap/static/umap/img/tags/hiking.svg new file mode 100644 index 00000000..a9998188 --- /dev/null +++ b/umap/static/umap/img/tags/hiking.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/history.svg b/umap/static/umap/img/tags/history.svg new file mode 100644 index 00000000..e11ac259 --- /dev/null +++ b/umap/static/umap/img/tags/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/public.svg b/umap/static/umap/img/tags/public.svg new file mode 100644 index 00000000..0b0a22a3 --- /dev/null +++ b/umap/static/umap/img/tags/public.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/science.svg b/umap/static/umap/img/tags/science.svg new file mode 100644 index 00000000..62455547 --- /dev/null +++ b/umap/static/umap/img/tags/science.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/shopping.svg b/umap/static/umap/img/tags/shopping.svg new file mode 100644 index 00000000..743c09b9 --- /dev/null +++ b/umap/static/umap/img/tags/shopping.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/sport.svg b/umap/static/umap/img/tags/sport.svg new file mode 100644 index 00000000..a439d17d --- /dev/null +++ b/umap/static/umap/img/tags/sport.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/tourism.svg b/umap/static/umap/img/tags/tourism.svg new file mode 100644 index 00000000..6c9253a9 --- /dev/null +++ b/umap/static/umap/img/tags/tourism.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/transports.svg b/umap/static/umap/img/tags/transports.svg new file mode 100644 index 00000000..acaaf9d7 --- /dev/null +++ b/umap/static/umap/img/tags/transports.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/img/tags/travel.svg b/umap/static/umap/img/tags/travel.svg new file mode 100644 index 00000000..3a506124 --- /dev/null +++ b/umap/static/umap/img/tags/travel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/umap/static/umap/js/modules/form/builder.js b/umap/static/umap/js/modules/form/builder.js index d0461036..0a282138 100644 --- a/umap/static/umap/js/modules/form/builder.js +++ b/umap/static/umap/js/modules/form/builder.js @@ -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, diff --git a/umap/static/umap/js/modules/form/fields.js b/umap/static/umap/js/modules/form/fields.js index 130b0d85..fe221518 100644 --- a/umap/static/umap/js/modules/form/fields.js +++ b/umap/static/umap/js/modules/form/fields.js @@ -324,6 +324,41 @@ Fields.CheckBox = class extends BaseElement { } } +Fields.CheckBoxes = class extends BaseElement { + getInputTemplate(value, label) { + return `` + } + + 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 ` + + ` + } +} + Fields.Select = class extends BaseElement { getTemplate() { return `` @@ -1296,12 +1331,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 d2142f8c..03936afd 100644 --- a/umap/static/umap/js/modules/schema.js +++ b/umap/static/umap/js/modules/schema.js @@ -500,6 +500,9 @@ export const SCHEMA = { helpEntries: ['sync'], default: false, }, + tags: { + type: Array, + }, tilelayer: { type: Object, impacts: ['background'], diff --git a/umap/static/umap/js/modules/umap.js b/umap/static/umap/js/modules/umap.js index add17341..e4c69bc7 100644 --- a/umap/static/umap/js/modules/umap.js +++ b/umap/static/umap/js/modules/umap.js @@ -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) diff --git a/umap/static/umap/map.css b/umap/static/umap/map.css index 87ee6c2d..864644cb 100644 --- a/umap/static/umap/map.css +++ b/umap/static/umap/map.css @@ -955,6 +955,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 */ /* *************************** */ diff --git a/umap/templates/umap/search_bar.html b/umap/templates/umap/search_bar.html index 701a0fb0..c3f22b0e 100644 --- a/umap/templates/umap/search_bar.html +++ b/umap/templates/umap/search_bar.html @@ -5,14 +5,22 @@