Skip to content

Commit bc97e15

Browse files
PartyNellsubmarcos
andauthored
Add configurable popups on markers (#339)
* fix tests * create a popup with configurable arguments for markers * use HTML template for popup API * add tests * add documentation about popups * add changelog entry * fix quality * change popup's title with str(obj) * change traduction * improve text formatting in popup content api * adapt tests * adapt documentation * add parameter to display popup or not * add changelog entry * change LABEL_PER_MODEL setting name * Update mapentity/templates/mapentity/mapentity_popup_content.html Co-authored-by: Jean-Etienne Castagnede <[email protected]> * remove spaces in the popup * fix tests * add get_popup_url in the model * fix quality * apply suggestions * modify magin and padding of popup * add traduction of field name for boolean fields in popup --------- Co-authored-by: Jean-Etienne Castagnede <[email protected]>
1 parent fe038ee commit bc97e15

File tree

16 files changed

+182
-13
lines changed

16 files changed

+182
-13
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ CHANGELOG
44
8.14.5+dev (XXXX-XX-XX)
55
-----------------------
66

7+
**Improvements**
8+
9+
- Add popup on marker with configurable information
10+
711
**Bug fixes**
812

913
- Fix filter context restoration
1014
- Fix layer restoration in screenshot
1115

1216

17+
1318
8.14.5 (2025-10-30)
1419
-----------------------
1520

docs/customization.rst

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,38 @@ The second way overrides these templates for all your models.
197197
you need to create a sub-directory named ``mapentity`` in ``main/templates``.
198198
Then you can create a file named ``override_detail_pdf.html``(or ``.css``) and it will be used for all your models if a specific template is not provided.
199199

200+
Popups
201+
------
202+
203+
MapEntity displays a popup when clicking on a map object.
204+
By default, it shows the object's name and a button linking to its detail page.
205+
206+
Configure popup fields for each model using the ``POPUP_CONTENT`` setting:
207+
208+
.. code-block:: python
209+
210+
POPUP_CONTENT = {
211+
"museum": ["name", "city", "public"],
212+
}
213+
214+
The key is the model name in lowercase (e.g., ``"museum"``), and the value is a list of field names to display.
215+
The popup title shows the object's string representation (using `__str__()`), with the configured fields displayed below.
216+
217+
If there is a display function for a field in the model then it will be used in priority.
218+
219+
.. code-block:: python
220+
221+
@property
222+
def public_display(self):
223+
return "Public" if self.public else "Not public"
224+
225+
If no display function exists, the field’s string representation is used by default.
226+
227+
Non-existent fields can be used if a display function exist.
228+
229+
If a model isn't configured in ``POPUP_CONTENT``, the object's string representation is used as the title.
230+
If a specified field doesn't exist on the model, it won't be displayed. The detail page button is always shown.
231+
If the option ``displayPopup`` is setup to false, then the popup will not appear when clicking on the feature.
200232

201233
Settings
202234
-----------
@@ -256,4 +288,4 @@ A help message will be added, and color of TinyMCE status bar and border will be
256288
MAPENTITY_CONFIG['MAX_CHARACTERS_BY_FIELD'] = {
257289
"tourism_touristicevent": [{'field': 'description_teaser_fr', 'value': 50}, {'field': 'accessibility_fr', 'value': 25}],
258290
"trekking_trek": [{'field': 'description_teaser_fr', 'value': 150}],
259-
}
291+
}

mapentity/locale/en/LC_MESSAGES/djangojs.po

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ msgid ""
88
msgstr ""
99
"Project-Id-Version: PACKAGE VERSION\n"
1010
"Report-Msgid-Bugs-To: \n"
11-
"POT-Creation-Date: 2025-09-02 15:19+0000\n"
11+
"POT-Creation-Date: 2025-10-24 13:31+0000\n"
1212
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414
"Language-Team: LANGUAGE <[email protected]>\n"
@@ -18,5 +18,8 @@ msgstr ""
1818
"Content-Transfer-Encoding: 8bit\n"
1919
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
2020

21+
msgid "Data unreachable"
22+
msgstr ""
23+
2124
msgid "Locate me"
2225
msgstr ""

mapentity/locale/fr/LC_MESSAGES/djangojs.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ msgstr ""
1818
"Plural-Forms: nplurals=2; plural=n > 1;\n"
1919
"X-Generator: Weblate 5.13.3\n"
2020

21+
msgid "Data unreachable"
22+
msgstr "Données inaccessibles"
23+
2124
msgid "Locate me"
2225
msgstr "Localisez-moi"

mapentity/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ def get_generic_detail_url(cls):
282282
def get_detail_url(self):
283283
return reverse(self._entity.url_name(ENTITY_DETAIL), args=[str(self.pk)])
284284

285+
def get_popup_url(self):
286+
return reverse(
287+
f"{self._meta.app_label.lower()}:{self._meta.model_name.lower()}-drf-popup-content",
288+
kwargs={"pk": self.pk},
289+
)
290+
285291
@property
286292
def map_image_url(self):
287293
return self.get_map_image_url()

