umap-project 2.8.1__py3-none-any.whl → 2.9.0b0__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 (159) hide show
  1. umap/__init__.py +1 -1
  2. umap/asgi.py +12 -7
  3. umap/context_processors.py +1 -0
  4. umap/locale/en/LC_MESSAGES/django.po +102 -59
  5. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/fr/LC_MESSAGES/django.po +105 -61
  7. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/hu/LC_MESSAGES/django.po +10 -10
  9. umap/management/commands/empty_trash.py +12 -1
  10. umap/migrations/0026_datalayer_modified_at_datalayer_share_status.py +26 -0
  11. umap/models.py +23 -3
  12. umap/settings/base.py +4 -1
  13. umap/static/umap/base.css +1 -1
  14. umap/static/umap/content.css +2 -22
  15. umap/static/umap/css/bar.css +7 -10
  16. umap/static/umap/css/form.css +28 -29
  17. umap/static/umap/css/icon.css +8 -2
  18. umap/static/umap/css/panel.css +2 -1
  19. umap/static/umap/css/tooltip.css +33 -31
  20. umap/static/umap/img/16-white.svg +2 -0
  21. umap/static/umap/img/16.svg +1 -1
  22. umap/static/umap/img/providers/bitbucket.png +0 -0
  23. umap/static/umap/img/providers/github.png +0 -0
  24. umap/static/umap/img/providers/keycloak.png +0 -0
  25. umap/static/umap/img/providers/openstreetmap-oauth2.png +0 -0
  26. umap/static/umap/img/providers/twitter-oauth2.png +0 -0
  27. umap/static/umap/img/source/16-white.svg +3 -1
  28. umap/static/umap/img/source/16.svg +1 -1
  29. umap/static/umap/js/components/alerts/alert.js +4 -1
  30. umap/static/umap/js/modules/browser.js +6 -6
  31. umap/static/umap/js/modules/caption.js +30 -7
  32. umap/static/umap/js/modules/data/features.js +21 -24
  33. umap/static/umap/js/modules/data/layer.js +71 -33
  34. umap/static/umap/js/modules/form/builder.js +241 -0
  35. umap/static/umap/js/modules/form/fields.js +1338 -0
  36. umap/static/umap/js/modules/formatter.js +5 -8
  37. umap/static/umap/js/modules/help.js +3 -1
  38. umap/static/umap/js/modules/importer.js +1 -1
  39. umap/static/umap/js/modules/permissions.js +5 -4
  40. umap/static/umap/js/modules/rendering/icon.js +5 -1
  41. umap/static/umap/js/modules/rendering/layers/classified.js +11 -7
  42. umap/static/umap/js/modules/rendering/layers/cluster.js +11 -1
  43. umap/static/umap/js/modules/rendering/map.js +0 -2
  44. umap/static/umap/js/modules/rules.js +2 -1
  45. umap/static/umap/js/modules/schema.js +5 -6
  46. umap/static/umap/js/modules/share.js +3 -3
  47. umap/static/umap/js/modules/sync/engine.js +18 -13
  48. umap/static/umap/js/modules/sync/updaters.js +8 -0
  49. umap/static/umap/js/modules/sync/websocket.js +10 -5
  50. umap/static/umap/js/modules/tableeditor.js +3 -2
  51. umap/static/umap/js/modules/ui/bar.js +17 -9
  52. umap/static/umap/js/modules/ui/base.js +7 -24
  53. umap/static/umap/js/modules/ui/tooltip.js +19 -11
  54. umap/static/umap/js/modules/umap.js +39 -27
  55. umap/static/umap/js/modules/utils.js +196 -12
  56. umap/static/umap/js/umap.controls.js +1 -13
  57. umap/static/umap/locale/br.js +21 -13
  58. umap/static/umap/locale/br.json +21 -13
  59. umap/static/umap/locale/ca.js +12 -4
  60. umap/static/umap/locale/ca.json +12 -4
  61. umap/static/umap/locale/cs_CZ.js +10 -4
  62. umap/static/umap/locale/cs_CZ.json +10 -4
  63. umap/static/umap/locale/de.js +12 -4
  64. umap/static/umap/locale/de.json +12 -4
  65. umap/static/umap/locale/el.js +12 -4
  66. umap/static/umap/locale/el.json +12 -4
  67. umap/static/umap/locale/en.js +9 -4
  68. umap/static/umap/locale/en.json +9 -4
  69. umap/static/umap/locale/es.js +20 -12
  70. umap/static/umap/locale/es.json +20 -12
  71. umap/static/umap/locale/eu.js +12 -4
  72. umap/static/umap/locale/eu.json +12 -4
  73. umap/static/umap/locale/fa_IR.js +12 -4
  74. umap/static/umap/locale/fa_IR.json +12 -4
  75. umap/static/umap/locale/fr.js +10 -5
  76. umap/static/umap/locale/fr.json +10 -5
  77. umap/static/umap/locale/gl.js +353 -345
  78. umap/static/umap/locale/gl.json +353 -345
  79. umap/static/umap/locale/hu.js +34 -26
  80. umap/static/umap/locale/hu.json +34 -26
  81. umap/static/umap/locale/it.js +100 -92
  82. umap/static/umap/locale/it.json +100 -92
  83. umap/static/umap/locale/ms.js +12 -4
  84. umap/static/umap/locale/ms.json +12 -4
  85. umap/static/umap/locale/nl.js +12 -4
  86. umap/static/umap/locale/nl.json +12 -4
  87. umap/static/umap/locale/pl.js +12 -4
  88. umap/static/umap/locale/pl.json +12 -4
  89. umap/static/umap/locale/pt.js +12 -4
  90. umap/static/umap/locale/pt.json +12 -4
  91. umap/static/umap/locale/pt_PT.js +12 -4
  92. umap/static/umap/locale/pt_PT.json +12 -4
  93. umap/static/umap/locale/th_TH.js +12 -4
  94. umap/static/umap/locale/th_TH.json +12 -4
  95. umap/static/umap/locale/zh_TW.js +10 -4
  96. umap/static/umap/locale/zh_TW.json +10 -4
  97. umap/static/umap/map.css +12 -8
  98. umap/static/umap/nav.css +2 -3
  99. umap/static/umap/unittests/utils.js +14 -0
  100. umap/static/umap/vars.css +2 -0
  101. umap/sync/__init__.py +0 -0
  102. umap/sync/app.py +181 -0
  103. umap/sync/payloads.py +49 -0
  104. umap/templates/auth/user_detail.html +4 -0
  105. umap/templates/auth/user_form.html +9 -6
  106. umap/templates/auth/user_stars.html +4 -0
  107. umap/templates/base.html +1 -1
  108. umap/templates/registration/login.html +2 -5
  109. umap/templates/umap/about.html +5 -0
  110. umap/templates/umap/about_summary.html +2 -2
  111. umap/templates/umap/components/provider.html +8 -0
  112. umap/templates/umap/js.html +0 -3
  113. umap/templates/umap/map_detail.html +1 -1
  114. umap/templates/umap/password_change.html +4 -0
  115. umap/templates/umap/password_change_done.html +4 -0
  116. umap/templates/umap/search.html +4 -0
  117. umap/templates/umap/team_confirm_delete.html +4 -0
  118. umap/templates/umap/team_detail.html +4 -0
  119. umap/templates/umap/team_form.html +4 -0
  120. umap/templates/umap/user_dashboard.html +1 -1
  121. umap/templates/umap/user_teams.html +4 -0
  122. umap/tests/base.py +3 -1
  123. umap/tests/integration/conftest.py +16 -23
  124. umap/tests/integration/test_basics.py +2 -2
  125. umap/tests/integration/test_caption.py +1 -0
  126. umap/tests/integration/test_draw_polygon.py +3 -3
  127. umap/tests/integration/test_edit_datalayer.py +1 -1
  128. umap/tests/integration/test_edit_map.py +3 -3
  129. umap/tests/integration/test_edit_polygon.py +1 -1
  130. umap/tests/integration/test_import.py +23 -1
  131. umap/tests/integration/test_optimistic_merge.py +1 -0
  132. umap/tests/integration/test_picto.py +8 -8
  133. umap/tests/integration/test_save.py +1 -0
  134. umap/tests/integration/test_star.py +13 -9
  135. umap/tests/integration/test_tableeditor.py +1 -0
  136. umap/tests/integration/test_websocket_sync.py +112 -33
  137. umap/tests/settings.py +2 -0
  138. umap/tests/test_datalayer.py +2 -3
  139. umap/tests/test_datalayer_views.py +20 -1
  140. umap/tests/test_empty_trash.py +10 -3
  141. umap/tests/test_map_views.py +11 -0
  142. umap/utils.py +24 -11
  143. umap/views.py +37 -6
  144. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/METADATA +15 -15
  145. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/RECORD +148 -147
  146. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/WHEEL +1 -1
  147. umap/management/commands/run_websocket_server.py +0 -23
  148. umap/settings/local_s3.py +0 -45
  149. umap/static/umap/bitbucket.png +0 -0
  150. umap/static/umap/github.png +0 -0
  151. umap/static/umap/js/umap.forms.js +0 -1242
  152. umap/static/umap/keycloak.png +0 -0
  153. umap/static/umap/openstreetmap.png +0 -0
  154. umap/static/umap/twitter.png +0 -0
  155. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +0 -468
  156. umap/tests/test_websocket_server.py +0 -22
  157. umap/websocket_server.py +0 -202
  158. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/entry_points.txt +0 -0
  159. {umap_project-2.8.1.dist-info → umap_project-2.9.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,8 @@
1
1
  import re
2
2
 
3
3
  import pytest
4
+ import redis
5
+ from django.conf import settings
4
6
  from playwright.sync_api import expect
5
7
 
6
8
  from umap.models import DataLayer, Map
@@ -9,11 +11,21 @@ from ..base import DataLayerFactory, MapFactory
9
11
 
10
12
  DATALAYER_UPDATE = re.compile(r".*/datalayer/update/.*")
11
13
 
14
+ pytestmark = pytest.mark.django_db
15
+
16
+
17
+ def setup_function():
18
+ # Sync client to prevent headache with pytest / pytest-asyncio and async
19
+ client = redis.from_url(settings.REDIS_URL)
20
+ # Make sure there are no dead peers in the Redis hash, otherwise asking for
21
+ # operations from another peer may never be answered
22
+ # FIXME this should not happen in an ideal world
23
+ assert client.connection_pool.connection_kwargs["db"] == 15
24
+ client.flushdb()
25
+
12
26
 
13
27
  @pytest.mark.xdist_group(name="websockets")
14
- def test_websocket_connection_can_sync_markers(
15
- new_page, live_server, websocket_server, tilelayer
16
- ):
28
+ def test_websocket_connection_can_sync_markers(new_page, asgi_live_server, tilelayer):
17
29
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
18
30
  map.settings["properties"]["syncEnabled"] = True
19
31
  map.save()
@@ -21,9 +33,9 @@ def test_websocket_connection_can_sync_markers(
21
33
 
22
34
  # Create two tabs
23
35
  peerA = new_page("Page A")
24
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
36
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
25
37
  peerB = new_page("Page B")
26
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
38
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
27
39
 
28
40
  a_marker_pane = peerA.locator(".leaflet-marker-pane > div")
29
41
  b_marker_pane = peerB.locator(".leaflet-marker-pane > div")
@@ -44,6 +56,7 @@ def test_websocket_connection_can_sync_markers(
44
56
  expect(peerB.get_by_role("button", name="Cancel edits")).to_be_hidden()
45
57
  peerA.locator("body").type("Synced name")
46
58
  peerA.locator("body").press("Escape")
59
+ peerA.wait_for_timeout(300)
47
60
 
48
61
  peerB.locator(".leaflet-marker-icon").first.click()
49
62
  peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
@@ -79,9 +92,7 @@ def test_websocket_connection_can_sync_markers(
79
92
 
80
93
 
81
94
  @pytest.mark.xdist_group(name="websockets")
82
- def test_websocket_connection_can_sync_polygons(
83
- context, live_server, websocket_server, tilelayer
84
- ):
95
+ def test_websocket_connection_can_sync_polygons(context, asgi_live_server, tilelayer):
85
96
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
86
97
  map.settings["properties"]["syncEnabled"] = True
87
98
  map.save()
@@ -89,9 +100,9 @@ def test_websocket_connection_can_sync_polygons(
89
100
 
90
101
  # Create two tabs
91
102
  peerA = context.new_page()
92
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
103
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
93
104
  peerB = context.new_page()
94
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
105
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
95
106
 
96
107
  b_map_el = peerB.locator("#map")
97
108
 
@@ -164,7 +175,7 @@ def test_websocket_connection_can_sync_polygons(
164
175
 
165
176
  @pytest.mark.xdist_group(name="websockets")
166
177
  def test_websocket_connection_can_sync_map_properties(
167
- new_page, live_server, websocket_server, tilelayer
178
+ new_page, asgi_live_server, tilelayer
168
179
  ):
169
180
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
170
181
  map.settings["properties"]["syncEnabled"] = True
@@ -173,9 +184,9 @@ def test_websocket_connection_can_sync_map_properties(
173
184
 
174
185
  # Create two tabs
175
186
  peerA = new_page()
176
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
187
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
177
188
  peerB = new_page()
178
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
189
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
179
190
 
180
191
  # Name change is synced
181
192
  peerA.get_by_role("link", name="Edit map name and caption").click()
@@ -187,16 +198,18 @@ def test_websocket_connection_can_sync_map_properties(
187
198
  # Zoom control is synced
188
199
  peerB.get_by_role("link", name="Map advanced properties").click()
189
200
  peerB.locator("summary").filter(has_text="User interface options").click()
190
- peerB.locator("div").filter(
191
- has_text=re.compile(r"^Display the zoom control")
192
- ).locator("label").nth(2).click()
201
+ switch = peerB.locator("div.formbox").filter(
202
+ has_text=re.compile("Display the zoom control")
203
+ )
204
+ expect(switch).to_be_visible()
205
+ switch.get_by_text("Never").click()
193
206
 
194
207
  expect(peerA.locator(".leaflet-control-zoom")).to_be_hidden()
195
208
 
196
209
 
197
210
  @pytest.mark.xdist_group(name="websockets")
198
211
  def test_websocket_connection_can_sync_datalayer_properties(
199
- new_page, live_server, websocket_server, tilelayer
212
+ new_page, asgi_live_server, tilelayer
200
213
  ):
201
214
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
202
215
  map.settings["properties"]["syncEnabled"] = True
@@ -205,9 +218,9 @@ def test_websocket_connection_can_sync_datalayer_properties(
205
218
 
206
219
  # Create two tabs
207
220
  peerA = new_page()
208
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
221
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
209
222
  peerB = new_page()
210
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
223
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
211
224
 
212
225
  # Layer addition, name and type are synced
213
226
  peerA.get_by_role("link", name="Manage layers").click()
@@ -225,7 +238,7 @@ def test_websocket_connection_can_sync_datalayer_properties(
225
238
 
226
239
  @pytest.mark.xdist_group(name="websockets")
227
240
  def test_websocket_connection_can_sync_cloned_polygons(
228
- context, live_server, websocket_server, tilelayer
241
+ context, asgi_live_server, tilelayer
229
242
  ):
230
243
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
231
244
  map.settings["properties"]["syncEnabled"] = True
@@ -234,9 +247,9 @@ def test_websocket_connection_can_sync_cloned_polygons(
234
247
 
235
248
  # Create two tabs
236
249
  peerA = context.new_page()
237
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
250
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
238
251
  peerB = context.new_page()
239
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
252
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
240
253
 
241
254
  b_map_el = peerB.locator("#map")
242
255
 
@@ -278,7 +291,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
278
291
  peerB.locator("path").nth(1).drag_to(b_map_el, target_position={"x": 400, "y": 400})
279
292
  peerB.locator("path").nth(1).click()
280
293
  peerB.locator("summary").filter(has_text="Shape properties").click()
281
- peerB.locator(".header > a:nth-child(2)").first.click()
294
+ peerB.locator(".umap-field-color button.define").first.click()
282
295
  peerB.get_by_title("Orchid", exact=True).first.click()
283
296
  peerB.locator("#map").press("Escape")
284
297
  peerB.get_by_role("button", name="Save").click()
@@ -288,7 +301,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
288
301
 
289
302
  @pytest.mark.xdist_group(name="websockets")
290
303
  def test_websocket_connection_can_sync_late_joining_peer(
291
- new_page, live_server, websocket_server, tilelayer
304
+ new_page, asgi_live_server, tilelayer
292
305
  ):
293
306
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
294
307
  map.settings["properties"]["syncEnabled"] = True
@@ -297,7 +310,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
297
310
 
298
311
  # Create first peer (A) and have it join immediately
299
312
  peerA = new_page("Page A")
300
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
313
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
301
314
 
302
315
  # Add a marker from peer A
303
316
  a_create_marker = peerA.get_by_title("Draw a marker")
@@ -308,6 +321,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
308
321
  a_map_el.click(position={"x": 220, "y": 220})
309
322
  peerA.locator("body").type("First marker")
310
323
  peerA.locator("body").press("Escape")
324
+ peerA.wait_for_timeout(300)
311
325
 
312
326
  # Add a polygon from peer A
313
327
  create_polygon = peerA.locator(".leaflet-control-toolbar ").get_by_title(
@@ -324,7 +338,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
324
338
 
325
339
  # Now create peer B and have it join
326
340
  peerB = new_page("Page B")
327
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
341
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
328
342
 
329
343
  # Check if peer B has received all the updates
330
344
  b_marker_pane = peerB.locator(".leaflet-marker-pane > div")
@@ -349,7 +363,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
349
363
 
350
364
 
351
365
  @pytest.mark.xdist_group(name="websockets")
352
- def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelayer):
366
+ def test_should_sync_datalayers(new_page, asgi_live_server, tilelayer):
353
367
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
354
368
  map.settings["properties"]["syncEnabled"] = True
355
369
  map.save()
@@ -358,9 +372,9 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
358
372
 
359
373
  # Create two tabs
360
374
  peerA = new_page("Page A")
361
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
375
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
362
376
  peerB = new_page("Page B")
363
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
377
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
364
378
 
365
379
  # Create a new layer from peerA
366
380
  peerA.get_by_role("link", name="Manage layers").click()
@@ -421,15 +435,80 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
421
435
 
422
436
 
423
437
  @pytest.mark.xdist_group(name="websockets")
424
- def test_create_and_sync_map(
425
- new_page, live_server, websocket_server, tilelayer, login, user
426
- ):
438
+ def test_should_sync_datalayers_delete(new_page, asgi_live_server, tilelayer):
439
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
440
+ map.settings["properties"]["syncEnabled"] = True
441
+ map.save()
442
+ data1 = {
443
+ "type": "FeatureCollection",
444
+ "features": [
445
+ {
446
+ "type": "Feature",
447
+ "properties": {
448
+ "name": "Point 1",
449
+ },
450
+ "geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
451
+ },
452
+ ],
453
+ "_umap_options": {
454
+ "name": "datalayer 1",
455
+ },
456
+ }
457
+ data2 = {
458
+ "type": "FeatureCollection",
459
+ "features": [
460
+ {
461
+ "type": "Feature",
462
+ "properties": {
463
+ "name": "Point 2",
464
+ },
465
+ "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
466
+ },
467
+ ],
468
+ "_umap_options": {
469
+ "name": "datalayer 2",
470
+ },
471
+ }
472
+ DataLayerFactory(map=map, data=data1)
473
+ DataLayerFactory(map=map, data=data2)
474
+
475
+ # Create two tabs
476
+ peerA = new_page("Page A")
477
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
478
+ peerB = new_page("Page B")
479
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
480
+
481
+ peerA.get_by_role("button", name="Open browser").click()
482
+ expect(peerA.get_by_text("datalayer 1")).to_be_visible()
483
+ expect(peerA.get_by_text("datalayer 2")).to_be_visible()
484
+ peerB.get_by_role("button", name="Open browser").click()
485
+ expect(peerB.get_by_text("datalayer 1")).to_be_visible()
486
+ expect(peerB.get_by_text("datalayer 2")).to_be_visible()
487
+
488
+ # Delete "datalayer 2" in peerA
489
+ peerA.locator(".datalayer").get_by_role("button", name="Delete layer").first.click()
490
+ peerA.get_by_role("button", name="OK").click()
491
+ expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
492
+ expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
493
+
494
+ # Save delete to the server
495
+ with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
496
+ peerA.get_by_role("button", name="Save").click()
497
+ expect(peerA.get_by_text("datalayer 2")).to_be_hidden()
498
+ expect(peerB.get_by_text("datalayer 2")).to_be_hidden()
499
+
500
+
501
+ @pytest.mark.xdist_group(name="websockets")
502
+ def test_create_and_sync_map(new_page, asgi_live_server, tilelayer, login, user):
427
503
  # Create a syncable map with peerA
428
504
  peerA = login(user, prefix="Page A")
429
- peerA.goto(f"{live_server.url}/en/map/new/")
505
+ peerA.goto(f"{asgi_live_server.url}/en/map/new/")
506
+ peerA.get_by_role("link", name="Map advanced properties").click()
507
+ expect(peerA.get_by_text("Real-time collaboration", exact=True)).to_be_hidden()
430
508
  with peerA.expect_response(re.compile("./map/create/.*")):
431
509
  peerA.get_by_role("button", name="Save Draft").click()
432
510
  peerA.get_by_role("link", name="Map advanced properties").click()
511
+ expect(peerA.get_by_text("Real-time collaboration", exact=True)).to_be_visible()
433
512
  peerA.get_by_text("Real-time collaboration", exact=True).click()
434
513
  peerA.get_by_text("Enable real-time").click()
435
514
  peerA.get_by_role("link", name="Update permissions and editors").click()
umap/tests/settings.py CHANGED
@@ -29,3 +29,5 @@ PASSWORD_HASHERS = [
29
29
  WEBSOCKET_ENABLED = True
30
30
  WEBSOCKET_BACK_PORT = "8010"
31
31
  WEBSOCKET_FRONT_URI = "ws://localhost:8010"
32
+
33
+ REDIS_URL = "redis://localhost:6379/15"
@@ -1,4 +1,3 @@
1
- import tempfile
2
1
  from pathlib import Path
3
2
 
4
3
  import pytest
@@ -290,5 +289,5 @@ def test_should_remove_all_versions_on_delete(map, settings):
290
289
  datalayer.geojson.storage.save(root / f"{path}.gz", ContentFile("{}"))
291
290
  assert len(datalayer.geojson.storage.listdir(root)[1]) == 10 + before
292
291
  datalayer.delete()
293
- found = datalayer.geojson.storage.listdir(root)[1]
294
- assert found == [other, f"{other}.gz"]
292
+ found = set(datalayer.geojson.storage.listdir(root)[1])
293
+ assert found == {other, f"{other}.gz"}
@@ -1,6 +1,8 @@
1
1
  import json
2
2
  from copy import deepcopy
3
+ from datetime import datetime, timedelta
3
4
  from pathlib import Path
5
+ from unittest import mock
4
6
  from uuid import uuid4
5
7
 
6
8
  import pytest
@@ -156,11 +158,14 @@ def test_should_not_be_possible_to_update_with_wrong_map_id_in_url(
156
158
 
157
159
 
158
160
  def test_delete(client, datalayer, map):
161
+ assert map.datalayers.count() == 1
159
162
  url = reverse("datalayer_delete", args=(map.pk, datalayer.pk))
160
163
  client.login(username=map.owner.username, password="123123")
161
164
  response = client.post(url, {}, follow=True)
162
165
  assert response.status_code == 200
163
- assert not DataLayer.objects.filter(pk=datalayer.pk).count()
166
+ assert DataLayer.objects.filter(pk=datalayer.pk).count()
167
+ assert map.datalayers.count() == 0
168
+ assert DataLayer.objects.get(pk=datalayer.pk).share_status == DataLayer.DELETED
164
169
  # Check that map has not been impacted
165
170
  assert Map.objects.filter(pk=map.pk).exists()
166
171
  # Test response is a json
@@ -621,3 +626,17 @@ def test_optimistic_merge_conflicting_change_raises(
621
626
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
622
627
  merged_features = json.load(modified_datalayer.geojson)["features"]
623
628
  assert merged_features == client1_data["features"]
629
+
630
+
631
+ def test_saving_datalayer_should_change_map_last_modified(
632
+ client, datalayer, map, post_data
633
+ ):
634
+ with mock.patch("django.utils.timezone.now") as mocked:
635
+ mocked.return_value = datetime.utcnow() - timedelta(days=8)
636
+ map.save() # Change last_modified to past
637
+ old_modified_at = map.modified_at.date()
638
+ url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
639
+ client.login(username=map.owner.username, password="123123")
640
+ response = client.post(url, post_data, follow=True)
641
+ assert response.status_code == 200
642
+ assert Map.objects.get(pk=map.pk).modified_at.date() != old_modified_at
@@ -4,15 +4,17 @@ from unittest import mock
4
4
  import pytest
5
5
  from django.core.management import call_command
6
6
 
7
- from umap.models import Map
7
+ from umap.models import DataLayer, Map
8
8
 
9
- from .base import MapFactory
9
+ from .base import DataLayerFactory, MapFactory
10
10
 
11
11
  pytestmark = pytest.mark.django_db
12
12
 
13
13
 
14
14
  def test_empty_trash(user):
15
15
  recent = MapFactory(owner=user)
16
+ recent_layer = DataLayerFactory(map=recent)
17
+ deleted_layer = DataLayerFactory(map=recent)
16
18
  recent_deleted = MapFactory(owner=user)
17
19
  recent_deleted.move_to_trash()
18
20
  recent_deleted.save()
@@ -20,15 +22,20 @@ def test_empty_trash(user):
20
22
  mocked.return_value = datetime.utcnow() - timedelta(days=8)
21
23
  old_deleted = MapFactory(owner=user)
22
24
  old_deleted.move_to_trash()
23
- old_deleted.save()
25
+ deleted_layer.move_to_trash()
24
26
  old = MapFactory(owner=user)
25
27
  assert Map.objects.count() == 4
28
+ assert DataLayer.objects.count() == 2
26
29
  call_command("empty_trash", "--days=7", "--dry-run")
27
30
  assert Map.objects.count() == 4
31
+ assert DataLayer.objects.count() == 2
28
32
  call_command("empty_trash", "--days=9")
29
33
  assert Map.objects.count() == 4
34
+ assert DataLayer.objects.count() == 2
30
35
  call_command("empty_trash", "--days=7")
31
36
  assert not Map.objects.filter(pk=old_deleted.pk)
32
37
  assert Map.objects.filter(pk=old.pk)
33
38
  assert Map.objects.filter(pk=recent.pk)
34
39
  assert Map.objects.filter(pk=recent_deleted.pk)
40
+ assert not DataLayer.objects.filter(pk=deleted_layer.pk)
41
+ assert DataLayer.objects.filter(pk=recent_layer.pk)
@@ -810,6 +810,17 @@ def test_oembed_shared_status_map(client, map, datalayer, share_status):
810
810
  assert response.status_code == 403
811
811
 
812
812
 
813
+ def test_download_does_not_include_delete_datalayers(client, map, datalayer):
814
+ datalayer.share_status = DataLayer.DELETED
815
+ datalayer.save()
816
+ url = reverse("map_download", args=(map.pk,))
817
+ response = client.get(url)
818
+ assert response.status_code == 200
819
+ # Test response is a json
820
+ j = json.loads(response.content.decode())
821
+ assert j["layers"] == []
822
+
823
+
813
824
  def test_oembed_no_url_map(client, map, datalayer):
814
825
  url = reverse("map_oembed")
815
826
  response = client.get(url)
umap/utils.py CHANGED
@@ -7,23 +7,36 @@ from django.core.serializers.json import DjangoJSONEncoder
7
7
  from django.urls import URLPattern, URLResolver, get_resolver
8
8
 
9
9
 
10
- def _urls_for_js(urls=None):
10
+ def _get_url_names(module):
11
+ def _get_names(resolver):
12
+ names = []
13
+ for pattern in resolver.url_patterns:
14
+ if getattr(pattern, "url_patterns", None):
15
+ # Do not add "admin" and other third party apps urls.
16
+ if not pattern.namespace:
17
+ names.extend(_get_names(pattern))
18
+ elif getattr(pattern, "name", None):
19
+ names.append(pattern.name)
20
+ return names
21
+
22
+ return _get_names(get_resolver(module))
23
+
24
+
25
+ def _urls_for_js():
11
26
  """
12
27
  Return templated URLs prepared for javascript.
13
28
  """
14
- if urls is None:
15
- # prevent circular import
16
- from .urls import i18n_urls, urlpatterns
17
-
18
- urls = [
19
- url.name for url in urlpatterns + i18n_urls if getattr(url, "name", None)
20
- ]
21
- urls = dict(zip(urls, [get_uri_template(url) for url in urls]))
29
+ urls = {}
30
+ for module in ["umap.urls", "umap.sync.app"]:
31
+ names = _get_url_names(module)
32
+ urls.update(
33
+ dict(zip(names, [get_uri_template(url, module=module) for url in names]))
34
+ )
22
35
  urls.update(getattr(settings, "UMAP_EXTRA_URLS", {}))
23
36
  return urls
24
37
 
25
38
 
26
- def get_uri_template(urlname, args=None, prefix=""):
39
+ def get_uri_template(urlname, args=None, prefix="", module=None):
27
40
  """
28
41
  Utility function to return an URI Template from a named URL in django
29
42
  Copied from django-digitalpaper.
@@ -45,7 +58,7 @@ def get_uri_template(urlname, args=None, prefix=""):
45
58
  paths = template % dict([p, "{%s}" % p] for p in args)
46
59
  return "%s/%s" % (prefix, paths)
47
60
 
48
- resolver = get_resolver(None)
61
+ resolver = get_resolver(module)
49
62
  parts = urlname.split(":")
50
63
  if len(parts) > 1 and parts[0] in resolver.namespace_dict:
51
64
  namespace = parts[0]
umap/views.py CHANGED
@@ -15,7 +15,7 @@ from urllib.request import Request, build_opener
15
15
 
16
16
  from django.conf import settings
17
17
  from django.contrib import messages
18
- from django.contrib.auth import get_user_model
18
+ 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
@@ -605,15 +605,22 @@ class MapDetailMixin(SessionMixin):
605
605
  "schema": Map.extra_schema,
606
606
  "id": self.get_id(),
607
607
  "starred": self.is_starred(),
608
+ "stars": self.stars(),
608
609
  "licences": dict((l.name, l.json) for l in Licence.objects.all()),
609
610
  "umap_version": VERSION,
610
611
  "featuresHaveOwner": settings.UMAP_DEFAULT_FEATURES_HAVE_OWNERS,
611
612
  "websocketEnabled": settings.WEBSOCKET_ENABLED,
612
- "websocketURI": settings.WEBSOCKET_FRONT_URI,
613
613
  "importers": settings.UMAP_IMPORTERS,
614
614
  "defaultLabelKeys": settings.UMAP_LABEL_KEYS,
615
615
  }
616
616
  created = bool(getattr(self, "object", None))
617
+ if created:
618
+ properties.update(
619
+ {
620
+ "created_at": self.object.created_at,
621
+ "modified_at": self.object.modified_at,
622
+ }
623
+ )
617
624
  if (created and self.object.owner) or (not created and not user.is_anonymous):
618
625
  edit_statuses = Map.EDIT_STATUS
619
626
  datalayer_statuses = DataLayer.EDIT_STATUS
@@ -671,6 +678,9 @@ class MapDetailMixin(SessionMixin):
671
678
  def is_starred(self):
672
679
  return False
673
680
 
681
+ def stars(self):
682
+ return 0
683
+
674
684
  def get_geojson(self):
675
685
  return {
676
686
  "geometry": {
@@ -732,14 +742,14 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
732
742
  def get_datalayers(self):
733
743
  # When initializing datalayers from map, we cannot get the reference version
734
744
  # the normal way, which is from the header X-Reference-Version
735
- return [dl.metadata(self.request) for dl in self.object.datalayer_set.all()]
745
+ return [dl.metadata(self.request) for dl in self.object.datalayers]
736
746
 
737
747
  @property
738
748
  def edit_mode(self):
739
749
  edit_mode = "disabled"
740
750
  if self.object.can_edit(self.request):
741
751
  edit_mode = "advanced"
742
- elif any(d.can_edit(self.request) for d in self.object.datalayer_set.all()):
752
+ elif any(d.can_edit(self.request) for d in self.object.datalayers):
743
753
  edit_mode = "simple"
744
754
  return edit_mode
745
755
 
@@ -773,6 +783,9 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
773
783
  return False
774
784
  return Star.objects.filter(by=user, map=self.object).exists()
775
785
 
786
+ def stars(self):
787
+ return Star.objects.filter(map=self.object).count()
788
+
776
789
 
777
790
  class MapDownload(DetailView):
778
791
  model = Map
@@ -1074,7 +1087,9 @@ class ToggleMapStarStatus(View):
1074
1087
  else:
1075
1088
  Star.objects.create(map=map_inst, by=self.request.user)
1076
1089
  status = True
1077
- return simple_json_response(starred=status)
1090
+ return simple_json_response(
1091
+ starred=status, stars=Star.objects.filter(map=map_inst).count()
1092
+ )
1078
1093
 
1079
1094
 
1080
1095
  class MapShortUrl(RedirectView):
@@ -1293,6 +1308,7 @@ class DataLayerUpdate(FormLessEditMixin, UpdateView):
1293
1308
 
1294
1309
  def form_valid(self, form):
1295
1310
  self.object = form.save()
1311
+ self.object.map.save(update_fields=["modified_at"])
1296
1312
  data = {**self.object.metadata(self.request)}
1297
1313
  if self.request.session.get("needs_reload"):
1298
1314
  data["geojson"] = json.loads(self.object.geojson.read().decode())
@@ -1309,7 +1325,7 @@ class DataLayerDelete(DeleteView):
1309
1325
  self.object = self.get_object()
1310
1326
  if self.object.map != self.kwargs["map_inst"]:
1311
1327
  return HttpResponseForbidden()
1312
- self.object.delete()
1328
+ self.object.move_to_trash()
1313
1329
  return simple_json_response(info=_("Layer successfully deleted."))
1314
1330
 
1315
1331
 
@@ -1403,3 +1419,18 @@ class LoginPopupEnd(TemplateView):
1403
1419
  """
1404
1420
 
1405
1421
  template_name = "umap/login_popup_end.html"
1422
+
1423
+ def get(self, *args, **kwargs):
1424
+ backend = self.request.session[BACKEND_SESSION_KEY]
1425
+ if backend in settings.DEPRECATED_AUTHENTICATION_BACKENDS:
1426
+ name = backend.split(".")[-1]
1427
+ messages.error(
1428
+ self.request,
1429
+ _(
1430
+ "Using “%(name)s” to authenticate is deprecated. "
1431
+ "Please configure another provider in your profile page."
1432
+ )
1433
+ % {"name": name},
1434
+ )
1435
+ return HttpResponseRedirect(reverse("user_profile"))
1436
+ return super().get(*args, **kwargs)
@@ -1,9 +1,10 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: umap-project
3
- Version: 2.8.1
3
+ Version: 2.9.0b0
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>
7
+ License-File: LICENSE
7
8
  Keywords: django,geodjango,leaflet,map,openstreetmap
8
9
  Classifier: Development Status :: 4 - Beta
9
10
  Classifier: Intended Audience :: Developers
@@ -16,38 +17,37 @@ Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
18
  Requires-Python: >=3.10
18
19
  Requires-Dist: django-agnocomplete==2.2.0
19
- Requires-Dist: django-environ==0.11.2
20
+ Requires-Dist: django-environ==0.12.0
20
21
  Requires-Dist: django-probes==1.7.0
21
- Requires-Dist: django==5.1.4
22
- Requires-Dist: pillow==11.0.0
23
- Requires-Dist: psycopg==3.2.3
22
+ Requires-Dist: django==5.1.5
23
+ Requires-Dist: pillow==11.1.0
24
+ Requires-Dist: psycopg==3.2.4
24
25
  Requires-Dist: rcssmin==1.2.0
25
26
  Requires-Dist: requests==2.32.3
26
27
  Requires-Dist: rjsmin==1.2.3
27
28
  Requires-Dist: social-auth-app-django==5.4.2
28
29
  Requires-Dist: social-auth-core==4.5.4
29
30
  Provides-Extra: dev
30
- Requires-Dist: djlint==1.36.3; extra == 'dev'
31
+ Requires-Dist: djlint==1.36.4; extra == 'dev'
31
32
  Requires-Dist: hatch==1.14.0; extra == 'dev'
32
33
  Requires-Dist: isort==5.13.2; extra == 'dev'
33
- Requires-Dist: mkdocs-material==9.5.48; extra == 'dev'
34
+ Requires-Dist: mkdocs-material==9.5.50; extra == 'dev'
34
35
  Requires-Dist: mkdocs-static-i18n==1.2.3; extra == 'dev'
35
36
  Requires-Dist: mkdocs==1.6.1; extra == 'dev'
36
- Requires-Dist: pymdown-extensions==10.13; extra == 'dev'
37
- Requires-Dist: ruff==0.8.4; extra == 'dev'
37
+ Requires-Dist: pymdown-extensions==10.14.1; extra == 'dev'
38
+ Requires-Dist: ruff==0.9.3; extra == 'dev'
38
39
  Requires-Dist: vermin==1.6.0; extra == 'dev'
39
40
  Provides-Extra: docker
40
41
  Requires-Dist: uwsgi==2.0.28; extra == 'docker'
41
42
  Provides-Extra: s3
42
43
  Requires-Dist: django-storages[s3]==1.14.4; extra == 's3'
43
44
  Provides-Extra: sync
44
- Requires-Dist: channels==4.2.0; extra == 'sync'
45
- Requires-Dist: daphne==4.1.2; extra == 'sync'
46
- Requires-Dist: pydantic==2.10.3; extra == 'sync'
47
- Requires-Dist: websockets==13.1; extra == 'sync'
45
+ Requires-Dist: pydantic==2.10.6; extra == 'sync'
46
+ Requires-Dist: redis==5.2.1; extra == 'sync'
48
47
  Provides-Extra: test
48
+ Requires-Dist: daphne==4.1.2; extra == 'test'
49
49
  Requires-Dist: factory-boy==3.3.1; extra == 'test'
50
- Requires-Dist: moto[s3]==5.0.24; extra == 'test'
50
+ Requires-Dist: moto[s3]==5.0.27; extra == 'test'
51
51
  Requires-Dist: playwright>=1.39; extra == 'test'
52
52
  Requires-Dist: pytest-django==4.9.0; extra == 'test'
53
53
  Requires-Dist: pytest-playwright==0.6.2; extra == 'test'