umap-project 2.6.2__py3-none-any.whl → 2.7.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 (211) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +64 -1
  3. umap/asgi.py +15 -0
  4. umap/context_processors.py +1 -0
  5. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/cs_CZ/LC_MESSAGES/django.po +96 -92
  7. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/de/LC_MESSAGES/django.po +19 -18
  9. umap/locale/en/LC_MESSAGES/django.po +47 -43
  10. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  11. umap/locale/es/LC_MESSAGES/django.po +134 -128
  12. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/fr/LC_MESSAGES/django.po +51 -47
  14. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/pt/LC_MESSAGES/django.po +64 -60
  16. umap/management/commands/clean_tilelayer.py +152 -0
  17. umap/management/commands/purge_purgatory.py +28 -0
  18. umap/models.py +27 -2
  19. umap/settings/base.py +3 -1
  20. umap/static/umap/base.css +4 -4
  21. umap/static/umap/css/contextmenu.css +6 -1
  22. umap/static/umap/css/icon.css +7 -2
  23. umap/static/umap/css/importers.css +4 -0
  24. umap/static/umap/img/16-white.svg +9 -2
  25. umap/static/umap/img/16.svg +1 -181
  26. umap/static/umap/img/24-white.svg +1 -0
  27. umap/static/umap/img/24.svg +1 -0
  28. umap/static/umap/img/importers/cadastrefr.svg +23 -0
  29. umap/static/umap/img/source/16-white.svg +10 -3
  30. umap/static/umap/img/source/16.svg +753 -197
  31. umap/static/umap/img/source/24-white.svg +3 -2
  32. umap/static/umap/img/source/24.svg +3 -2
  33. umap/static/umap/js/modules/autocomplete.js +7 -3
  34. umap/static/umap/js/modules/browser.js +55 -2
  35. umap/static/umap/js/modules/caption.js +16 -5
  36. umap/static/umap/js/modules/data/features.js +183 -8
  37. umap/static/umap/js/modules/data/layer.js +57 -40
  38. umap/static/umap/js/modules/formatter.js +3 -2
  39. umap/static/umap/js/modules/global.js +2 -0
  40. umap/static/umap/js/modules/importer.js +3 -0
  41. umap/static/umap/js/modules/importers/cadastrefr.js +62 -0
  42. umap/static/umap/js/modules/importers/communesfr.js +15 -3
  43. umap/static/umap/js/modules/permissions.js +123 -93
  44. umap/static/umap/js/modules/rendering/layers/classified.js +2 -0
  45. umap/static/umap/js/modules/rendering/ui.js +60 -213
  46. umap/static/umap/js/modules/share.js +1 -3
  47. umap/static/umap/js/modules/slideshow.js +1 -1
  48. umap/static/umap/js/modules/sync/engine.js +371 -14
  49. umap/static/umap/js/modules/sync/hlc.js +106 -0
  50. umap/static/umap/js/modules/sync/updaters.js +18 -6
  51. umap/static/umap/js/modules/sync/websocket.js +1 -1
  52. umap/static/umap/js/modules/tableeditor.js +1 -1
  53. umap/static/umap/js/modules/ui/base.js +2 -2
  54. umap/static/umap/js/modules/ui/contextmenu.js +51 -18
  55. umap/static/umap/js/modules/urls.js +5 -1
  56. umap/static/umap/js/modules/utils.js +28 -4
  57. umap/static/umap/js/umap.controls.js +76 -55
  58. umap/static/umap/js/umap.core.js +3 -3
  59. umap/static/umap/js/umap.forms.js +3 -1
  60. umap/static/umap/js/umap.js +115 -124
  61. umap/static/umap/locale/am_ET.js +2 -2
  62. umap/static/umap/locale/am_ET.json +2 -2
  63. umap/static/umap/locale/ar.js +2 -2
  64. umap/static/umap/locale/ar.json +2 -2
  65. umap/static/umap/locale/ast.js +2 -2
  66. umap/static/umap/locale/ast.json +2 -2
  67. umap/static/umap/locale/bg.js +2 -2
  68. umap/static/umap/locale/bg.json +2 -2
  69. umap/static/umap/locale/br.js +13 -4
  70. umap/static/umap/locale/br.json +13 -4
  71. umap/static/umap/locale/ca.js +30 -17
  72. umap/static/umap/locale/ca.json +30 -17
  73. umap/static/umap/locale/cs_CZ.js +89 -80
  74. umap/static/umap/locale/cs_CZ.json +89 -80
  75. umap/static/umap/locale/da.js +2 -2
  76. umap/static/umap/locale/da.json +2 -2
  77. umap/static/umap/locale/de.js +17 -8
  78. umap/static/umap/locale/de.json +17 -8
  79. umap/static/umap/locale/el.js +2 -2
  80. umap/static/umap/locale/el.json +2 -2
  81. umap/static/umap/locale/en.js +15 -4
  82. umap/static/umap/locale/en.json +15 -4
  83. umap/static/umap/locale/en_US.json +2 -2
  84. umap/static/umap/locale/es.js +338 -325
  85. umap/static/umap/locale/es.json +338 -325
  86. umap/static/umap/locale/et.js +2 -2
  87. umap/static/umap/locale/et.json +2 -2
  88. umap/static/umap/locale/eu.js +11 -4
  89. umap/static/umap/locale/eu.json +11 -4
  90. umap/static/umap/locale/fa_IR.js +11 -4
  91. umap/static/umap/locale/fa_IR.json +11 -4
  92. umap/static/umap/locale/fi.js +2 -2
  93. umap/static/umap/locale/fi.json +2 -2
  94. umap/static/umap/locale/fr.js +15 -4
  95. umap/static/umap/locale/fr.json +15 -4
  96. umap/static/umap/locale/gl.js +2 -2
  97. umap/static/umap/locale/gl.json +2 -2
  98. umap/static/umap/locale/he.js +2 -2
  99. umap/static/umap/locale/he.json +2 -2
  100. umap/static/umap/locale/hr.js +2 -2
  101. umap/static/umap/locale/hr.json +2 -2
  102. umap/static/umap/locale/hu.js +12 -5
  103. umap/static/umap/locale/hu.json +12 -5
  104. umap/static/umap/locale/id.js +2 -2
  105. umap/static/umap/locale/id.json +2 -2
  106. umap/static/umap/locale/is.js +2 -2
  107. umap/static/umap/locale/is.json +2 -2
  108. umap/static/umap/locale/it.js +2 -2
  109. umap/static/umap/locale/it.json +2 -2
  110. umap/static/umap/locale/ja.js +2 -2
  111. umap/static/umap/locale/ja.json +2 -2
  112. umap/static/umap/locale/ko.js +2 -2
  113. umap/static/umap/locale/ko.json +2 -2
  114. umap/static/umap/locale/lt.js +2 -2
  115. umap/static/umap/locale/lt.json +2 -2
  116. umap/static/umap/locale/ms.js +2 -2
  117. umap/static/umap/locale/ms.json +2 -2
  118. umap/static/umap/locale/nl.js +2 -2
  119. umap/static/umap/locale/nl.json +2 -2
  120. umap/static/umap/locale/no.js +2 -2
  121. umap/static/umap/locale/no.json +2 -2
  122. umap/static/umap/locale/pl.js +2 -2
  123. umap/static/umap/locale/pl.json +2 -2
  124. umap/static/umap/locale/pl_PL.json +2 -2
  125. umap/static/umap/locale/pt.js +19 -10
  126. umap/static/umap/locale/pt.json +19 -10
  127. umap/static/umap/locale/pt_BR.js +2 -2
  128. umap/static/umap/locale/pt_BR.json +2 -2
  129. umap/static/umap/locale/pt_PT.js +13 -4
  130. umap/static/umap/locale/pt_PT.json +13 -4
  131. umap/static/umap/locale/ro.js +2 -2
  132. umap/static/umap/locale/ro.json +2 -2
  133. umap/static/umap/locale/ru.js +2 -2
  134. umap/static/umap/locale/ru.json +2 -2
  135. umap/static/umap/locale/si.js +2 -2
  136. umap/static/umap/locale/si.json +2 -2
  137. umap/static/umap/locale/sk_SK.js +2 -2
  138. umap/static/umap/locale/sk_SK.json +2 -2
  139. umap/static/umap/locale/sl.js +2 -2
  140. umap/static/umap/locale/sl.json +2 -2
  141. umap/static/umap/locale/sr.js +2 -2
  142. umap/static/umap/locale/sr.json +2 -2
  143. umap/static/umap/locale/sv.js +2 -2
  144. umap/static/umap/locale/sv.json +2 -2
  145. umap/static/umap/locale/th_TH.js +2 -2
  146. umap/static/umap/locale/th_TH.json +2 -2
  147. umap/static/umap/locale/tr.js +2 -2
  148. umap/static/umap/locale/tr.json +2 -2
  149. umap/static/umap/locale/uk_UA.js +2 -2
  150. umap/static/umap/locale/uk_UA.json +2 -2
  151. umap/static/umap/locale/vi.js +2 -2
  152. umap/static/umap/locale/vi.json +2 -2
  153. umap/static/umap/locale/vi_VN.json +2 -2
  154. umap/static/umap/locale/zh.js +2 -2
  155. umap/static/umap/locale/zh.json +2 -2
  156. umap/static/umap/locale/zh_CN.json +2 -2
  157. umap/static/umap/locale/zh_TW.Big5.json +2 -2
  158. umap/static/umap/locale/zh_TW.js +13 -4
  159. umap/static/umap/locale/zh_TW.json +13 -4
  160. umap/static/umap/map.css +44 -29
  161. umap/static/umap/unittests/hlc.js +165 -0
  162. umap/static/umap/unittests/sync.js +321 -15
  163. umap/static/umap/unittests/utils.js +47 -0
  164. umap/static/umap/vars.css +2 -1
  165. umap/static/umap/vendors/colorbrewer/colorbrewer.js +309 -317
  166. umap/static/umap/vendors/dompurify/purify.es.js +15 -16
  167. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  168. umap/static/umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js +111 -80
  169. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js +2 -2
  170. umap/static/umap/vendors/locatecontrol/L.Control.Locate.min.js.map +1 -1
  171. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js +1 -1
  172. umap/static/umap/vendors/simple-statistics/simple-statistics.min.js.map +1 -1
  173. umap/templates/umap/css.html +0 -2
  174. umap/templates/umap/dashboard_menu.html +4 -2
  175. umap/templates/umap/js.html +0 -5
  176. umap/templates/umap/map_detail.html +2 -2
  177. umap/tests/fixtures/test_upload_data.csv +2 -2
  178. umap/tests/integration/test_anonymous_owned_map.py +1 -0
  179. umap/tests/integration/test_basics.py +1 -1
  180. umap/tests/integration/test_browser.py +69 -7
  181. umap/tests/integration/test_caption.py +3 -3
  182. umap/tests/integration/test_circles_layer.py +12 -0
  183. umap/tests/integration/test_cluster.py +53 -0
  184. umap/tests/integration/test_datalayer.py +2 -1
  185. umap/tests/integration/test_draw_polygon.py +17 -9
  186. umap/tests/integration/test_draw_polyline.py +84 -7
  187. umap/tests/integration/test_edit_datalayer.py +5 -8
  188. umap/tests/integration/test_edit_map.py +2 -2
  189. umap/tests/integration/test_edit_marker.py +1 -1
  190. umap/tests/integration/test_facets_browser.py +3 -3
  191. umap/tests/integration/test_import.py +1 -0
  192. umap/tests/integration/test_map.py +1 -0
  193. umap/tests/integration/test_owned_map.py +1 -1
  194. umap/tests/integration/test_view_marker.py +63 -0
  195. umap/tests/integration/test_view_polygon.py +12 -12
  196. umap/tests/integration/test_websocket_sync.py +65 -3
  197. umap/tests/test_clean_tilelayer.py +83 -0
  198. umap/tests/test_datalayer.py +24 -0
  199. umap/tests/test_map_views.py +20 -0
  200. umap/tests/test_purge_purgatory.py +25 -0
  201. umap/tests/test_websocket_server.py +22 -0
  202. umap/urls.py +5 -1
  203. umap/views.py +6 -3
  204. umap/websocket_server.py +130 -27
  205. {umap_project-2.6.2.dist-info → umap_project-2.7.0.dist-info}/METADATA +18 -14
  206. {umap_project-2.6.2.dist-info → umap_project-2.7.0.dist-info}/RECORD +209 -200
  207. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +0 -1
  208. umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +0 -7
  209. {umap_project-2.6.2.dist-info → umap_project-2.7.0.dist-info}/WHEEL +0 -0
  210. {umap_project-2.6.2.dist-info → umap_project-2.7.0.dist-info}/entry_points.txt +0 -0
  211. {umap_project-2.6.2.dist-info → umap_project-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -234,8 +234,8 @@ def test_can_delete_datalayer(live_server, map, login, datalayer):
234
234
  expect(layers).to_have_count(1)
235
235
  expect(markers).to_have_count(1)
236
236
  page.get_by_role("link", name="Manage layers").click()
237
- page.once("dialog", lambda dialog: dialog.accept())
238
237
  page.locator(".panel.right").get_by_title("Delete layer").click()
238
+ page.get_by_role("button", name="OK").click()
239
239
  with page.expect_response(re.compile(r".*/datalayer/delete/.*")):
240
240
  page.get_by_role("button", name="Save").click()
241
241
  expect(markers).to_have_count(0)
@@ -106,3 +106,66 @@ def test_extended_properties_in_popup(live_server, map, page, bootstrap):
106
106
  expect(page.get_by_text("Alt: 241")).to_be_visible()
107
107
  expect(page.get_by_text("Zoom: 7")).to_be_visible()
108
108
  expect(page.get_by_text("Layer: test datalayer")).to_be_visible()
109
+
110
+
111
+ def test_only_visible_markers_are_added_to_dom(live_server, map, page):
112
+ data = {
113
+ "type": "FeatureCollection",
114
+ "features": [
115
+ {
116
+ "type": "Feature",
117
+ "properties": {
118
+ "name": "marker 1",
119
+ "description": "added to dom",
120
+ },
121
+ "geometry": {
122
+ "type": "Point",
123
+ "coordinates": [14.6, 48.5],
124
+ },
125
+ },
126
+ {
127
+ "type": "Feature",
128
+ "properties": {
129
+ "name": "marker 2",
130
+ "description": "not added to dom at load",
131
+ },
132
+ "geometry": {
133
+ "type": "Point",
134
+ "coordinates": [12.6, 44.5],
135
+ },
136
+ },
137
+ ],
138
+ }
139
+ DataLayerFactory(map=map, data=data)
140
+ map.settings["properties"]["showLabel"] = True
141
+ map.save()
142
+ page.goto(f"{live_server.url}{map.get_absolute_url()}")
143
+ markers = page.locator(".leaflet-marker-icon")
144
+ tooltips = page.locator(".leaflet-tooltip")
145
+ expect(markers).to_have_count(1)
146
+ expect(tooltips).to_have_count(1)
147
+
148
+ # Zoom in/out to show the other marker
149
+ page.get_by_label("Zoom out").click()
150
+ expect(markers).to_have_count(2)
151
+ expect(tooltips).to_have_count(2)
152
+ page.get_by_label("Zoom in").click()
153
+ expect(markers).to_have_count(1)
154
+ expect(tooltips).to_have_count(1)
155
+
156
+ # Drag map to show/hide the marker
157
+ map_el = page.locator("#map")
158
+ map_el.drag_to(
159
+ map_el,
160
+ source_position={"x": 100, "y": 600},
161
+ target_position={"x": 100, "y": 200},
162
+ )
163
+ expect(markers).to_have_count(2)
164
+ expect(tooltips).to_have_count(2)
165
+ map_el.drag_to(
166
+ map_el,
167
+ source_position={"x": 100, "y": 600},
168
+ target_position={"x": 100, "y": 200},
169
+ )
170
+ expect(markers).to_have_count(1)
171
+ expect(tooltips).to_have_count(1)
@@ -1,4 +1,5 @@
1
1
  import re
2
+ from copy import deepcopy
2
3
 
3
4
  import pytest
4
5
  from playwright.sync_api import expect
@@ -33,19 +34,9 @@ DATALAYER_DATA = {
33
34
  }
34
35
 
35
36
 
36
- @pytest.fixture
37
- def bootstrap(map, live_server):
38
- map.settings["properties"]["zoom"] = 6
39
- map.settings["geometry"] = {
40
- "type": "Point",
41
- "coordinates": [8.429, 53.239],
42
- }
43
- map.save()
37
+ def test_should_open_popup_on_click(live_server, map, page):
44
38
  DataLayerFactory(map=map, data=DATALAYER_DATA)
45
-
46
-
47
- def test_should_open_popup_on_click(live_server, map, page, bootstrap):
48
- page.goto(f"{live_server.url}{map.get_absolute_url()}")
39
+ page.goto(f"{live_server.url}{map.get_absolute_url()}#6/53.239/8.429")
49
40
  polygon = page.locator("path").first
50
41
  expect(polygon).to_have_attribute("fill-opacity", "0.3")
51
42
  polygon.click()
@@ -57,3 +48,12 @@ def test_should_open_popup_on_click(live_server, map, page, bootstrap):
57
48
  # Close popup
58
49
  page.locator("#map").click()
59
50
  expect(polygon).to_have_attribute("fill-opacity", "0.3")
51
+
52
+
53
+ def test_should_not_react_to_click_if_interactive_false(live_server, map, page):
54
+ data = deepcopy(DATALAYER_DATA)
55
+ data["features"][0]["properties"]["_umap_options"] = {"interactive": False}
56
+ DataLayerFactory(map=map, data=data)
57
+ page.goto(f"{live_server.url}{map.get_absolute_url()}#6/53.239/8.429")
58
+ polygon = page.locator("path").first
59
+ expect(polygon).to_have_css("pointer-events", "none")
@@ -69,7 +69,7 @@ def test_websocket_connection_can_sync_markers(
69
69
 
70
70
  # Delete a marker from peer A and check it's been deleted on peer B
71
71
  a_first_marker.click(button="right")
72
- peerA.get_by_role("link", name="Delete this feature").click()
72
+ peerA.get_by_role("button", name="Delete this feature").click()
73
73
  peerA.locator("dialog").get_by_role("button", name="OK").click()
74
74
  expect(a_marker_pane).to_have_count(1)
75
75
  expect(b_marker_pane).to_have_count(1)
@@ -153,7 +153,7 @@ def test_websocket_connection_can_sync_polygons(
153
153
 
154
154
  # Delete a polygon from peer A and check it's been deleted on peer B
155
155
  a_polygon.click(button="right")
156
- peerA.get_by_role("link", name="Delete this feature").click()
156
+ peerA.get_by_role("button", name="Delete this feature").click()
157
157
  peerA.locator("dialog").get_by_role("button", name="OK").click()
158
158
  expect(a_polygons).to_have_count(0)
159
159
  expect(b_polygons).to_have_count(0)
@@ -268,7 +268,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
268
268
 
269
269
  # Clone on peer B and save
270
270
  b_polygon.click(button="right")
271
- peerB.get_by_role("link", name="Clone this feature").click()
271
+ peerB.get_by_role("button", name="Clone this feature").click()
272
272
 
273
273
  expect(peerB.locator("path")).to_have_count(2)
274
274
 
@@ -281,3 +281,65 @@ def test_websocket_connection_can_sync_cloned_polygons(
281
281
  peerB.get_by_role("button", name="Save").click()
282
282
 
283
283
  expect(peerB.locator("path")).to_have_count(2)
284
+
285
+
286
+ @pytest.mark.xdist_group(name="websockets")
287
+ def test_websocket_connection_can_sync_late_joining_peer(
288
+ new_page, live_server, websocket_server, tilelayer
289
+ ):
290
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
291
+ map.settings["properties"]["syncEnabled"] = True
292
+ map.save()
293
+ DataLayerFactory(map=map, data={})
294
+
295
+ # Create first peer (A) and have it join immediately
296
+ peerA = new_page("Page A")
297
+ peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
298
+
299
+ # Add a marker from peer A
300
+ a_create_marker = peerA.get_by_title("Draw a marker")
301
+ expect(a_create_marker).to_be_visible()
302
+ a_create_marker.click()
303
+
304
+ a_map_el = peerA.locator("#map")
305
+ a_map_el.click(position={"x": 220, "y": 220})
306
+ peerA.locator("body").type("First marker")
307
+ peerA.locator("body").press("Escape")
308
+
309
+ # Add a polygon from peer A
310
+ create_polygon = peerA.locator(".leaflet-control-toolbar ").get_by_title(
311
+ "Draw a polygon"
312
+ )
313
+ create_polygon.click()
314
+
315
+ a_map_el.click(position={"x": 200, "y": 200})
316
+ a_map_el.click(position={"x": 100, "y": 200})
317
+ a_map_el.click(position={"x": 100, "y": 100})
318
+ a_map_el.click(position={"x": 200, "y": 100})
319
+ a_map_el.click(position={"x": 200, "y": 100})
320
+ peerA.keyboard.press("Escape")
321
+
322
+ # Now create peer B and have it join
323
+ peerB = new_page("Page B")
324
+ peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
325
+
326
+ # Check if peer B has received all the updates
327
+ b_marker_pane = peerB.locator(".leaflet-marker-pane > div")
328
+ b_polygons = peerB.locator(".leaflet-overlay-pane path[fill='DarkBlue']")
329
+
330
+ expect(b_marker_pane).to_have_count(1)
331
+ expect(b_polygons).to_have_count(1)
332
+
333
+ # Verify marker properties
334
+ peerB.locator(".leaflet-marker-icon").first.click()
335
+ peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
336
+ expect(peerB.locator('input[name="name"]')).to_have_value("First marker")
337
+
338
+ # Verify polygon exists (we've already checked the count)
339
+ b_polygon = peerB.locator("path")
340
+ expect(b_polygon).to_be_visible()
341
+
342
+ # Optional: Verify polygon properties if you have any specific ones set
343
+
344
+ # Clean up: close edit mode
345
+ peerB.locator("body").press("Escape")
@@ -0,0 +1,83 @@
1
+ import pytest
2
+ from django.core.management import call_command
3
+
4
+ from umap.models import Map
5
+
6
+ pytestmark = pytest.mark.django_db
7
+
8
+
9
+ def test_can_delete_tilelayer_from_settings(map):
10
+ map.settings["properties"]["tilelayer"] = {
11
+ "name": "My TileLayer",
12
+ "maxZoom": 18,
13
+ "minZoom": 0,
14
+ "attribution": "My attribution",
15
+ "url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
16
+ }
17
+ map.save()
18
+ # Make sure its saved
19
+ map = Map.objects.get(pk=map.pk)
20
+ assert "tilelayer" in map.settings["properties"]
21
+ call_command(
22
+ "clean_tilelayer", "http://{s}.foo.bar.baz/{z}/{x}/{y}.png", "--no-input"
23
+ )
24
+ map = Map.objects.get(pk=map.pk)
25
+ assert "tilelayer" not in map.settings["properties"]
26
+
27
+
28
+ def test_can_replace_tilelayer_url_in_map_settings(map):
29
+ map.settings["properties"]["tilelayer"] = {
30
+ "name": "My TileLayer",
31
+ "maxZoom": 18,
32
+ "minZoom": 0,
33
+ "attribution": "My attribution",
34
+ "url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
35
+ }
36
+ map.save()
37
+ new = "https://{s}.foo.bar.baz/{z}/{x}/{y}.png"
38
+ call_command(
39
+ "clean_tilelayer",
40
+ "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
41
+ new,
42
+ "--no-input",
43
+ )
44
+ map = Map.objects.get(pk=map.pk)
45
+ assert map.settings["properties"]["tilelayer"]["url_template"] == new
46
+
47
+
48
+ def test_can_replace_tilelayer_by_name_in_map_settings(map, tilelayer):
49
+ map.settings["properties"]["tilelayer"] = {
50
+ "name": "My TileLayer",
51
+ "maxZoom": 18,
52
+ "minZoom": 0,
53
+ "attribution": "My attribution",
54
+ "url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
55
+ }
56
+ map.save()
57
+ call_command(
58
+ "clean_tilelayer",
59
+ "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
60
+ tilelayer.name,
61
+ "--no-input",
62
+ )
63
+ map = Map.objects.get(pk=map.pk)
64
+ assert map.settings["properties"]["tilelayer"] == tilelayer.json
65
+
66
+
67
+ def test_can_replace_tilelayer_by_id_in_map_settings(map, tilelayer):
68
+ map.settings["properties"]["tilelayer"] = {
69
+ "name": "My TileLayer",
70
+ "maxZoom": 18,
71
+ "minZoom": 0,
72
+ "attribution": "My attribution",
73
+ "url_template": "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
74
+ }
75
+ map.save()
76
+ call_command(
77
+ "clean_tilelayer",
78
+ "http://{s}.foo.bar.baz/{z}/{x}/{y}.png",
79
+ tilelayer.pk,
80
+ "--no-input",
81
+ )
82
+ map = Map.objects.get(pk=map.pk)
83
+ assert map.settings["properties"]["tilelayer"] == tilelayer.json
@@ -1,3 +1,4 @@
1
+ import tempfile
1
2
  from pathlib import Path
2
3
 
3
4
  import pytest
@@ -269,3 +270,26 @@ def test_anonymous_can_edit_in_inherit_mode_and_map_in_public_mode(
269
270
  map.save()
270
271
  fake_request.user = AnonymousUser()
271
272
  assert datalayer.can_edit(fake_request)
273
+
274
+
275
+ def test_should_remove_all_versions_on_delete(map, settings):
276
+ settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
277
+ datalayer = DataLayerFactory(uuid="0f1161c0-c07f-4ba4-86c5-8d8981d8a813", old_id=17)
278
+ root = Path(datalayer.storage_root())
279
+ before = len(datalayer.geojson.storage.listdir(root)[1])
280
+ other = "123456_1440918637.geojson"
281
+ files = [
282
+ f"{datalayer.pk}_1440924889.geojson",
283
+ f"{datalayer.pk}_1440923687.geojson",
284
+ f"{datalayer.pk}_1440918637.geojson",
285
+ f"{datalayer.old_id}_1440918537.geojson",
286
+ other,
287
+ ]
288
+ for path in files:
289
+ datalayer.geojson.storage.save(root / path, ContentFile("{}"))
290
+ datalayer.geojson.storage.save(root / f"{path}.gz", ContentFile("{}"))
291
+ assert len(datalayer.geojson.storage.listdir(root)[1]) == 10 + before
292
+ datalayer.delete()
293
+ found = datalayer.geojson.storage.listdir(root)[1]
294
+ assert found == [other, f"{other}.gz"]
295
+ assert len(list(Path(settings.UMAP_PURGATORY_ROOT).iterdir())) == 4 + before
@@ -368,6 +368,7 @@ def test_anonymous_create(cookieclient, post_data):
368
368
  assert (
369
369
  created_map.get_anonymous_edit_url() in j["permissions"]["anonymous_edit_url"]
370
370
  )
371
+ assert j["user"]["is_owner"] is True
371
372
  assert created_map.name == name
372
373
  key, value = created_map.signed_cookie_elements
373
374
  assert key in cookieclient.cookies
@@ -860,3 +861,22 @@ def test_ogp_links(client, map, datalayer):
860
861
  assert f'<meta property="og:title" content="{map.name}" />' in content
861
862
  assert f'<meta property="og:description" content="{map.description}" />' in content
862
863
  assert '<meta property="og:site_name" content="uMap" />' in content
864
+
865
+
866
+ def test_non_public_map_should_have_noindex_meta(client, map, datalayer):
867
+ map.share_status = Map.OPEN
868
+ map.save()
869
+ response = client.get(map.get_absolute_url())
870
+ assert response.status_code == 200
871
+ assert (
872
+ '<meta name="robots" content="noindex,nofollow">' in response.content.decode()
873
+ )
874
+
875
+
876
+ def test_demo_instance_should_have_noindex(client, map, datalayer, settings):
877
+ settings.UMAP_DEMO_SITE = True
878
+ response = client.get(map.get_absolute_url())
879
+ assert response.status_code == 200
880
+ assert (
881
+ '<meta name="robots" content="noindex,nofollow">' in response.content.decode()
882
+ )
@@ -0,0 +1,25 @@
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+ from django.core.management import call_command
6
+
7
+
8
+ def test_purge_purgatory(settings):
9
+ settings.UMAP_PURGATORY_ROOT = tempfile.mkdtemp()
10
+ root = Path(settings.UMAP_PURGATORY_ROOT)
11
+ old = root / "old.json"
12
+ old.write_text("{}")
13
+ stat = old.stat()
14
+ os.utime(old, times=(stat.st_mtime - 31 * 86400, stat.st_mtime - 31 * 86400))
15
+ recent = root / "recent.json"
16
+ recent.write_text("{}")
17
+ stat = recent.stat()
18
+ os.utime(recent, times=(stat.st_mtime - 8 * 86400, stat.st_mtime - 8 * 86400))
19
+ now = root / "now.json"
20
+ now.write_text("{}")
21
+ assert {f.name for f in root.iterdir()} == {"old.json", "recent.json", "now.json"}
22
+ call_command("purge_purgatory")
23
+ assert {f.name for f in root.iterdir()} == {"recent.json", "now.json"}
24
+ call_command("purge_purgatory", "--days=7")
25
+ assert {f.name for f in root.iterdir()} == {"now.json"}
@@ -0,0 +1,22 @@
1
+ from umap.websocket_server import OperationMessage, PeerMessage, Request, ServerRequest
2
+
3
+
4
+ def test_messages_are_parsed_correctly():
5
+ server = Request.model_validate(dict(kind="Server", action="list-peers")).root
6
+ assert type(server) is ServerRequest
7
+
8
+ operation = Request.model_validate(
9
+ dict(
10
+ kind="OperationMessage",
11
+ verb="upsert",
12
+ subject="map",
13
+ metadata={},
14
+ key="key",
15
+ )
16
+ ).root
17
+ assert type(operation) is OperationMessage
18
+
19
+ peer_message = Request.model_validate(
20
+ dict(kind="PeerMessage", sender="Alice", recipient="Bob", message={})
21
+ ).root
22
+ assert type(peer_message) is PeerMessage
umap/urls.py CHANGED
@@ -115,11 +115,15 @@ i18n_urls += decorated_patterns(
115
115
  name="map_star",
116
116
  ),
117
117
  path("me", views.user_dashboard, name="user_dashboard"),
118
- path("me/profile", views.user_profile, name="user_profile"),
119
118
  path("me/download", views.user_download, name="user_download"),
120
119
  path("me/teams", views.UserTeams.as_view(), name="user_teams"),
121
120
  path("team/create/", views.TeamNew.as_view(), name="team_new"),
122
121
  )
122
+
123
+ if settings.UMAP_ALLOW_EDIT_PROFILE:
124
+ i18n_urls.append(
125
+ path("me/profile", login_required(views.user_profile), name="user_profile")
126
+ )
123
127
  i18n_urls += decorated_patterns(
124
128
  [login_required, team_members_only],
125
129
  path("team/<int:pk>/edit/", views.TeamUpdate.as_view(), name="team_update"),
umap/views.py CHANGED
@@ -863,15 +863,17 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView):
863
863
  form.instance.owner = self.request.user
864
864
  self.object = form.save()
865
865
  permissions = self.get_permissions()
866
+ user_data = self.get_user_data()
866
867
  # User does not have the cookie yet.
867
868
  if not self.object.owner:
868
869
  anonymous_url = self.object.get_anonymous_edit_url()
869
870
  permissions["anonymous_edit_url"] = anonymous_url
871
+ user_data["is_owner"] = True
870
872
  response = simple_json_response(
871
873
  id=self.object.pk,
872
874
  url=self.object.get_absolute_url(),
873
875
  permissions=permissions,
874
- user=self.get_user_data(),
876
+ user=user_data,
875
877
  )
876
878
  if not self.request.user.is_authenticated:
877
879
  key, value = self.object.signed_cookie_elements
@@ -908,7 +910,7 @@ def get_websocket_auth_token(request, map_id, map_inst):
908
910
  return simple_json_response(token=signed_token)
909
911
 
910
912
 
911
- class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView):
913
+ class MapUpdate(FormLessEditMixin, PermissionsMixin, SessionMixin, UpdateView):
912
914
  model = Map
913
915
  form_class = MapSettingsForm
914
916
  pk_url_kwarg = "map_id"
@@ -920,6 +922,7 @@ class MapUpdate(FormLessEditMixin, PermissionsMixin, UpdateView):
920
922
  id=self.object.pk,
921
923
  url=self.object.get_absolute_url(),
922
924
  permissions=self.get_permissions(),
925
+ user=self.get_user_data(),
923
926
  )
924
927
 
925
928
 
@@ -1359,7 +1362,7 @@ def logout(request):
1359
1362
 
1360
1363
  class LoginPopupEnd(TemplateView):
1361
1364
  """
1362
- End of a loggin process in popup.
1365
+ End of a login process in popup.
1363
1366
  Basically close the popup.
1364
1367
  """
1365
1368