umap-project 2.8.1__py3-none-any.whl → 2.9.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 (262) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +15 -2
  3. umap/asgi.py +12 -7
  4. umap/context_processors.py +1 -0
  5. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/br/LC_MESSAGES/django.po +111 -67
  7. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/cs_CZ/LC_MESSAGES/django.po +110 -66
  9. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/el/LC_MESSAGES/django.po +129 -85
  11. umap/locale/en/LC_MESSAGES/django.po +103 -60
  12. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/es/LC_MESSAGES/django.po +114 -69
  14. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/fr/LC_MESSAGES/django.po +105 -61
  16. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/gl/LC_MESSAGES/django.po +216 -171
  18. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/hu/LC_MESSAGES/django.po +10 -10
  20. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/it/LC_MESSAGES/django.po +142 -98
  22. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/nl/LC_MESSAGES/django.po +196 -151
  24. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/pt/LC_MESSAGES/django.po +115 -71
  26. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/zh_TW/LC_MESSAGES/django.po +109 -65
  28. umap/management/commands/empty_trash.py +12 -1
  29. umap/migrations/0026_datalayer_modified_at_datalayer_share_status.py +26 -0
  30. umap/models.py +43 -13
  31. umap/settings/base.py +5 -2
  32. umap/static/umap/base.css +5 -2
  33. umap/static/umap/content.css +2 -22
  34. umap/static/umap/css/bar.css +39 -10
  35. umap/static/umap/css/contextmenu.css +14 -2
  36. umap/static/umap/css/form.css +33 -39
  37. umap/static/umap/css/icon.css +47 -5
  38. umap/static/umap/css/panel.css +20 -2
  39. umap/static/umap/css/popup.css +0 -1
  40. umap/static/umap/css/tooltip.css +33 -31
  41. umap/static/umap/img/16-white.svg +5 -3
  42. umap/static/umap/img/16.svg +1 -1
  43. umap/static/umap/img/24-white.svg +17 -16
  44. umap/static/umap/img/24.svg +29 -18
  45. umap/static/umap/img/providers/bitbucket.png +0 -0
  46. umap/static/umap/img/providers/github.png +0 -0
  47. umap/static/umap/img/providers/keycloak.png +0 -0
  48. umap/static/umap/img/providers/openstreetmap-oauth2.png +0 -0
  49. umap/static/umap/img/providers/twitter-oauth2.png +0 -0
  50. umap/static/umap/img/source/16-white.svg +6 -4
  51. umap/static/umap/img/source/16.svg +1 -1
  52. umap/static/umap/img/source/24-white.svg +20 -18
  53. umap/static/umap/img/source/24.svg +30 -19
  54. umap/static/umap/js/components/alerts/alert.js +4 -1
  55. umap/static/umap/js/modules/browser.js +8 -8
  56. umap/static/umap/js/modules/caption.js +30 -7
  57. umap/static/umap/js/modules/data/features.js +101 -56
  58. umap/static/umap/js/modules/data/layer.js +108 -83
  59. umap/static/umap/js/modules/form/builder.js +242 -0
  60. umap/static/umap/js/modules/form/fields.js +1346 -0
  61. umap/static/umap/js/modules/formatter.js +9 -8
  62. umap/static/umap/js/modules/help.js +20 -24
  63. umap/static/umap/js/modules/importer.js +6 -3
  64. umap/static/umap/js/modules/permissions.js +11 -6
  65. umap/static/umap/js/modules/rendering/icon.js +5 -1
  66. umap/static/umap/js/modules/rendering/layers/classified.js +12 -8
  67. umap/static/umap/js/modules/rendering/layers/cluster.js +11 -1
  68. umap/static/umap/js/modules/rendering/map.js +1 -23
  69. umap/static/umap/js/modules/rendering/ui.js +20 -38
  70. umap/static/umap/js/modules/rules.js +3 -2
  71. umap/static/umap/js/modules/saving.js +5 -0
  72. umap/static/umap/js/modules/schema.js +8 -6
  73. umap/static/umap/js/modules/share.js +3 -3
  74. umap/static/umap/js/modules/sync/engine.js +56 -26
  75. umap/static/umap/js/modules/sync/updaters.js +15 -6
  76. umap/static/umap/js/modules/sync/websocket.js +50 -37
  77. umap/static/umap/js/modules/tableeditor.js +3 -2
  78. umap/static/umap/js/modules/ui/bar.js +101 -9
  79. umap/static/umap/js/modules/ui/base.js +7 -24
  80. umap/static/umap/js/modules/ui/contextmenu.js +9 -2
  81. umap/static/umap/js/modules/ui/panel.js +5 -1
  82. umap/static/umap/js/modules/ui/tooltip.js +19 -11
  83. umap/static/umap/js/modules/umap.js +124 -71
  84. umap/static/umap/js/modules/utils.js +196 -12
  85. umap/static/umap/js/umap.controls.js +12 -354
  86. umap/static/umap/locale/am_ET.js +17 -5
  87. umap/static/umap/locale/am_ET.json +17 -5
  88. umap/static/umap/locale/ar.js +17 -5
  89. umap/static/umap/locale/ar.json +17 -5
  90. umap/static/umap/locale/ast.js +17 -5
  91. umap/static/umap/locale/ast.json +17 -5
  92. umap/static/umap/locale/bg.js +17 -5
  93. umap/static/umap/locale/bg.json +17 -5
  94. umap/static/umap/locale/br.js +33 -20
  95. umap/static/umap/locale/br.json +33 -20
  96. umap/static/umap/locale/ca.js +17 -5
  97. umap/static/umap/locale/ca.json +17 -5
  98. umap/static/umap/locale/cs_CZ.js +15 -5
  99. umap/static/umap/locale/cs_CZ.json +15 -5
  100. umap/static/umap/locale/da.js +17 -5
  101. umap/static/umap/locale/da.json +17 -5
  102. umap/static/umap/locale/de.js +17 -5
  103. umap/static/umap/locale/de.json +17 -5
  104. umap/static/umap/locale/el.js +63 -51
  105. umap/static/umap/locale/el.json +63 -51
  106. umap/static/umap/locale/en.js +15 -5
  107. umap/static/umap/locale/en.json +15 -5
  108. umap/static/umap/locale/en_US.json +17 -5
  109. umap/static/umap/locale/es.js +25 -13
  110. umap/static/umap/locale/es.json +25 -13
  111. umap/static/umap/locale/et.js +17 -5
  112. umap/static/umap/locale/et.json +17 -5
  113. umap/static/umap/locale/eu.js +17 -5
  114. umap/static/umap/locale/eu.json +17 -5
  115. umap/static/umap/locale/fa_IR.js +17 -5
  116. umap/static/umap/locale/fa_IR.json +17 -5
  117. umap/static/umap/locale/fi.js +17 -5
  118. umap/static/umap/locale/fi.json +17 -5
  119. umap/static/umap/locale/fr.js +16 -6
  120. umap/static/umap/locale/fr.json +16 -6
  121. umap/static/umap/locale/gl.js +357 -345
  122. umap/static/umap/locale/gl.json +357 -345
  123. umap/static/umap/locale/he.js +17 -5
  124. umap/static/umap/locale/he.json +17 -5
  125. umap/static/umap/locale/hr.js +17 -5
  126. umap/static/umap/locale/hr.json +17 -5
  127. umap/static/umap/locale/hu.js +39 -27
  128. umap/static/umap/locale/hu.json +39 -27
  129. umap/static/umap/locale/id.js +17 -5
  130. umap/static/umap/locale/id.json +17 -5
  131. umap/static/umap/locale/is.js +17 -5
  132. umap/static/umap/locale/is.json +17 -5
  133. umap/static/umap/locale/it.js +125 -113
  134. umap/static/umap/locale/it.json +125 -113
  135. umap/static/umap/locale/ja.js +17 -5
  136. umap/static/umap/locale/ja.json +17 -5
  137. umap/static/umap/locale/ko.js +17 -5
  138. umap/static/umap/locale/ko.json +17 -5
  139. umap/static/umap/locale/lt.js +17 -5
  140. umap/static/umap/locale/lt.json +17 -5
  141. umap/static/umap/locale/ms.js +17 -5
  142. umap/static/umap/locale/ms.json +17 -5
  143. umap/static/umap/locale/nl.js +132 -119
  144. umap/static/umap/locale/nl.json +132 -119
  145. umap/static/umap/locale/no.js +17 -5
  146. umap/static/umap/locale/no.json +17 -5
  147. umap/static/umap/locale/pl.js +17 -5
  148. umap/static/umap/locale/pl.json +17 -5
  149. umap/static/umap/locale/pl_PL.json +17 -5
  150. umap/static/umap/locale/pt.js +38 -25
  151. umap/static/umap/locale/pt.json +38 -25
  152. umap/static/umap/locale/pt_BR.js +17 -5
  153. umap/static/umap/locale/pt_BR.json +17 -5
  154. umap/static/umap/locale/pt_PT.js +17 -5
  155. umap/static/umap/locale/pt_PT.json +17 -5
  156. umap/static/umap/locale/ro.js +17 -5
  157. umap/static/umap/locale/ro.json +17 -5
  158. umap/static/umap/locale/ru.js +17 -5
  159. umap/static/umap/locale/ru.json +17 -5
  160. umap/static/umap/locale/sk_SK.js +17 -5
  161. umap/static/umap/locale/sk_SK.json +17 -5
  162. umap/static/umap/locale/sl.js +17 -5
  163. umap/static/umap/locale/sl.json +17 -5
  164. umap/static/umap/locale/sr.js +17 -5
  165. umap/static/umap/locale/sr.json +17 -5
  166. umap/static/umap/locale/sv.js +17 -5
  167. umap/static/umap/locale/sv.json +17 -5
  168. umap/static/umap/locale/th_TH.js +17 -5
  169. umap/static/umap/locale/th_TH.json +17 -5
  170. umap/static/umap/locale/tr.js +17 -5
  171. umap/static/umap/locale/tr.json +17 -5
  172. umap/static/umap/locale/uk_UA.js +17 -5
  173. umap/static/umap/locale/uk_UA.json +17 -5
  174. umap/static/umap/locale/vi.js +17 -5
  175. umap/static/umap/locale/vi.json +17 -5
  176. umap/static/umap/locale/vi_VN.json +17 -5
  177. umap/static/umap/locale/zh.js +17 -5
  178. umap/static/umap/locale/zh.json +17 -5
  179. umap/static/umap/locale/zh_CN.json +17 -5
  180. umap/static/umap/locale/zh_TW.Big5.json +17 -5
  181. umap/static/umap/locale/zh_TW.js +15 -5
  182. umap/static/umap/locale/zh_TW.json +15 -5
  183. umap/static/umap/map.css +29 -76
  184. umap/static/umap/nav.css +6 -3
  185. umap/static/umap/unittests/utils.js +14 -0
  186. umap/static/umap/vars.css +3 -0
  187. umap/static/umap/vendors/dompurify/purify.es.js +138 -354
  188. umap/static/umap/vendors/dompurify/purify.es.mjs.map +1 -1
  189. umap/static/umap/vendors/editable/Leaflet.Editable.js +1 -0
  190. umap/sync/__init__.py +0 -0
  191. umap/sync/app.py +187 -0
  192. umap/sync/payloads.py +56 -0
  193. umap/templates/auth/user_detail.html +4 -0
  194. umap/templates/auth/user_form.html +9 -6
  195. umap/templates/auth/user_stars.html +4 -0
  196. umap/templates/base.html +1 -1
  197. umap/templates/registration/login.html +2 -5
  198. umap/templates/umap/about.html +5 -0
  199. umap/templates/umap/about_summary.html +2 -2
  200. umap/templates/umap/components/provider.html +8 -0
  201. umap/templates/umap/content_footer.html +1 -1
  202. umap/templates/umap/css.html +0 -2
  203. umap/templates/umap/js.html +0 -4
  204. umap/templates/umap/map_detail.html +1 -1
  205. umap/templates/umap/password_change.html +4 -0
  206. umap/templates/umap/password_change_done.html +4 -0
  207. umap/templates/umap/search.html +4 -0
  208. umap/templates/umap/search_bar.html +1 -0
  209. umap/templates/umap/team_confirm_delete.html +4 -0
  210. umap/templates/umap/team_detail.html +4 -0
  211. umap/templates/umap/team_form.html +4 -0
  212. umap/templates/umap/user_dashboard.html +1 -1
  213. umap/templates/umap/user_teams.html +4 -0
  214. umap/tests/base.py +3 -1
  215. umap/tests/integration/conftest.py +16 -23
  216. umap/tests/integration/test_anonymous_owned_map.py +2 -2
  217. umap/tests/integration/test_basics.py +4 -7
  218. umap/tests/integration/test_caption.py +1 -0
  219. umap/tests/integration/test_categorized_layer.py +4 -8
  220. umap/tests/integration/test_choropleth.py +1 -1
  221. umap/tests/integration/test_conditional_rules.py +3 -3
  222. umap/tests/integration/test_draw_polygon.py +14 -22
  223. umap/tests/integration/test_draw_polyline.py +6 -14
  224. umap/tests/integration/test_edit_datalayer.py +11 -11
  225. umap/tests/integration/test_edit_map.py +30 -4
  226. umap/tests/integration/test_edit_marker.py +5 -5
  227. umap/tests/integration/test_edit_polygon.py +6 -6
  228. umap/tests/integration/test_features_id_generation.py +2 -6
  229. umap/tests/integration/test_import.py +115 -29
  230. umap/tests/integration/test_optimistic_merge.py +1 -0
  231. umap/tests/integration/test_owned_map.py +1 -1
  232. umap/tests/integration/test_picto.py +8 -8
  233. umap/tests/integration/test_save.py +3 -2
  234. umap/tests/integration/test_star.py +13 -9
  235. umap/tests/integration/test_tableeditor.py +8 -7
  236. umap/tests/integration/test_view_marker.py +10 -0
  237. umap/tests/integration/test_websocket_sync.py +239 -64
  238. umap/tests/settings.py +2 -0
  239. umap/tests/test_datalayer.py +2 -3
  240. umap/tests/test_datalayer_views.py +20 -1
  241. umap/tests/test_empty_trash.py +10 -3
  242. umap/tests/test_map_views.py +11 -0
  243. umap/utils.py +27 -11
  244. umap/views.py +37 -6
  245. {umap_project-2.8.1.dist-info → umap_project-2.9.0.dist-info}/METADATA +22 -22
  246. {umap_project-2.8.1.dist-info → umap_project-2.9.0.dist-info}/RECORD +249 -250
  247. {umap_project-2.8.1.dist-info → umap_project-2.9.0.dist-info}/WHEEL +1 -1
  248. umap/management/commands/run_websocket_server.py +0 -23
  249. umap/settings/local_s3.py +0 -45
  250. umap/static/umap/bitbucket.png +0 -0
  251. umap/static/umap/github.png +0 -0
  252. umap/static/umap/js/umap.forms.js +0 -1242
  253. umap/static/umap/keycloak.png +0 -0
  254. umap/static/umap/openstreetmap.png +0 -0
  255. umap/static/umap/twitter.png +0 -0
  256. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +0 -468
  257. umap/static/umap/vendors/toolbar/leaflet.toolbar.css +0 -1
  258. umap/static/umap/vendors/toolbar/leaflet.toolbar.js +0 -1
  259. umap/tests/test_websocket_server.py +0 -22
  260. umap/websocket_server.py +0 -202
  261. {umap_project-2.8.1.dist-info → umap_project-2.9.0.dist-info}/entry_points.txt +0 -0
  262. {umap_project-2.8.1.dist-info → umap_project-2.9.0.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,9 +56,10 @@ 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
- peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
62
+ peerB.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
50
63
  expect(peerB.locator('input[name="name"]')).to_have_value("Synced name")
51
64
 
52
65
  a_first_marker = peerA.locator("div:nth-child(4) > div:nth-child(2)").first
@@ -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,16 +100,14 @@ 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
 
98
109
  # Click on the Draw a polygon button on a new map.
99
- create_line = peerA.locator(".leaflet-control-toolbar ").get_by_title(
100
- "Draw a polygon"
101
- )
110
+ create_line = peerA.locator(".umap-edit-bar ").get_by_title("Draw a polygon")
102
111
  create_line.click()
103
112
 
104
113
  a_polygons = peerA.locator(".leaflet-overlay-pane path[fill='DarkBlue']")
@@ -113,11 +122,11 @@ def test_websocket_connection_can_sync_polygons(
113
122
  map.click(position={"x": 100, "y": 100})
114
123
  map.click(position={"x": 100, "y": 100})
115
124
 
116
- # It is created on peerA, but not yet synced
125
+ # It is created on peerA, and should be on peerB
117
126
  expect(a_polygons).to_have_count(1)
118
- expect(b_polygons).to_have_count(0)
127
+ expect(b_polygons).to_have_count(1)
119
128
 
120
- # Escaping the edition syncs
129
+ # Escaping the edition should not duplicate
121
130
  peerA.keyboard.press("Escape")
122
131
  expect(a_polygons).to_have_count(1)
123
132
  expect(b_polygons).to_have_count(1)
@@ -130,7 +139,7 @@ def test_websocket_connection_can_sync_polygons(
130
139
  assert b_polygon_bbox_t1 == a_polygon_bbox_t1
131
140
 
132
141
  b_polygon.click()
133
- peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
142
+ peerB.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
134
143
 
135
144
  edited_vertex = peerB.locator("div:nth-child(6)").first
136
145
  edited_vertex.drag_to(b_map_el, target_position={"x": 233, "y": 126})
@@ -144,7 +153,7 @@ def test_websocket_connection_can_sync_polygons(
144
153
 
145
154
  # Move the polygon on peer B and check it moved also on peer A
146
155
  b_polygon.click()
147
- peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
156
+ peerB.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
148
157
 
149
158
  b_polygon.drag_to(b_map_el, target_position={"x": 400, "y": 400})
150
159
  peerB.keyboard.press("Escape")
@@ -164,7 +173,7 @@ def test_websocket_connection_can_sync_polygons(
164
173
 
165
174
  @pytest.mark.xdist_group(name="websockets")
166
175
  def test_websocket_connection_can_sync_map_properties(
167
- new_page, live_server, websocket_server, tilelayer
176
+ new_page, asgi_live_server, tilelayer
168
177
  ):
169
178
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
170
179
  map.settings["properties"]["syncEnabled"] = True
@@ -173,30 +182,32 @@ def test_websocket_connection_can_sync_map_properties(
173
182
 
174
183
  # Create two tabs
175
184
  peerA = new_page()
176
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
185
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
177
186
  peerB = new_page()
178
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
187
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
179
188
 
180
189
  # Name change is synced
181
- peerA.get_by_role("link", name="Edit map name and caption").click()
190
+ peerA.get_by_role("button", name="Edit map name and caption").click()
182
191
  peerA.locator('input[name="name"]').click()
183
192
  peerA.locator('input[name="name"]').fill("it syncs!")
184
193
 
185
194
  expect(peerB.locator(".map-name").last).to_have_text("it syncs!")
186
195
 
187
196
  # Zoom control is synced
188
- peerB.get_by_role("link", name="Map advanced properties").click()
197
+ peerB.get_by_role("button", name="Map advanced properties").click()
189
198
  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()
199
+ switch = peerB.locator("div.formbox").filter(
200
+ has_text=re.compile("Display the zoom control")
201
+ )
202
+ expect(switch).to_be_visible()
203
+ switch.get_by_text("Never").click()
193
204
 
194
205
  expect(peerA.locator(".leaflet-control-zoom")).to_be_hidden()
195
206
 
196
207
 
197
208
  @pytest.mark.xdist_group(name="websockets")
198
209
  def test_websocket_connection_can_sync_datalayer_properties(
199
- new_page, live_server, websocket_server, tilelayer
210
+ new_page, asgi_live_server, tilelayer
200
211
  ):
201
212
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
202
213
  map.settings["properties"]["syncEnabled"] = True
@@ -205,19 +216,19 @@ def test_websocket_connection_can_sync_datalayer_properties(
205
216
 
206
217
  # Create two tabs
207
218
  peerA = new_page()
208
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
219
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
209
220
  peerB = new_page()
210
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
221
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
211
222
 
212
223
  # Layer addition, name and type are synced
213
- peerA.get_by_role("link", name="Manage layers").click()
224
+ peerA.get_by_role("button", name="Manage layers").click()
214
225
  peerA.get_by_role("button", name="Add a layer").click()
215
226
  peerA.locator('input[name="name"]').click()
216
227
  peerA.locator('input[name="name"]').fill("synced layer!")
217
228
  peerA.get_by_role("combobox").select_option("Choropleth")
218
229
  peerA.locator("body").press("Escape")
219
230
 
220
- peerB.get_by_role("link", name="Manage layers").click()
231
+ peerB.get_by_role("button", name="Manage layers").click()
221
232
  peerB.locator(".panel.right").get_by_role("button", name="Edit").first.click()
222
233
  expect(peerB.locator('input[name="name"]')).to_have_value("synced layer!")
223
234
  expect(peerB.get_by_role("combobox")).to_have_value("Choropleth")
@@ -225,7 +236,7 @@ def test_websocket_connection_can_sync_datalayer_properties(
225
236
 
226
237
  @pytest.mark.xdist_group(name="websockets")
227
238
  def test_websocket_connection_can_sync_cloned_polygons(
228
- context, live_server, websocket_server, tilelayer
239
+ context, asgi_live_server, tilelayer
229
240
  ):
230
241
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
231
242
  map.settings["properties"]["syncEnabled"] = True
@@ -234,16 +245,14 @@ def test_websocket_connection_can_sync_cloned_polygons(
234
245
 
235
246
  # Create two tabs
236
247
  peerA = context.new_page()
237
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
248
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
238
249
  peerB = context.new_page()
239
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
250
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
240
251
 
241
252
  b_map_el = peerB.locator("#map")
242
253
 
243
254
  # Click on the Draw a polygon button on a new map.
244
- create_line = peerA.locator(".leaflet-control-toolbar ").get_by_title(
245
- "Draw a polygon"
246
- )
255
+ create_line = peerA.locator(".umap-edit-bar ").get_by_title("Draw a polygon")
247
256
  create_line.click()
248
257
 
249
258
  a_polygons = peerA.locator(".leaflet-overlay-pane path[fill='DarkBlue']")
@@ -278,7 +287,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
278
287
  peerB.locator("path").nth(1).drag_to(b_map_el, target_position={"x": 400, "y": 400})
279
288
  peerB.locator("path").nth(1).click()
280
289
  peerB.locator("summary").filter(has_text="Shape properties").click()
281
- peerB.locator(".header > a:nth-child(2)").first.click()
290
+ peerB.locator(".umap-field-color button.define").first.click()
282
291
  peerB.get_by_title("Orchid", exact=True).first.click()
283
292
  peerB.locator("#map").press("Escape")
284
293
  peerB.get_by_role("button", name="Save").click()
@@ -288,7 +297,7 @@ def test_websocket_connection_can_sync_cloned_polygons(
288
297
 
289
298
  @pytest.mark.xdist_group(name="websockets")
290
299
  def test_websocket_connection_can_sync_late_joining_peer(
291
- new_page, live_server, websocket_server, tilelayer
300
+ new_page, asgi_live_server, tilelayer
292
301
  ):
293
302
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
294
303
  map.settings["properties"]["syncEnabled"] = True
@@ -297,7 +306,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
297
306
 
298
307
  # Create first peer (A) and have it join immediately
299
308
  peerA = new_page("Page A")
300
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
309
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
301
310
 
302
311
  # Add a marker from peer A
303
312
  a_create_marker = peerA.get_by_title("Draw a marker")
@@ -308,11 +317,10 @@ def test_websocket_connection_can_sync_late_joining_peer(
308
317
  a_map_el.click(position={"x": 220, "y": 220})
309
318
  peerA.locator("body").type("First marker")
310
319
  peerA.locator("body").press("Escape")
320
+ peerA.wait_for_timeout(300)
311
321
 
312
322
  # Add a polygon from peer A
313
- create_polygon = peerA.locator(".leaflet-control-toolbar ").get_by_title(
314
- "Draw a polygon"
315
- )
323
+ create_polygon = peerA.locator(".umap-edit-bar ").get_by_title("Draw a polygon")
316
324
  create_polygon.click()
317
325
 
318
326
  a_map_el.click(position={"x": 200, "y": 200})
@@ -324,7 +332,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
324
332
 
325
333
  # Now create peer B and have it join
326
334
  peerB = new_page("Page B")
327
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
335
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
328
336
 
329
337
  # Check if peer B has received all the updates
330
338
  b_marker_pane = peerB.locator(".leaflet-marker-pane > div")
@@ -335,7 +343,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
335
343
 
336
344
  # Verify marker properties
337
345
  peerB.locator(".leaflet-marker-icon").first.click()
338
- peerB.get_by_role("link", name="Toggle edit mode (⇧+Click)").click()
346
+ peerB.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
339
347
  expect(peerB.locator('input[name="name"]')).to_have_value("First marker")
340
348
 
341
349
  # Verify polygon exists (we've already checked the count)
@@ -349,7 +357,7 @@ def test_websocket_connection_can_sync_late_joining_peer(
349
357
 
350
358
 
351
359
  @pytest.mark.xdist_group(name="websockets")
352
- def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelayer):
360
+ def test_should_sync_datalayers(new_page, asgi_live_server, tilelayer):
353
361
  map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
354
362
  map.settings["properties"]["syncEnabled"] = True
355
363
  map.save()
@@ -358,12 +366,12 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
358
366
 
359
367
  # Create two tabs
360
368
  peerA = new_page("Page A")
361
- peerA.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
369
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
362
370
  peerB = new_page("Page B")
363
- peerB.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
371
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
364
372
 
365
373
  # Create a new layer from peerA
366
- peerA.get_by_role("link", name="Manage layers").click()
374
+ peerA.get_by_role("button", name="Manage layers").click()
367
375
  peerA.get_by_role("button", name="Add a layer").click()
368
376
 
369
377
  # Check layer has been sync to peerB
@@ -371,7 +379,7 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
371
379
  expect(peerB.get_by_text("Layer 1")).to_be_visible()
372
380
 
373
381
  # Draw a marker in layer 1 from peerA
374
- peerA.get_by_role("link", name="Draw a marker (Ctrl+M)").click()
382
+ peerA.get_by_role("button", name="Draw a marker (Ctrl+M)").click()
375
383
  peerA.locator("#map").click()
376
384
 
377
385
  # Check marker is visible from peerB
@@ -384,20 +392,22 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
384
392
  assert DataLayer.objects.count() == 1
385
393
 
386
394
  # Create another layer from peerA and draw a marker on it (without saving to server)
387
- peerA.get_by_role("link", name="Manage layers").click()
395
+ peerA.get_by_role("button", name="Manage layers").click()
388
396
  peerA.get_by_role("button", name="Add a layer").click()
389
- peerA.get_by_role("link", name="Draw a marker (Ctrl+M)").click()
397
+ peerA.get_by_role("button", name="Draw a marker (Ctrl+M)").click()
390
398
  peerA.locator("#map").click()
391
399
 
392
400
  # Make sure this new marker is in Layer 2 for peerB
393
- expect(peerB.get_by_text("Layer 2")).to_be_visible()
401
+ # Show features for this layer in the brower.
402
+ peerB.get_by_role("heading", name="Layer 2").locator(".datalayer-name").click()
403
+ expect(peerB.locator("li").filter(has_text="Layer 2")).to_be_visible()
394
404
  peerB.locator(".panel.left").get_by_role("button", name="Show/hide layer").nth(
395
405
  1
396
406
  ).click()
397
407
  expect(peerB.locator(".leaflet-marker-icon")).to_be_visible()
398
408
 
399
409
  # Now draw a marker from peerB
400
- peerB.get_by_role("link", name="Draw a marker (Ctrl+M)").click()
410
+ peerB.get_by_role("button", name="Draw a marker (Ctrl+M)").click()
401
411
  peerB.locator("#map").click()
402
412
  peerB.locator('input[name="name"]').fill("marker from peerB")
403
413
 
@@ -413,26 +423,93 @@ def test_should_sync_datalayers(new_page, live_server, websocket_server, tilelay
413
423
  1
414
424
  ).click()
415
425
 
416
- # Now peerA saves the layer 2 to the server
417
- with peerA.expect_response(re.compile(".*/datalayer/update/.*")):
418
- peerA.get_by_role("button", name="Save").click()
426
+ # Peer A should not be in dirty state
427
+ expect(peerA.locator("body")).not_to_have_class(re.compile(".*umap-is-dirty.*"))
428
+
429
+ # Peer A should only have two markers
430
+ expect(peerA.locator(".leaflet-marker-icon")).to_have_count(2)
419
431
 
420
432
  assert DataLayer.objects.count() == 2
421
433
 
422
434
 
423
435
  @pytest.mark.xdist_group(name="websockets")
424
- def test_create_and_sync_map(
425
- new_page, live_server, websocket_server, tilelayer, login, user
426
- ):
436
+ def test_should_sync_datalayers_delete(new_page, asgi_live_server, tilelayer):
437
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
438
+ map.settings["properties"]["syncEnabled"] = True
439
+ map.save()
440
+ data1 = {
441
+ "type": "FeatureCollection",
442
+ "features": [
443
+ {
444
+ "type": "Feature",
445
+ "properties": {
446
+ "name": "Point 1",
447
+ },
448
+ "geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
449
+ },
450
+ ],
451
+ "_umap_options": {
452
+ "name": "datalayer 1",
453
+ },
454
+ }
455
+ data2 = {
456
+ "type": "FeatureCollection",
457
+ "features": [
458
+ {
459
+ "type": "Feature",
460
+ "properties": {
461
+ "name": "Point 2",
462
+ },
463
+ "geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
464
+ },
465
+ ],
466
+ "_umap_options": {
467
+ "name": "datalayer 2",
468
+ },
469
+ }
470
+ DataLayerFactory(map=map, data=data1)
471
+ DataLayerFactory(map=map, data=data2)
472
+
473
+ # Create two tabs
474
+ peerA = new_page("Page A")
475
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
476
+ peerB = new_page("Page B")
477
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
478
+
479
+ 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()
482
+ 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()
485
+
486
+ # Delete "datalayer 2" in peerA
487
+ 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()
491
+
492
+ # Save delete to the server
493
+ with peerA.expect_response(re.compile(".*/datalayer/delete/.*")):
494
+ 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()
497
+
498
+
499
+ @pytest.mark.xdist_group(name="websockets")
500
+ def test_create_and_sync_map(new_page, asgi_live_server, tilelayer, login, user):
427
501
  # Create a syncable map with peerA
428
502
  peerA = login(user, prefix="Page A")
429
- peerA.goto(f"{live_server.url}/en/map/new/")
503
+ peerA.goto(f"{asgi_live_server.url}/en/map/new/")
504
+ peerA.get_by_role("button", name="Map advanced properties").click()
505
+ expect(peerA.get_by_text("Real-time collaboration", exact=True)).to_be_hidden()
430
506
  with peerA.expect_response(re.compile("./map/create/.*")):
431
507
  peerA.get_by_role("button", name="Save Draft").click()
432
- peerA.get_by_role("link", name="Map advanced properties").click()
508
+ peerA.get_by_role("button", name="Map advanced properties").click()
509
+ expect(peerA.get_by_text("Real-time collaboration", exact=True)).to_be_visible()
433
510
  peerA.get_by_text("Real-time collaboration", exact=True).click()
434
511
  peerA.get_by_text("Enable real-time").click()
435
- peerA.get_by_role("link", name="Update permissions and editors").click()
512
+ peerA.get_by_role("button", name="Update permissions and editors").click()
436
513
  peerA.locator('select[name="share_status"]').select_option(str(Map.PUBLIC))
437
514
  with peerA.expect_response(re.compile("./update/settings/.*")):
438
515
  peerA.get_by_role("button", name="Save").click()
@@ -458,6 +535,11 @@ def test_create_and_sync_map(
458
535
  expect(markersA).to_have_count(1)
459
536
  expect(markersB).to_have_count(1)
460
537
 
538
+ # Make sure only one layer has been created on peer B
539
+ peerB.get_by_role("button", name="Open browser").click()
540
+ expect(peerB.locator("h5").get_by_text("Layer 1")).to_be_visible()
541
+ peerB.get_by_role("button", name="Close").click()
542
+
461
543
  # Save and quit edit mode again
462
544
  with peerA.expect_response(re.compile("./datalayer/create/.*")):
463
545
  peerA.get_by_role("button", name="Save").click()
@@ -484,3 +566,96 @@ def test_create_and_sync_map(
484
566
  peerA.get_by_role("button", name="Edit").click()
485
567
  expect(markersA).to_have_count(2)
486
568
  expect(markersB).to_have_count(2)
569
+
570
+
571
+ @pytest.mark.xdist_group(name="websockets")
572
+ def test_saved_datalayer_are_not_duplicated(new_page, asgi_live_server, tilelayer):
573
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
574
+ map.settings["properties"]["syncEnabled"] = True
575
+ map.save()
576
+
577
+ # Create one tab
578
+ peerA = new_page("Page A")
579
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
580
+ # Create a new datalayer
581
+ peerA.get_by_title("Manage layers").click()
582
+ peerA.get_by_title("Add a layer").click()
583
+ peerA.locator("#map").click(position={"x": 220, "y": 220})
584
+ # Save layer to the server, so now the datalayer exist on the server AND
585
+ # is still in the live operations of peer A
586
+ with peerA.expect_response(re.compile(".*/datalayer/create/.*")):
587
+ peerA.get_by_role("button", name="Save").click()
588
+
589
+ # Now load the map from another tab
590
+ peerB = new_page("Page B")
591
+ peerB.goto(peerA.url)
592
+ peerB.get_by_role("button", name="Open browser").click()
593
+ expect(peerB.get_by_text("Layer 1")).to_be_visible()
594
+ peerB.get_by_role("button", name="Edit").click()
595
+ peerA.wait_for_timeout(300) # Let the synchro roll on.
596
+ expect(peerB.get_by_text("Layer 1")).to_be_visible()
597
+
598
+
599
+ @pytest.mark.xdist_group(name="websockets")
600
+ def test_should_sync_saved_status(new_page, asgi_live_server, tilelayer):
601
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
602
+ map.settings["properties"]["syncEnabled"] = True
603
+ map.save()
604
+
605
+ # Create two tabs
606
+ peerA = new_page("Page A")
607
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
608
+ peerB = new_page("Page B")
609
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
610
+
611
+ # Create a new marker from peerA
612
+ peerA.get_by_title("Draw a marker").click()
613
+ peerA.locator("#map").click(position={"x": 220, "y": 220})
614
+
615
+ # Peer A should be in dirty state
616
+ expect(peerA.locator("body")).to_have_class(re.compile(".*umap-is-dirty.*"))
617
+
618
+ # Peer B should not be in dirty state
619
+ expect(peerB.locator("body")).not_to_have_class(re.compile(".*umap-is-dirty.*"))
620
+
621
+ # Create a new marker from peerB
622
+ peerB.get_by_title("Draw a marker").click()
623
+ peerB.locator("#map").click(position={"x": 200, "y": 250})
624
+
625
+ # Peer B should be in dirty state
626
+ expect(peerB.locator("body")).to_have_class(re.compile(".*umap-is-dirty.*"))
627
+
628
+ # Peer A should still be in dirty state
629
+ expect(peerA.locator("body")).to_have_class(re.compile(".*umap-is-dirty.*"))
630
+
631
+ # Save layer to the server from peerA
632
+ with peerA.expect_response(re.compile(".*/datalayer/create/.*")):
633
+ peerA.get_by_role("button", name="Save").click()
634
+
635
+ # Peer B should not be in dirty state
636
+ expect(peerB.locator("body")).not_to_have_class(re.compile(".*umap-is-dirty.*"))
637
+
638
+ # Peer A should not be in dirty state
639
+ expect(peerA.locator("body")).not_to_have_class(re.compile(".*umap-is-dirty.*"))
640
+
641
+
642
+ @pytest.mark.xdist_group(name="websockets")
643
+ def test_should_sync_line_on_escape(new_page, asgi_live_server, tilelayer):
644
+ map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
645
+ map.settings["properties"]["syncEnabled"] = True
646
+ map.save()
647
+
648
+ # Create two tabs
649
+ peerA = new_page("Page A")
650
+ peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
651
+ peerB = new_page("Page B")
652
+ peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
653
+
654
+ # Create a new marker from peerA
655
+ peerA.get_by_title("Draw a polyline").click()
656
+ peerA.locator("#map").click(position={"x": 220, "y": 220})
657
+ peerA.locator("#map").click(position={"x": 200, "y": 200})
658
+ peerA.locator("body").press("Escape")
659
+
660
+ expect(peerA.locator("path")).to_have_count(1)
661
+ expect(peerB.locator("path")).to_have_count(1)
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)