umap-project 2.9.3__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (217) hide show
  1. umap/__init__.py +1 -1
  2. umap/context_processors.py +1 -0
  3. umap/forms.py +1 -2
  4. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  5. umap/locale/de/LC_MESSAGES/django.po +218 -96
  6. umap/locale/en/LC_MESSAGES/django.po +128 -52
  7. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/fr/LC_MESSAGES/django.po +128 -52
  9. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/hu/LC_MESSAGES/django.po +209 -88
  11. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/is/LC_MESSAGES/django.po +296 -175
  13. umap/migrations/0027_map_tags.py +23 -0
  14. umap/models.py +13 -2
  15. umap/settings/base.py +23 -5
  16. umap/static/umap/base.css +41 -8
  17. umap/static/umap/content.css +72 -37
  18. umap/static/umap/css/bar.css +43 -21
  19. umap/static/umap/css/dialog.css +4 -1
  20. umap/static/umap/css/form.css +40 -27
  21. umap/static/umap/css/icon.css +11 -1
  22. umap/static/umap/css/importers.css +7 -0
  23. umap/static/umap/img/16-white.svg +23 -2
  24. umap/static/umap/img/16.svg +1 -1
  25. umap/static/umap/img/24.svg +4 -4
  26. umap/static/umap/img/home.svg +7 -0
  27. umap/static/umap/img/importers/banfr.svg +1 -0
  28. umap/static/umap/img/marker.svg +2 -5
  29. umap/static/umap/img/source/16-white.svg +24 -3
  30. umap/static/umap/img/source/16.svg +1 -1
  31. umap/static/umap/img/source/24.svg +5 -5
  32. umap/static/umap/img/target.svg +1 -0
  33. umap/static/umap/js/components/alerts/alert.js +0 -1
  34. umap/static/umap/js/modules/browser.js +4 -4
  35. umap/static/umap/js/modules/caption.js +1 -1
  36. umap/static/umap/js/modules/data/features.js +25 -25
  37. umap/static/umap/js/modules/data/layer.js +91 -97
  38. umap/static/umap/js/modules/facets.js +9 -5
  39. umap/static/umap/js/modules/form/builder.js +21 -29
  40. umap/static/umap/js/modules/form/fields.js +40 -14
  41. umap/static/umap/js/modules/formatter.js +1 -1
  42. umap/static/umap/js/modules/global.js +9 -5
  43. umap/static/umap/js/modules/help.js +18 -5
  44. umap/static/umap/js/modules/importer.js +5 -2
  45. umap/static/umap/js/modules/importers/banfr.js +93 -0
  46. umap/static/umap/js/modules/importers/cadastrefr.js +2 -2
  47. umap/static/umap/js/modules/importers/communesfr.js +1 -1
  48. umap/static/umap/js/modules/permissions.js +20 -10
  49. umap/static/umap/js/modules/rendering/icon.js +15 -2
  50. umap/static/umap/js/modules/rendering/layers/classified.js +7 -7
  51. umap/static/umap/js/modules/rendering/layers/cluster.js +2 -2
  52. umap/static/umap/js/modules/rendering/layers/heat.js +4 -4
  53. umap/static/umap/js/modules/rendering/map.js +14 -6
  54. umap/static/umap/js/modules/rendering/popup.js +2 -2
  55. umap/static/umap/js/modules/rendering/template.js +3 -3
  56. umap/static/umap/js/modules/rendering/ui.js +17 -11
  57. umap/static/umap/js/modules/rules.js +13 -16
  58. umap/static/umap/js/modules/schema.js +23 -1
  59. umap/static/umap/js/modules/share.js +1 -1
  60. umap/static/umap/js/modules/slideshow.js +1 -0
  61. umap/static/umap/js/modules/sync/engine.js +141 -19
  62. umap/static/umap/js/modules/sync/undo.js +101 -0
  63. umap/static/umap/js/modules/sync/updaters.js +51 -28
  64. umap/static/umap/js/modules/tableeditor.js +1 -1
  65. umap/static/umap/js/modules/ui/bar.js +61 -21
  66. umap/static/umap/js/modules/ui/tooltip.js +1 -1
  67. umap/static/umap/js/modules/umap.js +190 -176
  68. umap/static/umap/js/modules/utils.js +30 -4
  69. umap/static/umap/js/umap.controls.js +82 -38
  70. umap/static/umap/locale/am_ET.js +11 -6
  71. umap/static/umap/locale/am_ET.json +11 -6
  72. umap/static/umap/locale/ar.js +11 -6
  73. umap/static/umap/locale/ar.json +11 -6
  74. umap/static/umap/locale/ast.js +11 -6
  75. umap/static/umap/locale/ast.json +11 -6
  76. umap/static/umap/locale/bg.js +11 -6
  77. umap/static/umap/locale/bg.json +11 -6
  78. umap/static/umap/locale/br.js +12 -7
  79. umap/static/umap/locale/br.json +12 -7
  80. umap/static/umap/locale/ca.js +11 -6
  81. umap/static/umap/locale/ca.json +11 -6
  82. umap/static/umap/locale/cs_CZ.js +11 -6
  83. umap/static/umap/locale/cs_CZ.json +11 -6
  84. umap/static/umap/locale/da.js +11 -6
  85. umap/static/umap/locale/da.json +11 -6
  86. umap/static/umap/locale/de.js +47 -42
  87. umap/static/umap/locale/de.json +47 -42
  88. umap/static/umap/locale/el.js +11 -6
  89. umap/static/umap/locale/el.json +11 -6
  90. umap/static/umap/locale/en.js +11 -6
  91. umap/static/umap/locale/en.json +11 -6
  92. umap/static/umap/locale/en_US.json +11 -6
  93. umap/static/umap/locale/es.js +11 -6
  94. umap/static/umap/locale/es.json +11 -6
  95. umap/static/umap/locale/et.js +11 -6
  96. umap/static/umap/locale/et.json +11 -6
  97. umap/static/umap/locale/eu.js +11 -6
  98. umap/static/umap/locale/eu.json +11 -6
  99. umap/static/umap/locale/fa_IR.js +11 -6
  100. umap/static/umap/locale/fa_IR.json +11 -6
  101. umap/static/umap/locale/fi.js +11 -6
  102. umap/static/umap/locale/fi.json +11 -6
  103. umap/static/umap/locale/fr.js +11 -6
  104. umap/static/umap/locale/fr.json +11 -6
  105. umap/static/umap/locale/gl.js +12 -7
  106. umap/static/umap/locale/gl.json +12 -7
  107. umap/static/umap/locale/he.js +11 -6
  108. umap/static/umap/locale/he.json +11 -6
  109. umap/static/umap/locale/hr.js +11 -6
  110. umap/static/umap/locale/hr.json +11 -6
  111. umap/static/umap/locale/hu.js +25 -20
  112. umap/static/umap/locale/hu.json +25 -20
  113. umap/static/umap/locale/id.js +11 -6
  114. umap/static/umap/locale/id.json +11 -6
  115. umap/static/umap/locale/is.js +151 -146
  116. umap/static/umap/locale/is.json +151 -146
  117. umap/static/umap/locale/it.js +11 -6
  118. umap/static/umap/locale/it.json +11 -6
  119. umap/static/umap/locale/ja.js +11 -6
  120. umap/static/umap/locale/ja.json +11 -6
  121. umap/static/umap/locale/ko.js +11 -6
  122. umap/static/umap/locale/ko.json +11 -6
  123. umap/static/umap/locale/lt.js +11 -6
  124. umap/static/umap/locale/lt.json +11 -6
  125. umap/static/umap/locale/ms.js +11 -6
  126. umap/static/umap/locale/ms.json +11 -6
  127. umap/static/umap/locale/nl.js +12 -7
  128. umap/static/umap/locale/nl.json +12 -7
  129. umap/static/umap/locale/no.js +11 -6
  130. umap/static/umap/locale/no.json +11 -6
  131. umap/static/umap/locale/pl.js +11 -6
  132. umap/static/umap/locale/pl.json +11 -6
  133. umap/static/umap/locale/pl_PL.json +11 -6
  134. umap/static/umap/locale/pt.js +11 -6
  135. umap/static/umap/locale/pt.json +11 -6
  136. umap/static/umap/locale/pt_BR.js +11 -6
  137. umap/static/umap/locale/pt_BR.json +11 -6
  138. umap/static/umap/locale/pt_PT.js +11 -6
  139. umap/static/umap/locale/pt_PT.json +11 -6
  140. umap/static/umap/locale/ro.js +11 -6
  141. umap/static/umap/locale/ro.json +11 -6
  142. umap/static/umap/locale/ru.js +11 -6
  143. umap/static/umap/locale/ru.json +11 -6
  144. umap/static/umap/locale/sk_SK.js +11 -6
  145. umap/static/umap/locale/sk_SK.json +11 -6
  146. umap/static/umap/locale/sl.js +11 -6
  147. umap/static/umap/locale/sl.json +11 -6
  148. umap/static/umap/locale/sr.js +11 -6
  149. umap/static/umap/locale/sr.json +11 -6
  150. umap/static/umap/locale/sv.js +11 -6
  151. umap/static/umap/locale/sv.json +11 -6
  152. umap/static/umap/locale/th_TH.js +11 -6
  153. umap/static/umap/locale/th_TH.json +11 -6
  154. umap/static/umap/locale/tr.js +11 -6
  155. umap/static/umap/locale/tr.json +11 -6
  156. umap/static/umap/locale/uk_UA.js +11 -6
  157. umap/static/umap/locale/uk_UA.json +11 -6
  158. umap/static/umap/locale/vi.js +11 -6
  159. umap/static/umap/locale/vi.json +11 -6
  160. umap/static/umap/locale/vi_VN.json +11 -6
  161. umap/static/umap/locale/zh.js +11 -6
  162. umap/static/umap/locale/zh.json +11 -6
  163. umap/static/umap/locale/zh_CN.json +11 -6
  164. umap/static/umap/locale/zh_TW.Big5.json +11 -6
  165. umap/static/umap/locale/zh_TW.js +19 -14
  166. umap/static/umap/locale/zh_TW.json +19 -14
  167. umap/static/umap/map.css +58 -28
  168. umap/static/umap/unittests/sync.js +0 -57
  169. umap/static/umap/unittests/utils.js +47 -0
  170. umap/static/umap/vars.css +5 -2
  171. umap/static/umap/vendors/photon/leaflet.photon.js +3 -0
  172. umap/sync/payloads.py +3 -2
  173. umap/templates/auth/user_detail.html +1 -1
  174. umap/templates/auth/user_stars.html +1 -1
  175. umap/templates/umap/content.html +17 -12
  176. umap/templates/umap/home.html +7 -5
  177. umap/templates/umap/map_fragment.html +1 -1
  178. umap/templates/umap/map_list.html +20 -13
  179. umap/templates/umap/search.html +7 -3
  180. umap/templates/umap/search_bar.html +13 -11
  181. umap/templates/umap/team_detail.html +1 -1
  182. umap/tests/base.py +2 -1
  183. umap/tests/fixtures/remote_data.umap +55 -0
  184. umap/tests/fixtures/test_upload_data_with_iconurl.umap +122 -0
  185. umap/tests/integration/test_browser.py +1 -3
  186. umap/tests/integration/test_conditional_rules.py +3 -0
  187. umap/tests/integration/test_edit_datalayer.py +2 -7
  188. umap/tests/integration/test_edit_map.py +15 -0
  189. umap/tests/integration/test_edit_polygon.py +1 -2
  190. umap/tests/integration/test_import.py +59 -2
  191. umap/tests/integration/test_optimistic_merge.py +4 -3
  192. umap/tests/integration/test_owned_map.py +0 -1
  193. umap/tests/integration/test_save.py +2 -4
  194. umap/tests/integration/test_undo_redo.py +267 -0
  195. umap/tests/integration/test_websocket_sync.py +78 -11
  196. umap/tests/settings.py +1 -3
  197. umap/tests/test_datalayer_s3.py +1 -0
  198. umap/tests/test_map_views.py +1 -0
  199. umap/tests/test_views.py +34 -0
  200. umap/utils.py +1 -1
  201. umap/views.py +23 -2
  202. {umap_project-2.9.3.dist-info → umap_project-3.0.0.dist-info}/METADATA +13 -12
  203. {umap_project-2.9.3.dist-info → umap_project-3.0.0.dist-info}/RECORD +206 -208
  204. umap/static/umap/js/modules/saving.js +0 -52
  205. umap/static/umap/test/.eslintrc +0 -21
  206. umap/static/umap/test/DataLayer.js +0 -463
  207. umap/static/umap/test/Feature.js +0 -131
  208. umap/static/umap/test/Map.js +0 -37
  209. umap/static/umap/test/Marker.js +0 -126
  210. umap/static/umap/test/Polygon.js +0 -111
  211. umap/static/umap/test/Polyline.js +0 -286
  212. umap/static/umap/test/Util.js +0 -28
  213. umap/static/umap/test/_pre.js +0 -455
  214. umap/static/umap/test/index.html +0 -139
  215. {umap_project-2.9.3.dist-info → umap_project-3.0.0.dist-info}/WHEEL +0 -0
  216. {umap_project-2.9.3.dist-info → umap_project-3.0.0.dist-info}/entry_points.txt +0 -0
  217. {umap_project-2.9.3.dist-info → umap_project-3.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,267 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from playwright.sync_api import expect
6
+
7
+ from umap.models import Map, TileLayer
8
+
9
+ from ..base import DataLayerFactory
10
+
11
+ pytestmark = pytest.mark.django_db
12
+
13
+ DATALAYER_DATA = {
14
+ "type": "FeatureCollection",
15
+ "features": [
16
+ {
17
+ "type": "Feature",
18
+ "properties": {
19
+ "name": "name poly",
20
+ },
21
+ "id": "gyNzM",
22
+ "geometry": {
23
+ "type": "Polygon",
24
+ "coordinates": [
25
+ [
26
+ [11.25, 53.585984],
27
+ [10.151367, 52.975108],
28
+ [12.689209, 52.167194],
29
+ [14.084473, 53.199452],
30
+ [12.634277, 53.618579],
31
+ [11.25, 53.585984],
32
+ [11.25, 53.585984],
33
+ ],
34
+ ],
35
+ },
36
+ },
37
+ ],
38
+ }
39
+
40
+
41
+ @pytest.fixture
42
+ def map_with_polygon(map, live_server):
43
+ map.settings["properties"]["zoom"] = 6
44
+ map.settings["geometry"] = {
45
+ "type": "Point",
46
+ "coordinates": [8.429, 53.239],
47
+ }
48
+ map.edit_status = Map.ANONYMOUS
49
+ map.save()
50
+ DataLayerFactory(map=map, data=DATALAYER_DATA)
51
+ return map
52
+
53
+
54
+ def test_can_undo_redo_map_name_change(page, live_server, tilelayer):
55
+ page.goto(f"{live_server.url}/en/map/new/")
56
+
57
+ expect(page.locator(".edit-undo")).to_be_disabled()
58
+ expect(page.locator(".edit-redo")).to_be_disabled()
59
+ page.get_by_title("Edit map name and caption").click()
60
+ name_input = page.locator('.map-metadata input[name="name"]')
61
+ expect(name_input).to_be_visible()
62
+ name_input.click()
63
+ name_input.press("Control+a")
64
+ name_input.fill("New map name")
65
+ expect(page.locator(".edit-undo")).to_be_enabled()
66
+ expect(page.locator(".edit-redo")).to_be_disabled()
67
+ map_name = page.locator(".umap-main-edit-toolbox .map-name")
68
+ expect(map_name).to_have_text("New map name")
69
+ name_input.fill("New name again")
70
+ expect(map_name).to_have_text("New name again")
71
+
72
+ page.locator(".edit-undo").click()
73
+ expect(map_name).to_have_text("New map name")
74
+ expect(page.locator(".edit-undo")).to_be_enabled()
75
+ expect(page.locator(".edit-redo")).to_be_enabled()
76
+
77
+ page.locator(".edit-redo").click()
78
+ expect(map_name).to_have_text("New name again")
79
+ expect(page.locator(".edit-undo")).to_be_enabled()
80
+ expect(page.locator(".edit-redo")).to_be_disabled()
81
+
82
+ page.locator(".edit-undo").click()
83
+ expect(map_name).to_have_text("New map name")
84
+ expect(page.locator(".edit-undo")).to_be_enabled()
85
+ expect(page.locator(".edit-redo")).to_be_enabled()
86
+
87
+
88
+ def test_can_undo_redo_layer_color_change(
89
+ page, map_with_polygon, live_server, tilelayer
90
+ ):
91
+ page.goto(f"{live_server.url}{map_with_polygon.get_absolute_url()}?edit")
92
+
93
+ expect(page.locator(".edit-undo")).to_be_disabled()
94
+ expect(page.locator(".edit-redo")).to_be_disabled()
95
+ page.get_by_role("button", name="Manage layers").click()
96
+ page.locator(".panel").get_by_title("Edit", exact=True).click()
97
+ page.get_by_text("Shape properties").click()
98
+ page.locator(".umap-field-color .define").click()
99
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
100
+ page.get_by_title("DarkRed").first.click()
101
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
102
+ expect(page.locator(".edit-undo")).to_be_enabled()
103
+ expect(page.locator(".edit-redo")).to_be_disabled()
104
+
105
+ page.locator(".edit-undo").click()
106
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
107
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(0)
108
+ expect(page.locator(".edit-undo")).to_be_disabled()
109
+ expect(page.locator(".edit-redo")).to_be_enabled()
110
+
111
+ page.locator(".edit-redo").click()
112
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
113
+ expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(0)
114
+ expect(page.locator(".edit-undo")).to_be_enabled()
115
+ expect(page.locator(".edit-redo")).to_be_disabled()
116
+
117
+
118
+ def test_can_undo_redo_tilelayer_change(live_server, page, openmap, tilelayer):
119
+ TileLayer.objects.create(
120
+ url_template="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
121
+ attribution="OSM/Carto",
122
+ name="Black Tiles",
123
+ )
124
+ page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
125
+ old_pattern = re.compile(
126
+ r"https://[abc]{1}.tile.openstreetmap.fr/osmfr/\d+/\d+/\d+.png"
127
+ )
128
+ tiles = page.locator(".leaflet-tile-pane img")
129
+ expect(tiles.first).to_have_attribute("src", old_pattern)
130
+
131
+ new_pattern = re.compile(
132
+ r"https://[abcd]{1}.basemaps.cartocdn.com/dark_all/\d+/\d+/\d+.png"
133
+ )
134
+ page.get_by_role("button", name="Change tilelayers").click()
135
+ page.locator("li").filter(has_text="Black Tiles").get_by_role("img").click()
136
+
137
+ tiles = page.locator(".leaflet-tile-pane img")
138
+ expect(tiles.first).to_have_attribute("src", new_pattern)
139
+
140
+ page.locator(".edit-undo").click()
141
+ tiles = page.locator(".leaflet-tile-pane img")
142
+ expect(tiles.first).to_have_attribute("src", old_pattern)
143
+
144
+ page.locator(".edit-redo").click()
145
+ tiles = page.locator(".leaflet-tile-pane img")
146
+ expect(tiles.first).to_have_attribute("src", new_pattern)
147
+
148
+
149
+ def test_can_undo_redo_marker_drag(live_server, page, tilelayer):
150
+ page.goto(f"{live_server.url}/en/map/new")
151
+
152
+ marker = page.locator(".leaflet-marker-icon")
153
+ map = page.locator("#map")
154
+
155
+ # Create a marker
156
+ page.get_by_title("Draw a marker").click()
157
+ map.click(position={"x": 225, "y": 225})
158
+ expect(marker).to_have_count(1)
159
+
160
+ # Drag marker
161
+ old_bbox = marker.bounding_box()
162
+ marker.first.drag_to(map, target_position={"x": 250, "y": 250})
163
+ assert marker.bounding_box() != old_bbox
164
+
165
+ # Undo
166
+ page.locator(".edit-undo").click()
167
+ assert marker.bounding_box() == old_bbox
168
+
169
+ # Redo
170
+ page.locator(".edit-redo").click()
171
+ assert marker.bounding_box() != old_bbox
172
+
173
+
174
+ def test_can_undo_redo_polygon_geometry_change(live_server, page, tilelayer):
175
+ page.goto(f"{live_server.url}/en/map/new")
176
+
177
+ # Click on the Draw a polygon button on a new map.
178
+ page.get_by_title("Draw a polygon").click()
179
+
180
+ polygon = page.locator("path[fill='DarkBlue']")
181
+ expect(polygon).to_have_count(0)
182
+
183
+ # Click on the map, it will create a polygon.
184
+ map = page.locator("#map")
185
+ map.click(position={"x": 200, "y": 200})
186
+ map.click(position={"x": 100, "y": 200})
187
+ map.click(position={"x": 100, "y": 100})
188
+ map.click(position={"x": 100, "y": 100})
189
+
190
+ # It is created on peerA, and should be on peerB
191
+ expect(polygon).to_have_count(1)
192
+ old_bbox = polygon.bounding_box()
193
+
194
+ edited_vertex = page.locator(".leaflet-middle-icon:nth-child(3)").first
195
+ edited_vertex.drag_to(map, target_position={"x": 250, "y": 250})
196
+ page.keyboard.press("Escape")
197
+
198
+ assert polygon.bounding_box() != old_bbox
199
+
200
+ page.locator(".edit-undo").click()
201
+ assert polygon.bounding_box() == old_bbox
202
+
203
+ page.locator(".edit-redo").click()
204
+ assert polygon.bounding_box() != old_bbox
205
+
206
+
207
+ def test_can_undo_redo_marker_create(live_server, page, tilelayer):
208
+ page.goto(f"{live_server.url}/en/map/new")
209
+
210
+ page.get_by_title("Open Browser").click()
211
+ marker = page.locator(".leaflet-marker-icon")
212
+ map = page.locator("#map")
213
+
214
+ # Create a marker
215
+ page.get_by_title("Draw a marker").click()
216
+ map.click(position={"x": 600, "y": 100})
217
+ expect(marker).to_have_count(1)
218
+ expect(page.locator(".panel .datalayer")).to_have_count(1)
219
+
220
+ page.locator(".edit-undo").click()
221
+ expect(marker).to_have_count(0)
222
+ # Layer still exists
223
+ expect(page.locator(".panel .datalayer")).to_have_count(1)
224
+
225
+ page.locator(".edit-undo").click()
226
+ expect(page.locator(".panel .datalayer")).to_have_count(0)
227
+
228
+ page.locator(".edit-redo").click()
229
+ expect(page.locator(".panel .datalayer")).to_have_count(1)
230
+
231
+ page.locator(".edit-redo").click()
232
+ expect(marker).to_have_count(1)
233
+
234
+
235
+ def test_undo_redo_import(live_server, page, tilelayer):
236
+ page.goto(f"{live_server.url}/map/new/")
237
+ page.get_by_title("Open Browser").click()
238
+
239
+ page.get_by_title("Import data").click()
240
+ file_input = page.locator("input[type='file']")
241
+ with page.expect_file_chooser() as fc_info:
242
+ file_input.click()
243
+ file_chooser = fc_info.value
244
+ path = Path(__file__).parent.parent / "fixtures/test_upload_data.json"
245
+ file_chooser.set_files(path)
246
+ page.get_by_role("button", name="Import data", exact=True).click()
247
+ # Close the import panel
248
+ page.keyboard.press("Escape")
249
+
250
+ layers = page.locator(".umap-browser .datalayer")
251
+ expect(layers).to_have_count(1)
252
+
253
+ features_count = page.locator(".umap-browser .datalayer-counter")
254
+ expect(features_count).to_have_text("(5)")
255
+
256
+ page.locator(".edit-undo").click()
257
+ expect(features_count).to_be_hidden()
258
+ expect(layers).to_have_count(1)
259
+
260
+ page.locator(".edit-undo").click()
261
+ expect(layers).to_have_count(0)
262
+
263
+ page.locator(".edit-redo").click()
264
+ expect(layers).to_have_count(1)
265
+
266
+ page.locator(".edit-redo").click()
267
+ expect(features_count).to_have_text("(5)")
@@ -86,7 +86,6 @@ def test_websocket_connection_can_sync_markers(new_page, asgi_live_server, tilel
86
86
  # Delete a marker from peer A and check it's been deleted on peer B
87
87
  a_first_marker.click(button="right")
88
88
  peerA.get_by_role("button", name="Delete this feature").click()
89
- peerA.locator("dialog").get_by_role("button", name="OK").click()
90
89
  expect(a_marker_pane).to_have_count(1)
91
90
  expect(b_marker_pane).to_have_count(1)
92
91
 
@@ -166,7 +165,6 @@ def test_websocket_connection_can_sync_polygons(context, asgi_live_server, tilel
166
165
  # Delete a polygon from peer A and check it's been deleted on peer B
167
166
  a_polygon.click(button="right")
168
167
  peerA.get_by_role("button", name="Delete this feature").click()
169
- peerA.locator("dialog").get_by_role("button", name="OK").click()
170
168
  expect(a_polygons).to_have_count(0)
171
169
  expect(b_polygons).to_have_count(0)
172
170
 
@@ -477,23 +475,22 @@ def test_should_sync_datalayers_delete(new_page, asgi_live_server, tilelayer):
477
475
  peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
478
476
 
479
477
  peerA.get_by_role("button", name="Open browser").click()
480
- expect(peerA.get_by_text("datalayer 1")).to_be_visible()
481
- expect(peerA.get_by_text("datalayer 2")).to_be_visible()
478
+ expect(peerA.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
479
+ expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
482
480
  peerB.get_by_role("button", name="Open browser").click()
483
- expect(peerB.get_by_text("datalayer 1")).to_be_visible()
484
- expect(peerB.get_by_text("datalayer 2")).to_be_visible()
481
+ expect(peerB.locator(".panel").get_by_text("datalayer 1")).to_be_visible()
482
+ expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_visible()
485
483
 
486
484
  # Delete "datalayer 2" in peerA
487
485
  peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click()
488
- peerA.get_by_role("button", name="OK").click()
489
- expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
490
- expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
486
+ expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
487
+ expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
491
488
 
492
489
  # Save delete to the server
493
490
  with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
494
491
  peerA.get_by_role("button", name="Save").click()
495
- expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
496
- expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
492
+ expect(peerA.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
493
+ expect(peerB.locator(".panel").get_by_text("datalayer 2")).to_be_hidden()
497
494
 
498
495
 
499
496
  @pytest.mark.xdist_group(name="websockets")
@@ -659,3 +656,73 @@ def test_should_sync_line_on_escape(new_page, asgi_live_server, tilelayer):
659
656
 
660
657
  expect(peerA.locator("path")).to_have_count(1)
661
658
  expect(peerB.locator("path")).to_have_count(1)
659
+
660
+
661
+ @pytest.mark.xdist_group(name="websockets")
662
+ def test_should_sync_datalayer_clear(
663
+ new_page, asgi_live_server, tilelayer, map, datalayer
664
+ ):
665
+ map.settings["properties"]["syncEnabled"] = True
666
+ map.edit_status = Map.ANONYMOUS
667
+ map.save()
668
+
669
+ # Create two tabs
670
+ peerA = new_page("Page A")
671
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
672
+ peerB = new_page("Page B")
673
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
674
+ expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
675
+ expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
676
+
677
+ # Clear layer in peer A
678
+ peerA.get_by_role("button", name="Manage layers").click()
679
+ peerA.get_by_role("button", name="Edit", exact=True).click()
680
+ peerA.locator("summary").filter(has_text="Advanced actions").click()
681
+ peerA.get_by_role("button", name="Empty").click()
682
+ expect(peerA.locator(".leaflet-marker-icon")).to_have_count(0)
683
+ expect(peerB.locator(".leaflet-marker-icon")).to_have_count(0)
684
+
685
+ # Undo in peer A
686
+ peerA.locator(".edit-undo").click()
687
+ expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
688
+ expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
689
+
690
+
691
+ @pytest.mark.xdist_group(name="websockets")
692
+ def test_should_save_remote_dirty_datalayers(new_page, asgi_live_server, tilelayer):
693
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
694
+ map.settings["properties"]["syncEnabled"] = True
695
+ map.save()
696
+
697
+ assert not DataLayer.objects.count()
698
+
699
+ # Create two tabs
700
+ peerA = new_page("Page A")
701
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
702
+ peerB = new_page("Page B")
703
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
704
+
705
+ # Create a new layer from peerA
706
+ peerA.get_by_role("button", name="Manage layers").click()
707
+ peerA.get_by_role("button", name="Add a layer").click()
708
+
709
+ # Create a new layer from peerB
710
+ peerB.get_by_role("button", name="Manage layers").click()
711
+ peerB.get_by_role("button", name="Add a layer").click()
712
+
713
+ # Save from peerA to the server
714
+ counter = 0
715
+
716
+ def on_response(response):
717
+ nonlocal counter
718
+ if "/datalayer/create/" in response.url:
719
+ counter += 1
720
+ # Wait for the two datalayer saves
721
+ if counter == 2:
722
+ return True
723
+ return False
724
+
725
+ with peerA.expect_response(on_response):
726
+ peerA.get_by_role("button", name="Save").click()
727
+
728
+ assert DataLayer.objects.count() == 2
umap/tests/settings.py CHANGED
@@ -26,8 +26,6 @@ PASSWORD_HASHERS = [
26
26
  ]
27
27
 
28
28
 
29
- WEBSOCKET_ENABLED = True
30
- WEBSOCKET_BACK_PORT = "8010"
31
- WEBSOCKET_FRONT_URI = "ws://localhost:8010"
29
+ REALTIME_ENABLED = True
32
30
 
33
31
  REDIS_URL = "redis://localhost:6379/15"
@@ -103,6 +103,7 @@ def test_get_version(map, datalayer):
103
103
  ],
104
104
  "type": "Point",
105
105
  },
106
+ "id": "ExNTQ",
106
107
  "properties": {
107
108
  "_umap_options": {
108
109
  "color": "DarkCyan",
@@ -694,6 +694,7 @@ def test_download(client, map, datalayer):
694
694
  "coordinates": [14.68896484375, 48.55297816440071],
695
695
  "type": "Point",
696
696
  },
697
+ "id": "ExNTQ",
697
698
  "properties": {
698
699
  "_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
699
700
  "description": "Da place anonymous again 755",
umap/tests/test_views.py CHANGED
@@ -183,7 +183,12 @@ def test_stats_empty(client):
183
183
  "maps_count": 0,
184
184
  "users_active_last_week_count": 0,
185
185
  "users_count": 0,
186
+ "active_sessions": 0,
186
187
  "version": VERSION,
188
+ "editors_count": 0,
189
+ "members_count": 0,
190
+ "orphans_count": 0,
191
+ "owners_count": 0,
187
192
  }
188
193
 
189
194
 
@@ -199,7 +204,12 @@ def test_stats_basic(client, map, datalayer, user2):
199
204
  "maps_count": 1,
200
205
  "users_active_last_week_count": 1,
201
206
  "users_count": 2,
207
+ "active_sessions": 0,
202
208
  "version": VERSION,
209
+ "editors_count": 0,
210
+ "members_count": 0,
211
+ "orphans_count": 1,
212
+ "owners_count": 1,
203
213
  }
204
214
 
205
215
 
@@ -476,3 +486,27 @@ def test_cannot_search_deleted_map(client, map):
476
486
  url = reverse("search")
477
487
  response = client.get(url + "?q=Blé")
478
488
  assert "Blé dur" not in response.content.decode()
489
+
490
+
491
+ @pytest.mark.django_db
492
+ def test_filter_by_tag(client, map):
493
+ # Very basic search, that do not deal with accent nor case.
494
+ # See install.md for how to have a smarter dict + index.
495
+ map.name = "Blé dur"
496
+ map.tags = ["bike"]
497
+ map.save()
498
+ url = reverse("search")
499
+ response = client.get(url + "?tags=bike")
500
+ assert "Blé dur" in response.content.decode()
501
+
502
+
503
+ @pytest.mark.django_db
504
+ def test_can_combine_search_and_filter(client, map):
505
+ # Very basic search, that do not deal with accent nor case.
506
+ # See install.md for how to have a smarter dict + index.
507
+ map.name = "Blé dur"
508
+ map.tags = ["bike"]
509
+ map.save()
510
+ url = reverse("search")
511
+ response = client.get(url + "?q=dur&tags=bike")
512
+ assert "Blé dur" in response.content.decode()
umap/utils.py CHANGED
@@ -28,7 +28,7 @@ def _urls_for_js():
28
28
  """
29
29
  urls = {}
30
30
  modules = ["umap.urls"]
31
- if settings.WEBSOCKET_ENABLED:
31
+ if settings.REALTIME_ENABLED:
32
32
  modules.append("umap.sync.app")
33
33
  for module in modules:
34
34
  names = _get_url_names(module)
umap/views.py CHANGED
@@ -19,6 +19,7 @@ from django.contrib.auth import BACKEND_SESSION_KEY, get_user_model
19
19
  from django.contrib.auth import logout as do_logout
20
20
  from django.contrib.gis.measure import D
21
21
  from django.contrib.postgres.search import SearchQuery, SearchVector
22
+ from django.contrib.sessions.models import Session
22
23
  from django.contrib.staticfiles.storage import staticfiles_storage
23
24
  from django.core.exceptions import PermissionDenied
24
25
  from django.core.mail import send_mail
@@ -333,12 +334,18 @@ class TeamMaps(PaginatorMixin, DetailView):
333
334
  class SearchMixin:
334
335
  def get_search_queryset(self, **kwargs):
335
336
  q = self.request.GET.get("q")
337
+ tags = [t for t in self.request.GET.getlist("tags") if t]
338
+ qs = Map.objects.all()
336
339
  if q:
337
340
  vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
338
341
  query = SearchQuery(
339
342
  q, config=settings.UMAP_SEARCH_CONFIGURATION, search_type="websearch"
340
343
  )
341
- return Map.objects.annotate(search=vector).filter(search=query)
344
+ qs = qs.annotate(search=vector).filter(search=query)
345
+ if tags:
346
+ qs = qs.filter(tags__contains=tags)
347
+ if q or tags:
348
+ return qs
342
349
 
343
350
 
344
351
  class Search(PaginatorMixin, TemplateView, PublicMapsMixin, SearchMixin):
@@ -609,7 +616,7 @@ class MapDetailMixin(SessionMixin):
609
616
  "licences": dict((l.name, l.json) for l in Licence.objects.all()),
610
617
  "umap_version": VERSION,
611
618
  "featuresHaveOwner": settings.UMAP_DEFAULT_FEATURES_HAVE_OWNERS,
612
- "websocketEnabled": settings.WEBSOCKET_ENABLED,
619
+ "websocketEnabled": settings.REALTIME_ENABLED,
613
620
  "importers": settings.UMAP_IMPORTERS,
614
621
  "defaultLabelKeys": settings.UMAP_LABEL_KEYS,
615
622
  }
@@ -1371,6 +1378,13 @@ class PictogramJSONList(ListView):
1371
1378
 
1372
1379
  def stats(request):
1373
1380
  last_week = make_aware(datetime.now()) - timedelta(days=7)
1381
+ users = User.objects.values_list("pk", flat=True)
1382
+ owners = set(
1383
+ Map.objects.filter(owner__isnull=False).values_list("owner", flat=True)
1384
+ )
1385
+ editors = set(Map.editors.through.objects.values_list("user_id", flat=True))
1386
+ members = set(Team.users.through.objects.values_list("user_id", flat=True))
1387
+ orphans = set(users) - owners - editors - members
1374
1388
  return simple_json_response(
1375
1389
  **{
1376
1390
  "version": VERSION,
@@ -1382,6 +1396,13 @@ def stats(request):
1382
1396
  "users_active_last_week_count": User.objects.filter(
1383
1397
  last_login__gt=last_week
1384
1398
  ).count(),
1399
+ "active_sessions": Session.objects.filter(
1400
+ expire_date__gt=datetime.utcnow()
1401
+ ).count(),
1402
+ "owners_count": len(owners),
1403
+ "editors_count": len(editors),
1404
+ "members_count": len(members),
1405
+ "orphans_count": len(orphans),
1385
1406
  }
1386
1407
  )
1387
1408
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: umap-project
3
- Version: 2.9.3
3
+ Version: 3.0.0
4
4
  Summary: Create maps with OpenStreetMap layers in a minute and embed them in your site.
5
5
  Author-email: Yohan Boniface <yb@enix.org>
6
6
  Maintainer-email: David Larlet <david@larlet.fr>
@@ -19,41 +19,42 @@ Requires-Python: >=3.10
19
19
  Requires-Dist: django-agnocomplete==2.2.0
20
20
  Requires-Dist: django-environ==0.12.0
21
21
  Requires-Dist: django-probes==1.7.0
22
- Requires-Dist: django==5.1.7
22
+ Requires-Dist: django==5.1.8
23
23
  Requires-Dist: pillow==11.1.0
24
- Requires-Dist: psycopg==3.2.4
24
+ Requires-Dist: psycopg==3.2.6
25
25
  Requires-Dist: rcssmin==1.2.1
26
26
  Requires-Dist: requests==2.32.3
27
27
  Requires-Dist: rjsmin==1.2.4
28
- Requires-Dist: social-auth-app-django==5.4.2
29
- Requires-Dist: social-auth-core==4.5.4
28
+ Requires-Dist: social-auth-app-django==5.4.3
29
+ Requires-Dist: social-auth-core==4.5.6
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: djlint==1.36.4; extra == 'dev'
32
32
  Requires-Dist: hatch==1.14.0; extra == 'dev'
33
33
  Requires-Dist: isort==6.0.1; extra == 'dev'
34
- Requires-Dist: mkdocs-material==9.6.4; extra == 'dev'
34
+ Requires-Dist: mkdocs-material==9.6.10; extra == 'dev'
35
35
  Requires-Dist: mkdocs-static-i18n==1.3.0; extra == 'dev'
36
36
  Requires-Dist: mkdocs==1.6.1; extra == 'dev'
37
37
  Requires-Dist: pymdown-extensions==10.14.3; extra == 'dev'
38
- Requires-Dist: ruff==0.9.9; extra == 'dev'
38
+ Requires-Dist: ruff==0.11.2; extra == 'dev'
39
39
  Requires-Dist: vermin==1.6.0; extra == 'dev'
40
40
  Provides-Extra: docker
41
- Requires-Dist: uwsgi==2.0.28; extra == 'docker'
41
+ Requires-Dist: uvicorn==0.34.0; extra == 'docker'
42
42
  Provides-Extra: s3
43
- Requires-Dist: django-storages[s3]==1.14.4; extra == 's3'
43
+ Requires-Dist: django-storages[s3]==1.14.5; extra == 's3'
44
44
  Provides-Extra: sync
45
- Requires-Dist: pydantic==2.10.6; extra == 'sync'
45
+ Requires-Dist: pydantic==2.11.1; extra == 'sync'
46
46
  Requires-Dist: redis==5.2.1; extra == 'sync'
47
+ Requires-Dist: websockets==15.0.1; extra == 'sync'
47
48
  Provides-Extra: test
48
49
  Requires-Dist: daphne==4.1.2; extra == 'test'
49
50
  Requires-Dist: factory-boy==3.3.3; extra == 'test'
50
- Requires-Dist: moto[s3]==5.0.28; extra == 'test'
51
+ Requires-Dist: moto[s3]==5.1.1; extra == 'test'
51
52
  Requires-Dist: playwright>=1.39; extra == 'test'
52
53
  Requires-Dist: pytest-django==4.10.0; extra == 'test'
53
54
  Requires-Dist: pytest-playwright==0.7.0; extra == 'test'
54
55
  Requires-Dist: pytest-rerunfailures==15.0; extra == 'test'
55
56
  Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'test'
56
- Requires-Dist: pytest==8.3.4; extra == 'test'
57
+ Requires-Dist: pytest==8.3.5; extra == 'test'
57
58
  Description-Content-Type: text/markdown
58
59
 
59
60
  # uMap project