umap-project 2.8.2__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.
- umap/__init__.py +1 -1
- umap/asgi.py +12 -7
- umap/context_processors.py +1 -0
- umap/locale/en/LC_MESSAGES/django.po +102 -59
- umap/locale/fr/LC_MESSAGES/django.mo +0 -0
- umap/locale/fr/LC_MESSAGES/django.po +105 -61
- umap/management/commands/empty_trash.py +12 -1
- umap/migrations/0026_datalayer_modified_at_datalayer_share_status.py +26 -0
- umap/models.py +23 -3
- umap/settings/base.py +4 -1
- umap/static/umap/base.css +1 -1
- umap/static/umap/content.css +2 -22
- umap/static/umap/css/bar.css +7 -10
- umap/static/umap/css/form.css +28 -29
- umap/static/umap/css/icon.css +8 -2
- umap/static/umap/css/panel.css +2 -1
- umap/static/umap/css/tooltip.css +33 -31
- umap/static/umap/img/16-white.svg +2 -0
- umap/static/umap/img/16.svg +1 -1
- umap/static/umap/img/providers/bitbucket.png +0 -0
- umap/static/umap/img/providers/github.png +0 -0
- umap/static/umap/img/providers/keycloak.png +0 -0
- umap/static/umap/img/providers/openstreetmap-oauth2.png +0 -0
- umap/static/umap/img/providers/twitter-oauth2.png +0 -0
- umap/static/umap/img/source/16-white.svg +3 -1
- umap/static/umap/img/source/16.svg +1 -1
- umap/static/umap/js/components/alerts/alert.js +4 -1
- umap/static/umap/js/modules/browser.js +6 -6
- umap/static/umap/js/modules/caption.js +30 -7
- umap/static/umap/js/modules/data/features.js +21 -24
- umap/static/umap/js/modules/data/layer.js +71 -33
- umap/static/umap/js/modules/form/builder.js +241 -0
- umap/static/umap/js/modules/form/fields.js +1338 -0
- umap/static/umap/js/modules/formatter.js +5 -8
- umap/static/umap/js/modules/help.js +3 -1
- umap/static/umap/js/modules/importer.js +1 -1
- umap/static/umap/js/modules/permissions.js +5 -4
- umap/static/umap/js/modules/rendering/icon.js +5 -1
- umap/static/umap/js/modules/rendering/layers/classified.js +11 -7
- umap/static/umap/js/modules/rendering/layers/cluster.js +11 -1
- umap/static/umap/js/modules/rendering/map.js +0 -2
- umap/static/umap/js/modules/rules.js +2 -1
- umap/static/umap/js/modules/schema.js +5 -6
- umap/static/umap/js/modules/share.js +3 -3
- umap/static/umap/js/modules/sync/engine.js +18 -13
- umap/static/umap/js/modules/sync/updaters.js +8 -0
- umap/static/umap/js/modules/sync/websocket.js +10 -5
- umap/static/umap/js/modules/tableeditor.js +3 -2
- umap/static/umap/js/modules/ui/bar.js +17 -9
- umap/static/umap/js/modules/ui/base.js +7 -24
- umap/static/umap/js/modules/ui/tooltip.js +19 -11
- umap/static/umap/js/modules/umap.js +36 -24
- umap/static/umap/js/modules/utils.js +196 -12
- umap/static/umap/js/umap.controls.js +0 -12
- umap/static/umap/locale/br.js +21 -13
- umap/static/umap/locale/br.json +21 -13
- umap/static/umap/locale/ca.js +12 -4
- umap/static/umap/locale/ca.json +12 -4
- umap/static/umap/locale/cs_CZ.js +10 -4
- umap/static/umap/locale/cs_CZ.json +10 -4
- umap/static/umap/locale/de.js +12 -4
- umap/static/umap/locale/de.json +12 -4
- umap/static/umap/locale/el.js +12 -4
- umap/static/umap/locale/el.json +12 -4
- umap/static/umap/locale/en.js +9 -4
- umap/static/umap/locale/en.json +9 -4
- umap/static/umap/locale/es.js +20 -12
- umap/static/umap/locale/es.json +20 -12
- umap/static/umap/locale/eu.js +12 -4
- umap/static/umap/locale/eu.json +12 -4
- umap/static/umap/locale/fa_IR.js +12 -4
- umap/static/umap/locale/fa_IR.json +12 -4
- umap/static/umap/locale/fr.js +10 -5
- umap/static/umap/locale/fr.json +10 -5
- umap/static/umap/locale/gl.js +353 -345
- umap/static/umap/locale/gl.json +353 -345
- umap/static/umap/locale/hu.js +9 -4
- umap/static/umap/locale/hu.json +9 -4
- umap/static/umap/locale/it.js +100 -92
- umap/static/umap/locale/it.json +100 -92
- umap/static/umap/locale/ms.js +12 -4
- umap/static/umap/locale/ms.json +12 -4
- umap/static/umap/locale/nl.js +12 -4
- umap/static/umap/locale/nl.json +12 -4
- umap/static/umap/locale/pl.js +12 -4
- umap/static/umap/locale/pl.json +12 -4
- umap/static/umap/locale/pt.js +12 -4
- umap/static/umap/locale/pt.json +12 -4
- umap/static/umap/locale/pt_PT.js +12 -4
- umap/static/umap/locale/pt_PT.json +12 -4
- umap/static/umap/locale/th_TH.js +12 -4
- umap/static/umap/locale/th_TH.json +12 -4
- umap/static/umap/locale/zh_TW.js +10 -4
- umap/static/umap/locale/zh_TW.json +10 -4
- umap/static/umap/map.css +12 -8
- umap/static/umap/nav.css +2 -3
- umap/static/umap/unittests/utils.js +14 -0
- umap/static/umap/vars.css +2 -0
- umap/sync/__init__.py +0 -0
- umap/sync/app.py +181 -0
- umap/sync/payloads.py +49 -0
- umap/templates/auth/user_detail.html +4 -0
- umap/templates/auth/user_form.html +9 -6
- umap/templates/auth/user_stars.html +4 -0
- umap/templates/base.html +1 -1
- umap/templates/registration/login.html +2 -5
- umap/templates/umap/about.html +5 -0
- umap/templates/umap/about_summary.html +2 -2
- umap/templates/umap/components/provider.html +8 -0
- umap/templates/umap/js.html +0 -3
- umap/templates/umap/map_detail.html +1 -1
- umap/templates/umap/password_change.html +4 -0
- umap/templates/umap/password_change_done.html +4 -0
- umap/templates/umap/search.html +4 -0
- umap/templates/umap/team_confirm_delete.html +4 -0
- umap/templates/umap/team_detail.html +4 -0
- umap/templates/umap/team_form.html +4 -0
- umap/templates/umap/user_dashboard.html +1 -1
- umap/templates/umap/user_teams.html +4 -0
- umap/tests/base.py +3 -1
- umap/tests/integration/conftest.py +16 -23
- umap/tests/integration/test_basics.py +2 -2
- umap/tests/integration/test_caption.py +1 -0
- umap/tests/integration/test_draw_polygon.py +3 -3
- umap/tests/integration/test_edit_datalayer.py +1 -1
- umap/tests/integration/test_edit_map.py +3 -3
- umap/tests/integration/test_edit_polygon.py +1 -1
- umap/tests/integration/test_import.py +23 -1
- umap/tests/integration/test_optimistic_merge.py +1 -0
- umap/tests/integration/test_picto.py +8 -8
- umap/tests/integration/test_save.py +1 -0
- umap/tests/integration/test_star.py +13 -9
- umap/tests/integration/test_tableeditor.py +1 -0
- umap/tests/integration/test_websocket_sync.py +112 -33
- umap/tests/settings.py +2 -0
- umap/tests/test_datalayer.py +2 -3
- umap/tests/test_datalayer_views.py +20 -1
- umap/tests/test_empty_trash.py +10 -3
- umap/tests/test_map_views.py +11 -0
- umap/utils.py +24 -11
- umap/views.py +37 -6
- {umap_project-2.8.2.dist-info → umap_project-2.9.0b0.dist-info}/METADATA +15 -15
- {umap_project-2.8.2.dist-info → umap_project-2.9.0b0.dist-info}/RECORD +146 -145
- {umap_project-2.8.2.dist-info → umap_project-2.9.0b0.dist-info}/WHEEL +1 -1
- umap/management/commands/run_websocket_server.py +0 -23
- umap/settings/local_s3.py +0 -45
- umap/static/umap/bitbucket.png +0 -0
- umap/static/umap/github.png +0 -0
- umap/static/umap/js/umap.forms.js +0 -1242
- umap/static/umap/keycloak.png +0 -0
- umap/static/umap/openstreetmap.png +0 -0
- umap/static/umap/twitter.png +0 -0
- umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +0 -468
- umap/tests/test_websocket_server.py +0 -22
- umap/websocket_server.py +0 -202
- {umap_project-2.8.2.dist-info → umap_project-2.9.0b0.dist-info}/entry_points.txt +0 -0
- {umap_project-2.8.2.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"{
|
|
36
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
25
37
|
peerB = new_page("Page B")
|
|
26
|
-
peerB.goto(f"{
|
|
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"{
|
|
103
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
93
104
|
peerB = context.new_page()
|
|
94
|
-
peerB.goto(f"{
|
|
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,
|
|
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"{
|
|
187
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
177
188
|
peerB = new_page()
|
|
178
|
-
peerB.goto(f"{
|
|
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(
|
|
192
|
-
)
|
|
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,
|
|
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"{
|
|
221
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
209
222
|
peerB = new_page()
|
|
210
|
-
peerB.goto(f"{
|
|
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,
|
|
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"{
|
|
250
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
238
251
|
peerB = context.new_page()
|
|
239
|
-
peerB.goto(f"{
|
|
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(".
|
|
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,
|
|
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"{
|
|
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"{
|
|
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,
|
|
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"{
|
|
375
|
+
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
|
362
376
|
peerB = new_page("Page B")
|
|
363
|
-
peerB.goto(f"{
|
|
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
|
|
425
|
-
|
|
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"{
|
|
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
umap/tests/test_datalayer.py
CHANGED
|
@@ -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 ==
|
|
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
|
|
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
|
umap/tests/test_empty_trash.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
umap/tests/test_map_views.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: umap-project
|
|
3
|
-
Version: 2.
|
|
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.
|
|
20
|
+
Requires-Dist: django-environ==0.12.0
|
|
20
21
|
Requires-Dist: django-probes==1.7.0
|
|
21
|
-
Requires-Dist: django==5.1.
|
|
22
|
-
Requires-Dist: pillow==11.
|
|
23
|
-
Requires-Dist: psycopg==3.2.
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
-
Requires-Dist: ruff==0.
|
|
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:
|
|
45
|
-
Requires-Dist:
|
|
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.
|
|
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'
|