umap-project 3.0.5__py3-none-any.whl → 3.1.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.
- umap/__init__.py +1 -1
- umap/forms.py +1 -1
- umap/locale/br/LC_MESSAGES/django.mo +0 -0
- umap/locale/br/LC_MESSAGES/django.po +219 -72
- umap/locale/ca/LC_MESSAGES/django.mo +0 -0
- umap/locale/ca/LC_MESSAGES/django.po +286 -95
- umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- umap/locale/cs_CZ/LC_MESSAGES/django.po +211 -65
- umap/locale/da/LC_MESSAGES/django.mo +0 -0
- umap/locale/da/LC_MESSAGES/django.po +394 -202
- umap/locale/de/LC_MESSAGES/django.mo +0 -0
- umap/locale/de/LC_MESSAGES/django.po +146 -75
- umap/locale/el/LC_MESSAGES/django.mo +0 -0
- umap/locale/el/LC_MESSAGES/django.po +125 -59
- umap/locale/en/LC_MESSAGES/django.mo +0 -0
- umap/locale/en/LC_MESSAGES/django.po +124 -58
- umap/locale/es/LC_MESSAGES/django.mo +0 -0
- umap/locale/es/LC_MESSAGES/django.po +125 -59
- umap/locale/et/LC_MESSAGES/django.mo +0 -0
- umap/locale/et/LC_MESSAGES/django.po +210 -64
- umap/locale/eu/LC_MESSAGES/django.mo +0 -0
- umap/locale/eu/LC_MESSAGES/django.po +212 -65
- umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
- umap/locale/fa_IR/LC_MESSAGES/django.po +286 -95
- umap/locale/fr/LC_MESSAGES/django.mo +0 -0
- umap/locale/fr/LC_MESSAGES/django.po +125 -59
- umap/locale/gl/LC_MESSAGES/django.mo +0 -0
- umap/locale/gl/LC_MESSAGES/django.po +212 -66
- umap/locale/hu/LC_MESSAGES/django.mo +0 -0
- umap/locale/hu/LC_MESSAGES/django.po +148 -78
- umap/locale/is/LC_MESSAGES/django.mo +0 -0
- umap/locale/is/LC_MESSAGES/django.po +130 -60
- umap/locale/it/LC_MESSAGES/django.mo +0 -0
- umap/locale/it/LC_MESSAGES/django.po +219 -73
- umap/locale/ms/LC_MESSAGES/django.mo +0 -0
- umap/locale/ms/LC_MESSAGES/django.po +289 -98
- umap/locale/nl/LC_MESSAGES/django.mo +0 -0
- umap/locale/nl/LC_MESSAGES/django.po +128 -61
- umap/locale/pl/LC_MESSAGES/django.mo +0 -0
- umap/locale/pl/LC_MESSAGES/django.po +287 -96
- umap/locale/pt/LC_MESSAGES/django.mo +0 -0
- umap/locale/pt/LC_MESSAGES/django.po +211 -65
- umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
- umap/locale/zh_TW/LC_MESSAGES/django.po +212 -66
- umap/management/commands/migrate_to_S3.py +42 -20
- umap/management/commands/purge_old_versions.py +63 -0
- umap/management/commands/switch_user.py +52 -0
- umap/managers.py +29 -2
- umap/middleware.py +1 -1
- umap/migrations/0028_map_is_template.py +21 -0
- umap/models.py +14 -4
- umap/settings/base.py +22 -0
- umap/static/umap/base.css +4 -2
- umap/static/umap/content.css +1 -1
- umap/static/umap/css/dialog.css +5 -2
- umap/static/umap/css/form.css +19 -12
- umap/static/umap/css/icon.css +6 -0
- umap/static/umap/css/importers.css +4 -0
- umap/static/umap/css/panel.css +2 -0
- umap/static/umap/img/16-white.svg +5 -1
- umap/static/umap/img/16.svg +1 -1
- umap/static/umap/img/24-white.svg +3 -2
- umap/static/umap/img/24.svg +3 -4
- umap/static/umap/img/importers/opendata.svg +1 -0
- umap/static/umap/img/source/16-white.svg +8 -4
- umap/static/umap/img/source/16.svg +1 -1
- umap/static/umap/img/source/24-white.svg +5 -4
- umap/static/umap/img/source/24.svg +5 -6
- umap/static/umap/js/components/modal.js +27 -0
- umap/static/umap/js/modules/caption.js +4 -4
- umap/static/umap/js/modules/data/features.js +40 -4
- umap/static/umap/js/modules/data/layer.js +208 -138
- umap/static/umap/js/modules/form/builder.js +6 -14
- umap/static/umap/js/modules/form/fields.js +2 -2
- umap/static/umap/js/modules/help.js +15 -3
- umap/static/umap/js/modules/importer.js +7 -4
- umap/static/umap/js/modules/importers/opendata.js +142 -0
- umap/static/umap/js/modules/permissions.js +3 -3
- umap/static/umap/js/modules/rendering/controls.js +34 -2
- umap/static/umap/js/modules/rendering/icon.js +2 -2
- umap/static/umap/js/modules/rendering/layers/base.js +1 -1
- umap/static/umap/js/modules/rendering/layers/classified.js +55 -49
- umap/static/umap/js/modules/rendering/layers/cluster.js +16 -9
- umap/static/umap/js/modules/rendering/layers/heat.js +13 -11
- umap/static/umap/js/modules/rendering/map.js +5 -0
- umap/static/umap/js/modules/rendering/ui.js +23 -0
- umap/static/umap/js/modules/rules.js +24 -23
- umap/static/umap/js/modules/schema.js +60 -4
- umap/static/umap/js/modules/sync/updaters.js +7 -3
- umap/static/umap/js/modules/tableeditor.js +7 -30
- umap/static/umap/js/modules/templates.js +122 -0
- umap/static/umap/js/modules/ui/bar.js +13 -3
- umap/static/umap/js/modules/ui/panel.js +1 -1
- umap/static/umap/js/modules/umap.js +28 -13
- umap/static/umap/js/umap.controls.js +11 -4
- umap/static/umap/locale/br.js +51 -18
- umap/static/umap/locale/br.json +51 -18
- umap/static/umap/locale/da.js +343 -310
- umap/static/umap/locale/da.json +343 -310
- umap/static/umap/locale/de.js +40 -7
- umap/static/umap/locale/de.json +40 -7
- umap/static/umap/locale/el.js +31 -4
- umap/static/umap/locale/el.json +31 -4
- umap/static/umap/locale/en.js +34 -1
- umap/static/umap/locale/en.json +34 -1
- umap/static/umap/locale/es.js +37 -4
- umap/static/umap/locale/es.json +37 -4
- umap/static/umap/locale/fr.js +34 -1
- umap/static/umap/locale/fr.json +34 -1
- umap/static/umap/locale/hu.js +44 -17
- umap/static/umap/locale/hu.json +44 -17
- umap/static/umap/locale/it.js +74 -41
- umap/static/umap/locale/it.json +74 -41
- umap/static/umap/locale/nl.js +42 -9
- umap/static/umap/locale/nl.json +42 -9
- umap/static/umap/map.css +3 -23
- umap/static/umap/vendors/textpath/leaflet.textpath.js +184 -0
- umap/storage/fs.py +19 -9
- umap/templates/umap/dashboard_menu.html +5 -0
- umap/templates/umap/design_system.html +9 -0
- umap/templates/umap/js.html +3 -0
- umap/templates/umap/map_init.html +3 -1
- umap/templates/umap/map_list.html +2 -2
- umap/templates/umap/map_table.html +18 -18
- umap/templates/umap/user_dashboard.html +9 -58
- umap/templates/umap/user_map_table.html +36 -0
- umap/templates/umap/user_templates.html +19 -0
- umap/templatetags/umap_tags.py +5 -0
- umap/tests/integration/test_basics.py +9 -1
- umap/tests/integration/test_conditional_rules.py +57 -0
- umap/tests/integration/test_datalayer.py +16 -9
- umap/tests/integration/test_edit_marker.py +11 -0
- umap/tests/integration/test_tableeditor.py +42 -7
- umap/tests/integration/test_templates.py +44 -0
- umap/tests/test_dashboard.py +19 -0
- umap/tests/test_purge_old_versions.py +91 -0
- umap/tests/test_switch_user.py +31 -0
- umap/tests/test_views.py +67 -0
- umap/urls.py +7 -1
- umap/views.py +65 -19
- {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/METADATA +15 -15
- {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/RECORD +145 -132
- {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/WHEEL +0 -0
- {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/entry_points.txt +0 -0
- {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -54,22 +54,29 @@ def test_should_honour_fromZoom(live_server, map, datalayer, page):
|
|
|
54
54
|
expect(markers).to_be_visible()
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def test_should_honour_toZoom(live_server, map, datalayer, page):
|
|
57
|
+
def test_should_honour_toZoom(live_server, map, datalayer, page, new_page):
|
|
58
58
|
set_options(datalayer, displayOnLoad=True, toZoom=6)
|
|
59
|
+
# Loading at zoom 7 should not show the marker
|
|
59
60
|
page.goto(f"{live_server.url}{map.get_absolute_url()}#7/48.55/14.68")
|
|
60
61
|
markers = page.locator(".leaflet-marker-icon")
|
|
61
62
|
expect(markers).to_be_hidden()
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
# Loading at zoom 6 should show the marker
|
|
65
|
+
page2 = new_page()
|
|
66
|
+
markers = page2.locator(".leaflet-marker-icon")
|
|
67
|
+
page2.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.55/14.68")
|
|
68
|
+
expect(page2).to_have_url(re.compile(r".*#6/48\..+/14\..+"))
|
|
64
69
|
expect(markers).to_be_visible()
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
|
|
71
|
+
# Now try to unzoom/rezoom and check that markers show/hide accordingly.
|
|
72
|
+
page2.get_by_label("Zoom out").click()
|
|
73
|
+
expect(page2).to_have_url(re.compile(r".*#5/48\..+/14\..+"))
|
|
67
74
|
expect(markers).to_be_visible()
|
|
68
|
-
|
|
69
|
-
expect(
|
|
75
|
+
page2.get_by_label("Zoom in").click()
|
|
76
|
+
expect(page2).to_have_url(re.compile(r".*#6/48\..+/14\..+"))
|
|
70
77
|
expect(markers).to_be_visible()
|
|
71
|
-
|
|
72
|
-
expect(
|
|
78
|
+
page2.get_by_label("Zoom in").click()
|
|
79
|
+
expect(page2).to_have_url(re.compile(r".*#7/48\..+/14\..+"))
|
|
73
80
|
expect(markers).to_be_hidden()
|
|
74
81
|
|
|
75
82
|
|
|
@@ -118,3 +118,14 @@ def test_should_follow_datalayer_style_when_changing_datalayer(
|
|
|
118
118
|
page.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
|
|
119
119
|
page.locator(".umap-field-datalayer select").select_option(label="other datalayer")
|
|
120
120
|
expect(marker).to_have_css("background-color", "rgb(148, 0, 211)")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_add_property_from_feature_properties_panel(
|
|
124
|
+
live_server, openmap, page, datalayer
|
|
125
|
+
):
|
|
126
|
+
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
|
127
|
+
page.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
|
|
128
|
+
page.get_by_role("button", name="Add a new property").click()
|
|
129
|
+
page.locator('input[name="prompt"]').fill("newprop")
|
|
130
|
+
page.get_by_role("button", name="OK").click()
|
|
131
|
+
expect(page.locator(".panel.right").get_by_text("newprop")).to_be_visible()
|
|
@@ -113,17 +113,52 @@ def test_cannot_add_property_with_a_dot(live_server, openmap, datalayer, page):
|
|
|
113
113
|
expect(page.locator("table th button[data-property=name]")).to_have_count(1)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def test_rename_property(live_server, openmap,
|
|
117
|
-
|
|
116
|
+
def test_rename_property(live_server, openmap, page):
|
|
117
|
+
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
|
|
118
|
+
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.093/1.890")
|
|
118
119
|
page.get_by_role("button", name="Manage layers").click()
|
|
119
120
|
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
|
120
|
-
expect(page.locator("table th button[data-property=
|
|
121
|
-
page.locator("thead button[data-property=
|
|
121
|
+
expect(page.locator("table th button[data-property=mytype]")).to_have_count(1)
|
|
122
|
+
page.locator("thead button[data-property=mytype]").click()
|
|
122
123
|
page.get_by_text("Rename this column").click()
|
|
123
|
-
page.locator("dialog").locator("input").fill("
|
|
124
|
+
page.locator("dialog").locator("input").fill("mynewtype")
|
|
125
|
+
page.get_by_role("button", name="OK").click()
|
|
126
|
+
expect(page.locator("table th button[data-property=mynewtype]")).to_have_count(1)
|
|
127
|
+
expect(page.locator("table th button[data-property=mytype]")).to_have_count(0)
|
|
128
|
+
|
|
129
|
+
page.locator(".panel.full").get_by_role("button", name="Close").click()
|
|
130
|
+
page.locator(".leaflet-marker-icon").first.click()
|
|
131
|
+
page.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
|
|
132
|
+
expect(page.locator(".panel.right .umap-field-mynewtype")).to_be_visible()
|
|
133
|
+
expect(page.locator(".panel.right .umap-field-mytype")).to_be_hidden()
|
|
134
|
+
page.locator(".edit-undo").click()
|
|
135
|
+
page.locator(".panel.right").get_by_role("button", name="Close").click()
|
|
136
|
+
page.locator(".leaflet-marker-icon").first.click()
|
|
137
|
+
page.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
|
|
138
|
+
expect(page.locator(".panel.right .umap-field-mynewtype")).to_be_hidden()
|
|
139
|
+
expect(page.locator(".panel.right .umap-field-mytype")).to_be_visible()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_delete_property(live_server, openmap, page):
|
|
143
|
+
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
|
|
144
|
+
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.093/1.890")
|
|
145
|
+
page.get_by_role("button", name="Manage layers").click()
|
|
146
|
+
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
|
147
|
+
expect(page.locator("table th button[data-property=mytype]")).to_have_count(1)
|
|
148
|
+
page.locator("thead button[data-property=mytype]").click()
|
|
149
|
+
page.get_by_text("Delete this column").click()
|
|
124
150
|
page.get_by_role("button", name="OK").click()
|
|
125
|
-
expect(page.locator("table th button[data-property=
|
|
126
|
-
|
|
151
|
+
expect(page.locator("table th button[data-property=mytype]")).to_have_count(0)
|
|
152
|
+
|
|
153
|
+
page.locator(".panel.full").get_by_role("button", name="Close").click()
|
|
154
|
+
page.locator(".leaflet-marker-icon").first.click()
|
|
155
|
+
page.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
|
|
156
|
+
expect(page.locator(".panel.right .umap-field-mytype")).to_be_hidden()
|
|
157
|
+
page.locator(".edit-undo").click()
|
|
158
|
+
page.locator(".panel.right").get_by_role("button", name="Close").click()
|
|
159
|
+
page.locator(".leaflet-marker-icon").first.click()
|
|
160
|
+
page.get_by_role("button", name="Toggle edit mode (⇧+Click)").click()
|
|
161
|
+
expect(page.locator(".panel.right .umap-field-mytype")).to_be_visible()
|
|
127
162
|
|
|
128
163
|
|
|
129
164
|
def test_delete_selected_rows(live_server, openmap, page):
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from playwright.sync_api import expect
|
|
3
|
+
|
|
4
|
+
pytestmark = pytest.mark.django_db
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_reuse_template_button(map, datalayer, page, live_server, context):
|
|
8
|
+
map.is_template = True
|
|
9
|
+
map.save()
|
|
10
|
+
page.goto(f"{live_server.url}{map.get_absolute_url()}")
|
|
11
|
+
expect(page.get_by_text("Reuse this template")).to_be_visible()
|
|
12
|
+
with context.expect_page() as new_page_info:
|
|
13
|
+
page.get_by_text("Reuse this template").click()
|
|
14
|
+
new_page = new_page_info.value
|
|
15
|
+
expect(new_page.get_by_text("Reuse this template")).to_be_hidden()
|
|
16
|
+
expect(page.locator(".leaflet-marker-icon")).to_have_count(1)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_load_template_from_panel(map, datalayer, page, live_server):
|
|
20
|
+
map.name = "My Great Template"
|
|
21
|
+
map.is_template = True
|
|
22
|
+
map.save()
|
|
23
|
+
page.goto(f"{live_server.url}/en/map/new")
|
|
24
|
+
page.get_by_title("Load template").click()
|
|
25
|
+
page.get_by_role("button", name="From community").click()
|
|
26
|
+
page.get_by_label("My Great Template").check()
|
|
27
|
+
page.get_by_role("button", name="Load template with data").click()
|
|
28
|
+
expect(page.get_by_text("My Great Template")).to_be_visible()
|
|
29
|
+
expect(page.locator(".leaflet-marker-icon")).to_have_count(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_load_template_without_data(map, datalayer, page, live_server):
|
|
33
|
+
map.name = "My Great Template"
|
|
34
|
+
map.is_template = True
|
|
35
|
+
map.save()
|
|
36
|
+
page.goto(f"{live_server.url}/en/map/new")
|
|
37
|
+
page.get_by_title("Load template").click()
|
|
38
|
+
page.get_by_role("button", name="From community").click()
|
|
39
|
+
page.get_by_label("My Great Template").check()
|
|
40
|
+
page.locator(".panel").get_by_role(
|
|
41
|
+
"button", name="Load template", exact=True
|
|
42
|
+
).click()
|
|
43
|
+
expect(page.get_by_text("My Great Template")).to_be_visible()
|
|
44
|
+
expect(page.locator(".leaflet-marker-icon")).to_have_count(0)
|
umap/tests/test_dashboard.py
CHANGED
|
@@ -80,3 +80,22 @@ def test_user_dashboard_display_user_maps_distinct(client, map):
|
|
|
80
80
|
body = response.content.decode()
|
|
81
81
|
assert body.count(f'<a href="/en/map/test-map_{map.pk}">test map</a>') == 1
|
|
82
82
|
assert body.count(anonymap.name) == 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_user_dashboard_search(client, map):
|
|
86
|
+
new_map = MapFactory(name="A map about bicycle", owner=map.owner)
|
|
87
|
+
client.login(username=map.owner.username, password="123123")
|
|
88
|
+
response = client.get(f"{reverse('user_dashboard')}?q=bicycle")
|
|
89
|
+
assert response.status_code == 200
|
|
90
|
+
body = response.content.decode()
|
|
91
|
+
assert map.name not in body
|
|
92
|
+
assert new_map.name in body
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_user_dashboard_search_empty(client, map):
|
|
96
|
+
client.login(username=map.owner.username, password="123123")
|
|
97
|
+
response = client.get(f"{reverse('user_dashboard')}?q=car")
|
|
98
|
+
assert response.status_code == 200
|
|
99
|
+
body = response.content.decode()
|
|
100
|
+
assert map.name not in body
|
|
101
|
+
assert "No map found." in body
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest import mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from django.core.files.base import ContentFile
|
|
7
|
+
from django.core.management import call_command
|
|
8
|
+
|
|
9
|
+
from .base import DataLayerFactory, MapFactory
|
|
10
|
+
|
|
11
|
+
pytestmark = pytest.mark.django_db
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_purge_old_versions(map):
|
|
15
|
+
map2 = MapFactory()
|
|
16
|
+
other_layer = DataLayerFactory(map=map2)
|
|
17
|
+
recent_layer = DataLayerFactory(map=map)
|
|
18
|
+
old_layer = DataLayerFactory(map=map)
|
|
19
|
+
older_layer = DataLayerFactory(map=map)
|
|
20
|
+
other_layer.geojson.storage.save(
|
|
21
|
+
Path(other_layer.geojson.storage._base_path(other_layer))
|
|
22
|
+
/ f"{other_layer.uuid}_1440918537.geojson",
|
|
23
|
+
ContentFile("{}"),
|
|
24
|
+
)
|
|
25
|
+
with mock.patch("django.utils.timezone.now") as mocked:
|
|
26
|
+
mocked.return_value = datetime.utcnow() - timedelta(days=7)
|
|
27
|
+
old_layer.save()
|
|
28
|
+
old_layer.geojson.storage.save(
|
|
29
|
+
Path(old_layer.geojson.storage._base_path(old_layer))
|
|
30
|
+
/ f"{old_layer.uuid}_1440918537.geojson",
|
|
31
|
+
ContentFile("{}"),
|
|
32
|
+
)
|
|
33
|
+
old_layer.geojson.storage.save(
|
|
34
|
+
Path(old_layer.geojson.storage._base_path(old_layer))
|
|
35
|
+
/ f"{old_layer.uuid}_1440918537.geojson.gz",
|
|
36
|
+
ContentFile("{}"),
|
|
37
|
+
)
|
|
38
|
+
mocked.return_value = datetime.utcnow() - timedelta(days=12)
|
|
39
|
+
older_layer.save()
|
|
40
|
+
older_layer.geojson.storage.save(
|
|
41
|
+
Path(older_layer.geojson.storage._base_path(older_layer))
|
|
42
|
+
/ f"{older_layer.uuid}_1340918536.geojson",
|
|
43
|
+
ContentFile("{}"),
|
|
44
|
+
)
|
|
45
|
+
older_layer.geojson.storage.save(
|
|
46
|
+
Path(older_layer.geojson.storage._base_path(older_layer))
|
|
47
|
+
/ f"{older_layer.uuid}_1340918536.geojson.gz",
|
|
48
|
+
ContentFile("{}"),
|
|
49
|
+
)
|
|
50
|
+
assert len(recent_layer.versions) == 1
|
|
51
|
+
assert len(old_layer.versions) == 2
|
|
52
|
+
assert len(older_layer.versions) == 2
|
|
53
|
+
assert len(other_layer.versions) == 2
|
|
54
|
+
root = old_layer.geojson.storage._base_path(old_layer)
|
|
55
|
+
# Files including gz for map 1
|
|
56
|
+
# recent layer geojson
|
|
57
|
+
# old layer 2 geojson + 1 gzip
|
|
58
|
+
# older layer 2 geojson + 1 gzip
|
|
59
|
+
assert len(old_layer.geojson.storage.listdir(root)[1]) == 7
|
|
60
|
+
call_command("purge_old_versions", "--days-ago=7", "--dry-run")
|
|
61
|
+
assert len(recent_layer.versions) == 1
|
|
62
|
+
assert len(old_layer.versions) == 2
|
|
63
|
+
assert len(older_layer.versions) == 2
|
|
64
|
+
assert len(other_layer.versions) == 2
|
|
65
|
+
assert len(old_layer.geojson.storage.listdir(root)[1]) == 7
|
|
66
|
+
call_command("purge_old_versions", "--days-ago=9")
|
|
67
|
+
assert len(recent_layer.versions) == 1
|
|
68
|
+
assert len(old_layer.versions) == 2
|
|
69
|
+
assert len(older_layer.versions) == 2
|
|
70
|
+
assert len(other_layer.versions) == 2
|
|
71
|
+
assert len(old_layer.geojson.storage.listdir(root)[1]) == 7
|
|
72
|
+
call_command("purge_old_versions", "--days-ago=7")
|
|
73
|
+
assert len(recent_layer.versions) == 1
|
|
74
|
+
assert len(old_layer.versions) == 1
|
|
75
|
+
assert len(older_layer.versions) == 2
|
|
76
|
+
assert len(other_layer.versions) == 2
|
|
77
|
+
# Files including gz for map 1
|
|
78
|
+
# recent layer geojson
|
|
79
|
+
# old layer 1 geojson
|
|
80
|
+
# older layer 2 geojson + 1 gz
|
|
81
|
+
assert len(old_layer.geojson.storage.listdir(root)[1]) == 5
|
|
82
|
+
call_command("purge_old_versions", "--days-ago=7", "--days-to-select=0")
|
|
83
|
+
assert len(recent_layer.versions) == 1
|
|
84
|
+
assert len(old_layer.versions) == 1
|
|
85
|
+
assert len(older_layer.versions) == 1
|
|
86
|
+
assert len(other_layer.versions) == 2
|
|
87
|
+
# Files including gz for map 1
|
|
88
|
+
# recent layer geojson
|
|
89
|
+
# old layer 1 geojson
|
|
90
|
+
# older layer 1 geojson
|
|
91
|
+
assert len(old_layer.geojson.storage.listdir(root)[1]) == 3
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.core.management import call_command
|
|
3
|
+
|
|
4
|
+
from umap.models import Map, Team
|
|
5
|
+
|
|
6
|
+
from .base import MapFactory
|
|
7
|
+
|
|
8
|
+
pytestmark = pytest.mark.django_db
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_switch_user(user, user2, map, team):
|
|
12
|
+
user_owned = MapFactory(owner=user)
|
|
13
|
+
user2_owned = MapFactory(owner=user2)
|
|
14
|
+
map.editors.add(user)
|
|
15
|
+
map.save()
|
|
16
|
+
team.users.add(user)
|
|
17
|
+
team.save()
|
|
18
|
+
call_command("switch_user", user.username, user2.username, "--dry-run")
|
|
19
|
+
assert Map.objects.get(pk=user_owned.pk).owner == user
|
|
20
|
+
assert Map.objects.get(pk=user2_owned.pk).owner == user2
|
|
21
|
+
assert user in Map.objects.get(pk=map.pk).editors.all()
|
|
22
|
+
assert user2 not in Map.objects.get(pk=map.pk).editors.all()
|
|
23
|
+
assert user in Team.objects.get(pk=team.pk).users.all()
|
|
24
|
+
assert user2 not in Team.objects.get(pk=team.pk).users.all()
|
|
25
|
+
call_command("switch_user", user.username, user2.username)
|
|
26
|
+
assert Map.objects.get(pk=user_owned.pk).owner == user2
|
|
27
|
+
assert Map.objects.get(pk=user2_owned.pk).owner == user2
|
|
28
|
+
assert user not in Map.objects.get(pk=map.pk).editors.all()
|
|
29
|
+
assert user2 in Map.objects.get(pk=map.pk).editors.all()
|
|
30
|
+
assert user not in Team.objects.get(pk=team.pk).users.all()
|
|
31
|
+
assert user2 in Team.objects.get(pk=team.pk).users.all()
|
umap/tests/test_views.py
CHANGED
|
@@ -181,6 +181,7 @@ def test_stats_empty(client):
|
|
|
181
181
|
assert json.loads(response.content.decode()) == {
|
|
182
182
|
"maps_active_last_week_count": 0,
|
|
183
183
|
"maps_count": 0,
|
|
184
|
+
"teams_count": 0,
|
|
184
185
|
"users_active_last_week_count": 0,
|
|
185
186
|
"users_count": 0,
|
|
186
187
|
"active_sessions": 0,
|
|
@@ -189,6 +190,9 @@ def test_stats_empty(client):
|
|
|
189
190
|
"members_count": 0,
|
|
190
191
|
"orphans_count": 0,
|
|
191
192
|
"owners_count": 0,
|
|
193
|
+
"anonymous_allowed": False,
|
|
194
|
+
"realtime_enabled": True,
|
|
195
|
+
"importers": [],
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
|
|
@@ -202,6 +206,7 @@ def test_stats_basic(client, map, datalayer, user2):
|
|
|
202
206
|
assert json.loads(response.content.decode()) == {
|
|
203
207
|
"maps_active_last_week_count": 1,
|
|
204
208
|
"maps_count": 1,
|
|
209
|
+
"teams_count": 0,
|
|
205
210
|
"users_active_last_week_count": 1,
|
|
206
211
|
"users_count": 2,
|
|
207
212
|
"active_sessions": 0,
|
|
@@ -210,6 +215,9 @@ def test_stats_basic(client, map, datalayer, user2):
|
|
|
210
215
|
"members_count": 0,
|
|
211
216
|
"orphans_count": 1,
|
|
212
217
|
"owners_count": 1,
|
|
218
|
+
"anonymous_allowed": False,
|
|
219
|
+
"realtime_enabled": True,
|
|
220
|
+
"importers": [],
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
|
|
@@ -528,3 +536,62 @@ def test_can_find_small_usernames(client):
|
|
|
528
536
|
data = json.loads(response.content)["data"]
|
|
529
537
|
assert len(data) == 1
|
|
530
538
|
assert data[0]["label"] == "JoeJoe"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@pytest.mark.django_db
|
|
542
|
+
def test_templates_list(client, user, user2):
|
|
543
|
+
public = MapFactory(
|
|
544
|
+
owner=user,
|
|
545
|
+
name="A public template",
|
|
546
|
+
share_status=Map.PUBLIC,
|
|
547
|
+
is_template=True,
|
|
548
|
+
)
|
|
549
|
+
link_only = MapFactory(
|
|
550
|
+
owner=user,
|
|
551
|
+
name="A link-only template",
|
|
552
|
+
share_status=Map.OPEN,
|
|
553
|
+
is_template=True,
|
|
554
|
+
)
|
|
555
|
+
private = MapFactory(
|
|
556
|
+
owner=user,
|
|
557
|
+
name="A link-only template",
|
|
558
|
+
share_status=Map.PRIVATE,
|
|
559
|
+
is_template=True,
|
|
560
|
+
)
|
|
561
|
+
someone_else = MapFactory(
|
|
562
|
+
owner=user2,
|
|
563
|
+
name="A public template from someone else",
|
|
564
|
+
share_status=Map.PUBLIC,
|
|
565
|
+
is_template=True,
|
|
566
|
+
)
|
|
567
|
+
staff = UserFactory(username="Staff", is_staff=True)
|
|
568
|
+
Star.objects.create(by=staff, map=someone_else)
|
|
569
|
+
client.login(username=user.username, password="123123")
|
|
570
|
+
url = reverse("template_list")
|
|
571
|
+
|
|
572
|
+
# Ask for mine
|
|
573
|
+
response = client.get(f"{url}?source=mine")
|
|
574
|
+
templates = json.loads(response.content)["templates"]
|
|
575
|
+
ids = [t["id"] for t in templates]
|
|
576
|
+
assert public.pk in ids
|
|
577
|
+
assert link_only.pk in ids
|
|
578
|
+
assert private.pk in ids
|
|
579
|
+
assert someone_else.pk not in ids
|
|
580
|
+
|
|
581
|
+
# Ask for staff ones
|
|
582
|
+
response = client.get(f"{url}?source=staff")
|
|
583
|
+
templates = json.loads(response.content)["templates"]
|
|
584
|
+
ids = [t["id"] for t in templates]
|
|
585
|
+
assert public.pk not in ids
|
|
586
|
+
assert link_only.pk not in ids
|
|
587
|
+
assert private.pk not in ids
|
|
588
|
+
assert someone_else.pk in ids
|
|
589
|
+
|
|
590
|
+
# Ask for community ones
|
|
591
|
+
response = client.get(f"{url}?source=community")
|
|
592
|
+
templates = json.loads(response.content)["templates"]
|
|
593
|
+
ids = [t["id"] for t in templates]
|
|
594
|
+
assert public.pk in ids
|
|
595
|
+
assert link_only.pk not in ids
|
|
596
|
+
assert private.pk not in ids
|
|
597
|
+
assert someone_else.pk in ids
|
umap/urls.py
CHANGED
|
@@ -73,6 +73,11 @@ i18n_urls = [
|
|
|
73
73
|
views.PictogramJSONList.as_view(),
|
|
74
74
|
name="pictogram_list_json",
|
|
75
75
|
),
|
|
76
|
+
re_path(
|
|
77
|
+
r"^templates/json/$",
|
|
78
|
+
views.TemplateList.as_view(),
|
|
79
|
+
name="template_list",
|
|
80
|
+
),
|
|
76
81
|
]
|
|
77
82
|
i18n_urls += decorated_patterns(
|
|
78
83
|
[can_view_map, cache_control(must_revalidate=True)],
|
|
@@ -114,9 +119,10 @@ i18n_urls += decorated_patterns(
|
|
|
114
119
|
views.ToggleMapStarStatus.as_view(),
|
|
115
120
|
name="map_star",
|
|
116
121
|
),
|
|
117
|
-
path("me", views.
|
|
122
|
+
path("me", views.UserDashboard.as_view(), name="user_dashboard"),
|
|
118
123
|
path("me/download", views.user_download, name="user_download"),
|
|
119
124
|
path("me/teams", views.UserTeams.as_view(), name="user_teams"),
|
|
125
|
+
path("me/templates", views.UserTemplates.as_view(), name="user_templates"),
|
|
120
126
|
path("team/create/", views.TeamNew.as_view(), name="team_new"),
|
|
121
127
|
)
|
|
122
128
|
|
umap/views.py
CHANGED
|
@@ -138,9 +138,7 @@ class PublicMapsMixin(object):
|
|
|
138
138
|
return maps
|
|
139
139
|
|
|
140
140
|
def get_highlighted_maps(self):
|
|
141
|
-
|
|
142
|
-
stars = Star.objects.filter(by__in=staff).values("map")
|
|
143
|
-
qs = Map.public.filter(pk__in=stars)
|
|
141
|
+
qs = Map.public.starred_by_staff()
|
|
144
142
|
maps = qs.order_by("-modified_at")
|
|
145
143
|
return maps
|
|
146
144
|
|
|
@@ -332,10 +330,10 @@ class TeamMaps(PaginatorMixin, DetailView):
|
|
|
332
330
|
|
|
333
331
|
|
|
334
332
|
class SearchMixin:
|
|
335
|
-
def get_search_queryset(self, **kwargs):
|
|
333
|
+
def get_search_queryset(self, qs=None, **kwargs):
|
|
336
334
|
q = self.request.GET.get("q")
|
|
337
335
|
tags = [t for t in self.request.GET.getlist("tags") if t]
|
|
338
|
-
qs = Map.
|
|
336
|
+
qs = qs or Map.public.all()
|
|
339
337
|
if q:
|
|
340
338
|
vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
|
|
341
339
|
query = SearchQuery(
|
|
@@ -382,14 +380,11 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
|
|
382
380
|
return self.get_queryset().get(pk=self.request.user.pk)
|
|
383
381
|
|
|
384
382
|
def get_maps(self):
|
|
385
|
-
qs =
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.union(qs.filter(editors=self.object))
|
|
391
|
-
.union(qs.filter(team__in=teams))
|
|
392
|
-
)
|
|
383
|
+
qs = Map.private.filter(is_template=False)
|
|
384
|
+
search_qs = self.get_search_queryset(qs)
|
|
385
|
+
if search_qs is not None:
|
|
386
|
+
qs = search_qs
|
|
387
|
+
qs = qs.for_user(self.object)
|
|
393
388
|
return qs.order_by("-modified_at")
|
|
394
389
|
|
|
395
390
|
def get_context_data(self, **kwargs):
|
|
@@ -398,7 +393,26 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
|
|
|
398
393
|
return super().get_context_data(**kwargs)
|
|
399
394
|
|
|
400
395
|
|
|
401
|
-
|
|
396
|
+
class UserTemplates(PaginatorMixin, DetailView, SearchMixin):
|
|
397
|
+
model = User
|
|
398
|
+
template_name = "umap/user_templates.html"
|
|
399
|
+
list_template_name = "umap/map_table.html"
|
|
400
|
+
|
|
401
|
+
def get_object(self):
|
|
402
|
+
return self.get_queryset().get(pk=self.request.user.pk)
|
|
403
|
+
|
|
404
|
+
def get_maps(self):
|
|
405
|
+
qs = Map.private.filter(is_template=True)
|
|
406
|
+
search_qs = self.get_search_queryset(qs)
|
|
407
|
+
if search_qs is not None:
|
|
408
|
+
qs = search_qs
|
|
409
|
+
qs = qs.for_user(self.object)
|
|
410
|
+
return qs.order_by("-modified_at")
|
|
411
|
+
|
|
412
|
+
def get_context_data(self, **kwargs):
|
|
413
|
+
page = self.paginate(self.get_maps(), settings.UMAP_MAPS_PER_PAGE_OWNER)
|
|
414
|
+
kwargs.update({"q": self.request.GET.get("q"), "maps": page})
|
|
415
|
+
return super().get_context_data(**kwargs)
|
|
402
416
|
|
|
403
417
|
|
|
404
418
|
class UserDownload(DetailView, SearchMixin):
|
|
@@ -408,9 +422,9 @@ class UserDownload(DetailView, SearchMixin):
|
|
|
408
422
|
return self.get_queryset().get(pk=self.request.user.pk)
|
|
409
423
|
|
|
410
424
|
def get_maps(self):
|
|
411
|
-
qs = Map.
|
|
412
|
-
|
|
413
|
-
return
|
|
425
|
+
qs = Map.private.filter(id__in=self.request.GET.getlist("map_id"))
|
|
426
|
+
qsu = qs.for_user(self.object)
|
|
427
|
+
return qsu.order_by("-modified_at")
|
|
414
428
|
|
|
415
429
|
def render_to_response(self, context, *args, **kwargs):
|
|
416
430
|
zip_buffer = io.BytesIO()
|
|
@@ -619,6 +633,7 @@ class MapDetailMixin(SessionMixin):
|
|
|
619
633
|
"websocketEnabled": settings.REALTIME_ENABLED,
|
|
620
634
|
"importers": settings.UMAP_IMPORTERS,
|
|
621
635
|
"defaultLabelKeys": settings.UMAP_LABEL_KEYS,
|
|
636
|
+
"help_links": settings.UMAP_HELP_LINKS,
|
|
622
637
|
}
|
|
623
638
|
created = bool(getattr(self, "object", None))
|
|
624
639
|
if created:
|
|
@@ -668,7 +683,7 @@ class MapDetailMixin(SessionMixin):
|
|
|
668
683
|
geojson["properties"] = {}
|
|
669
684
|
geojson["properties"].update(properties)
|
|
670
685
|
geojson["properties"]["datalayers"] = self.get_datalayers()
|
|
671
|
-
context["map_settings"] = json_dumps(geojson, indent=settings.DEBUG)
|
|
686
|
+
context["map_settings"] = json_dumps(geojson, indent=settings.DEBUG or None)
|
|
672
687
|
self.set_preconnect(geojson["properties"], context)
|
|
673
688
|
return context
|
|
674
689
|
|
|
@@ -775,6 +790,7 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
|
|
|
775
790
|
if "properties" not in map_settings:
|
|
776
791
|
map_settings["properties"] = {}
|
|
777
792
|
map_settings["properties"]["name"] = self.object.name
|
|
793
|
+
map_settings["properties"]["is_template"] = self.object.is_template
|
|
778
794
|
map_settings["properties"]["permissions"] = self.get_permissions()
|
|
779
795
|
author = self.object.get_author()
|
|
780
796
|
if author:
|
|
@@ -802,7 +818,10 @@ class MapDownload(DetailView):
|
|
|
802
818
|
return reverse("map_download", args=(self.object.pk,))
|
|
803
819
|
|
|
804
820
|
def render_to_response(self, context, *args, **kwargs):
|
|
805
|
-
|
|
821
|
+
include_data = self.request.GET.get("include_data") != "0"
|
|
822
|
+
umapjson = self.object.generate_umapjson(
|
|
823
|
+
self.request, include_data=include_data
|
|
824
|
+
)
|
|
806
825
|
response = simple_json_response(**umapjson)
|
|
807
826
|
response["Content-Disposition"] = (
|
|
808
827
|
f'attachment; filename="umap_backup_{self.object.slug}.umap"'
|
|
@@ -1391,6 +1410,10 @@ def stats(request):
|
|
|
1391
1410
|
return simple_json_response(
|
|
1392
1411
|
**{
|
|
1393
1412
|
"version": VERSION,
|
|
1413
|
+
"realtime_enabled": settings.REALTIME_ENABLED,
|
|
1414
|
+
"anonymous_allowed": settings.UMAP_ALLOW_ANONYMOUS,
|
|
1415
|
+
"importers": list(settings.UMAP_IMPORTERS.keys()),
|
|
1416
|
+
"teams_count": Team.objects.count(),
|
|
1394
1417
|
"maps_count": Map.objects.count(),
|
|
1395
1418
|
"maps_active_last_week_count": Map.objects.filter(
|
|
1396
1419
|
modified_at__gt=last_week
|
|
@@ -1456,3 +1479,26 @@ class LoginPopupEnd(TemplateView):
|
|
|
1456
1479
|
if backend in settings.DEPRECATED_AUTHENTICATION_BACKENDS:
|
|
1457
1480
|
return HttpResponseRedirect(reverse("user_profile"))
|
|
1458
1481
|
return super().get(*args, **kwargs)
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
class TemplateList(ListView):
|
|
1485
|
+
model = Map
|
|
1486
|
+
|
|
1487
|
+
def render_to_response(self, context, **response_kwargs):
|
|
1488
|
+
source = self.request.GET.get("source")
|
|
1489
|
+
if source == "mine":
|
|
1490
|
+
qs = Map.private.filter(is_template=True).for_user(self.request.user)
|
|
1491
|
+
elif source == "community":
|
|
1492
|
+
qs = Map.public.filter(is_template=True)
|
|
1493
|
+
elif source == "staff":
|
|
1494
|
+
qs = Map.public.starred_by_staff().filter(is_template=True)
|
|
1495
|
+
templates = [
|
|
1496
|
+
{
|
|
1497
|
+
"id": m.id,
|
|
1498
|
+
"name": m.name,
|
|
1499
|
+
"description": m.description,
|
|
1500
|
+
"url": m.get_absolute_url(),
|
|
1501
|
+
}
|
|
1502
|
+
for m in qs.order_by("-modified_at")
|
|
1503
|
+
]
|
|
1504
|
+
return simple_json_response(templates=templates)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: umap-project
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Create maps with OpenStreetMap layers in a minute and embed them in your site.
|
|
5
5
|
Author-email: Yohan Boniface <yb@enix.org>
|
|
6
6
|
Maintainer-email: David Larlet <david@larlet.fr>
|
|
@@ -19,42 +19,42 @@ Requires-Python: >=3.10
|
|
|
19
19
|
Requires-Dist: django-agnocomplete==2.2.0
|
|
20
20
|
Requires-Dist: django-environ==0.12.0
|
|
21
21
|
Requires-Dist: django-probes==1.7.0
|
|
22
|
-
Requires-Dist: django==5.
|
|
22
|
+
Requires-Dist: django==5.2.2
|
|
23
23
|
Requires-Dist: pillow==11.2.1
|
|
24
|
-
Requires-Dist: psycopg==3.2.
|
|
24
|
+
Requires-Dist: psycopg==3.2.9
|
|
25
25
|
Requires-Dist: rcssmin==1.2.1
|
|
26
|
-
Requires-Dist: requests==2.32.
|
|
26
|
+
Requires-Dist: requests==2.32.4
|
|
27
27
|
Requires-Dist: rjsmin==1.2.4
|
|
28
28
|
Requires-Dist: social-auth-app-django==5.4.3
|
|
29
|
-
Requires-Dist: social-auth-core==4.
|
|
29
|
+
Requires-Dist: social-auth-core==4.6.1
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: djlint==1.36.4; extra == 'dev'
|
|
32
32
|
Requires-Dist: hatch==1.14.1; extra == 'dev'
|
|
33
33
|
Requires-Dist: isort==6.0.1; extra == 'dev'
|
|
34
|
-
Requires-Dist: mkdocs-material==9.6.
|
|
34
|
+
Requires-Dist: mkdocs-material==9.6.14; extra == 'dev'
|
|
35
35
|
Requires-Dist: mkdocs-static-i18n==1.3.0; extra == 'dev'
|
|
36
36
|
Requires-Dist: mkdocs==1.6.1; extra == 'dev'
|
|
37
|
-
Requires-Dist: pymdown-extensions==10.
|
|
38
|
-
Requires-Dist: ruff==0.11.
|
|
37
|
+
Requires-Dist: pymdown-extensions==10.15; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff==0.11.13; extra == 'dev'
|
|
39
39
|
Requires-Dist: vermin==1.6.0; extra == 'dev'
|
|
40
40
|
Provides-Extra: docker
|
|
41
|
-
Requires-Dist: uvicorn==0.34.
|
|
41
|
+
Requires-Dist: uvicorn==0.34.3; extra == 'docker'
|
|
42
42
|
Provides-Extra: s3
|
|
43
43
|
Requires-Dist: django-storages[s3]==1.14.6; extra == 's3'
|
|
44
44
|
Provides-Extra: sync
|
|
45
|
-
Requires-Dist: pydantic==2.11.
|
|
46
|
-
Requires-Dist: redis==
|
|
45
|
+
Requires-Dist: pydantic==2.11.5; extra == 'sync'
|
|
46
|
+
Requires-Dist: redis==6.2.0; extra == 'sync'
|
|
47
47
|
Requires-Dist: websockets==15.0.1; extra == 'sync'
|
|
48
48
|
Provides-Extra: test
|
|
49
|
-
Requires-Dist: daphne==4.
|
|
49
|
+
Requires-Dist: daphne==4.2.0; extra == 'test'
|
|
50
50
|
Requires-Dist: factory-boy==3.3.3; extra == 'test'
|
|
51
|
-
Requires-Dist: moto[s3]==5.1.
|
|
51
|
+
Requires-Dist: moto[s3]==5.1.5; extra == 'test'
|
|
52
52
|
Requires-Dist: playwright>=1.39; extra == 'test'
|
|
53
53
|
Requires-Dist: pytest-django==4.11.1; extra == 'test'
|
|
54
54
|
Requires-Dist: pytest-playwright==0.7.0; extra == 'test'
|
|
55
|
-
Requires-Dist: pytest-rerunfailures==15.
|
|
55
|
+
Requires-Dist: pytest-rerunfailures==15.1; extra == 'test'
|
|
56
56
|
Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'test'
|
|
57
|
-
Requires-Dist: pytest==8.
|
|
57
|
+
Requires-Dist: pytest==8.4.0; extra == 'test'
|
|
58
58
|
Description-Content-Type: text/markdown
|
|
59
59
|
|
|
60
60
|
# uMap project
|