mapentity/serializers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
MapentityGeojsonModelSerializer,
66
)
77
from .gpx import GPXSerializer
8-
from .helpers import json_django_dumps, plain_text, smart_plain_text
8+
from .helpers import field_as_string, json_django_dumps, plain_text, smart_plain_text
99
from .shapefile import ZipShapeSerializer
1010

1111
__all__ = [
1212
"plain_text",
1313
"smart_plain_text",
14+
"field_as_string",
1415
"CSVSerializer",
1516
"GPXSerializer",
1617
"MapentityDatatableSerializer",

mapentity/serializers/helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ def field_as_string(obj, field, ascii=False):
2727
or isinstance(value, Decimal)
2828
):
2929
value = number_format(value)
30+
if hasattr(value, "all"):
31+
# convert ManyRelatedManager to QuerySet
32+
value = value.all()
3033
if isinstance(value, list) or isinstance(value, QuerySet):
3134
value = ", ".join([str(val) for val in value])
3235
return smart_plain_text(value, ascii)

mapentity/static/mapentity/leaflet-objectslayer.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ L.ObjectsLayer = L.GeoJSON.extend({
33
indexing: true,
44
highlight: true,
55
objectUrl: null,
6+
displayPopup: true,
67
styles: {
78
'default': {'color': 'blue', 'weight': 2, 'opacity': 0.8},
89
highlight: {'color': 'red', 'weight': 5, 'opacity': 1},
@@ -69,13 +70,25 @@ L.ObjectsLayer = L.GeoJSON.extend({
6970
}
7071
}, this));
7172

72-
73-
// Optionnaly make them clickable
74-
if (this.options.objectUrl) {
75-
this.on('click', function(e) {
76-
window.location = this.options.objectUrl(e.layer.properties, e.layer);
77-
}, this);
78-
}
73+
this.on('click', async function (e) {
74+
if(this.options.displayPopup){
75+
var popup_content;
76+
try{
77+
popup_content = await this.getPopupContent(e.layer);
78+
} catch (error) {
79+
popup_content = gettext('Data unreachable');
80+
}
81+
if (e.target._popup){
82+
// update popup content if it has been already bind
83+
var popup = e.target._popup;
84+
popup.setContent(popup_content);
85+
popup.update();
86+
} else {
87+
// bind a new popup
88+
this.bindPopup(popup_content).openPopup(e.latlng);
89+
}
90+
}
91+
}, this);
7992

8093
var dataurl = null;
8194
if (typeof(geojson) == 'string') {
@@ -220,8 +233,26 @@ L.ObjectsLayer = L.GeoJSON.extend({
220233
layer._defaultStyle = this.options.styles['default'];
221234
layer.setStyle(layer._defaultStyle);
222235
}
223-
}
224-
});
236+
},
225237

238+
getPopupContent: async function (layer){
239+
const popup_url = window.SETTINGS.urls.popup.replace(new RegExp('modelname', 'g'), this.options.modelname)
240+
.replace('0', layer.properties.id);
241+
242+
// fetch data
243+
var response = await window.fetch(popup_url);
244+
if (!response.ok){
245+
throw new Error(`HTTP error! Status: ${response.status}`);
246+
} else {
247+
// parse data
248+
try {
249+
return response.json();
250+
} catch (error) {
251+
throw new Error('Cannot parse data');
252+
}
253+
}
254+
},
255+
256+
});
226257

227258
L.ObjectsLayer.include(L.LayerIndexMixin);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="d-flex flex-column justify-content-center">
2+
<p class="text-center m-0 p-1"><strong>{{ title }}</strong></p>
3+
{% if attributes %}
4+
<p class="m-0 p-1">
5+
{% for field in attributes %}{{ field|truncatechars:100 }}<br>{% endfor %}
6+
</p>
7+
{% endif %}
8+
<button id="detail-btn" class="btn btn-sm btn-info mt-2" onclick="window.location.href='{{ detail_url }}'">{{ button_label }}</button>
9+
</div>

mapentity/tests/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def get_expected_geojson_attrs(self):
5757
def get_expected_datatables_attrs(self):
5858
return {}
5959

60+
def get_expected_popup_content(self):
61+
return ""
62+
6063
def setUp(self):
6164
if self.user:
6265
self.client.force_login(user=self.user)
@@ -444,6 +447,17 @@ def test_api_geojson_list_for_model(self):
444447
},
445448
)
446449

450+
def test_api_popup_content(self):
451+
if self.model is None:
452+
return # Abstract test should not run
453+
454+
self.obj = self.modelfactory.create()
455+
popup_url = self.obj.get_popup_url()
456+
response = self.client.get(popup_url)
457+
self.assertEqual(response.status_code, 200, f"{popup_url} not found")
458+
content_json = response.json()
459+
self.assertEqual(content_json, self.get_expected_popup_content())
460+
447461

448462
class MapEntityLiveTest(LiveServerTestCase):
449463
model = None

0 commit comments

Comments
 (0)