umap-project 3.0.6__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.

Files changed (142) hide show
  1. umap/__init__.py +1 -1
  2. umap/forms.py +1 -1
  3. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/br/LC_MESSAGES/django.po +219 -72
  5. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/ca/LC_MESSAGES/django.po +286 -95
  7. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/cs_CZ/LC_MESSAGES/django.po +211 -65
  9. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/da/LC_MESSAGES/django.po +394 -202
  11. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/de/LC_MESSAGES/django.po +146 -75
  13. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/el/LC_MESSAGES/django.po +125 -59
  15. umap/locale/en/LC_MESSAGES/django.po +124 -58
  16. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  17. umap/locale/es/LC_MESSAGES/django.po +125 -59
  18. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  19. umap/locale/et/LC_MESSAGES/django.po +210 -64
  20. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  21. umap/locale/eu/LC_MESSAGES/django.po +212 -65
  22. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/fa_IR/LC_MESSAGES/django.po +286 -95
  24. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/fr/LC_MESSAGES/django.po +125 -59
  26. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/gl/LC_MESSAGES/django.po +212 -66
  28. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/hu/LC_MESSAGES/django.po +148 -78
  30. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/is/LC_MESSAGES/django.po +130 -60
  32. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/it/LC_MESSAGES/django.po +125 -59
  34. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/ms/LC_MESSAGES/django.po +289 -98
  36. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/nl/LC_MESSAGES/django.po +128 -61
  38. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/pl/LC_MESSAGES/django.po +287 -96
  40. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  41. umap/locale/pt/LC_MESSAGES/django.po +211 -65
  42. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  43. umap/locale/zh_TW/LC_MESSAGES/django.po +212 -66
  44. umap/management/commands/migrate_to_S3.py +42 -20
  45. umap/management/commands/purge_old_versions.py +63 -0
  46. umap/management/commands/switch_user.py +52 -0
  47. umap/managers.py +29 -2
  48. umap/middleware.py +1 -1
  49. umap/migrations/0028_map_is_template.py +21 -0
  50. umap/models.py +14 -4
  51. umap/settings/base.py +22 -0
  52. umap/static/umap/base.css +4 -2
  53. umap/static/umap/content.css +1 -1
  54. umap/static/umap/css/dialog.css +5 -2
  55. umap/static/umap/css/form.css +19 -12
  56. umap/static/umap/css/icon.css +6 -0
  57. umap/static/umap/css/importers.css +4 -0
  58. umap/static/umap/css/panel.css +2 -0
  59. umap/static/umap/img/16-white.svg +5 -1
  60. umap/static/umap/img/16.svg +1 -1
  61. umap/static/umap/img/24-white.svg +3 -2
  62. umap/static/umap/img/24.svg +3 -4
  63. umap/static/umap/img/importers/opendata.svg +1 -0
  64. umap/static/umap/img/source/16-white.svg +8 -4
  65. umap/static/umap/img/source/16.svg +1 -1
  66. umap/static/umap/img/source/24-white.svg +5 -4
  67. umap/static/umap/img/source/24.svg +5 -6
  68. umap/static/umap/js/components/modal.js +27 -0
  69. umap/static/umap/js/modules/caption.js +4 -4
  70. umap/static/umap/js/modules/data/features.js +40 -4
  71. umap/static/umap/js/modules/data/layer.js +208 -138
  72. umap/static/umap/js/modules/form/builder.js +6 -14
  73. umap/static/umap/js/modules/form/fields.js +2 -2
  74. umap/static/umap/js/modules/help.js +11 -3
  75. umap/static/umap/js/modules/importer.js +7 -4
  76. umap/static/umap/js/modules/importers/opendata.js +142 -0
  77. umap/static/umap/js/modules/permissions.js +3 -3
  78. umap/static/umap/js/modules/rendering/controls.js +34 -2
  79. umap/static/umap/js/modules/rendering/icon.js +2 -2
  80. umap/static/umap/js/modules/rendering/layers/base.js +1 -1
  81. umap/static/umap/js/modules/rendering/layers/classified.js +55 -49
  82. umap/static/umap/js/modules/rendering/layers/cluster.js +16 -9
  83. umap/static/umap/js/modules/rendering/layers/heat.js +13 -11
  84. umap/static/umap/js/modules/rendering/map.js +5 -0
  85. umap/static/umap/js/modules/rendering/ui.js +23 -0
  86. umap/static/umap/js/modules/rules.js +24 -23
  87. umap/static/umap/js/modules/schema.js +60 -4
  88. umap/static/umap/js/modules/sync/updaters.js +7 -3
  89. umap/static/umap/js/modules/tableeditor.js +7 -30
  90. umap/static/umap/js/modules/templates.js +122 -0
  91. umap/static/umap/js/modules/ui/bar.js +13 -3
  92. umap/static/umap/js/modules/ui/panel.js +1 -1
  93. umap/static/umap/js/modules/umap.js +28 -13
  94. umap/static/umap/js/umap.controls.js +11 -4
  95. umap/static/umap/locale/br.js +51 -18
  96. umap/static/umap/locale/br.json +51 -18
  97. umap/static/umap/locale/da.js +343 -310
  98. umap/static/umap/locale/da.json +343 -310
  99. umap/static/umap/locale/de.js +40 -7
  100. umap/static/umap/locale/de.json +40 -7
  101. umap/static/umap/locale/el.js +31 -4
  102. umap/static/umap/locale/el.json +31 -4
  103. umap/static/umap/locale/en.js +33 -1
  104. umap/static/umap/locale/en.json +33 -1
  105. umap/static/umap/locale/es.js +37 -4
  106. umap/static/umap/locale/es.json +37 -4
  107. umap/static/umap/locale/fr.js +34 -1
  108. umap/static/umap/locale/fr.json +34 -1
  109. umap/static/umap/locale/hu.js +44 -17
  110. umap/static/umap/locale/hu.json +44 -17
  111. umap/static/umap/locale/it.js +74 -41
  112. umap/static/umap/locale/it.json +74 -41
  113. umap/static/umap/locale/nl.js +42 -9
  114. umap/static/umap/locale/nl.json +42 -9
  115. umap/static/umap/map.css +3 -23
  116. umap/static/umap/vendors/textpath/leaflet.textpath.js +184 -0
  117. umap/storage/fs.py +19 -9
  118. umap/templates/umap/dashboard_menu.html +5 -0
  119. umap/templates/umap/design_system.html +9 -0
  120. umap/templates/umap/js.html +3 -0
  121. umap/templates/umap/map_list.html +2 -2
  122. umap/templates/umap/map_table.html +18 -18
  123. umap/templates/umap/user_dashboard.html +9 -58
  124. umap/templates/umap/user_map_table.html +36 -0
  125. umap/templates/umap/user_templates.html +19 -0
  126. umap/templatetags/umap_tags.py +5 -0
  127. umap/tests/integration/test_conditional_rules.py +57 -0
  128. umap/tests/integration/test_datalayer.py +16 -9
  129. umap/tests/integration/test_edit_marker.py +11 -0
  130. umap/tests/integration/test_tableeditor.py +42 -7
  131. umap/tests/integration/test_templates.py +44 -0
  132. umap/tests/test_dashboard.py +19 -0
  133. umap/tests/test_purge_old_versions.py +91 -0
  134. umap/tests/test_switch_user.py +31 -0
  135. umap/tests/test_views.py +67 -0
  136. umap/urls.py +7 -1
  137. umap/views.py +64 -18
  138. {umap_project-3.0.6.dist-info → umap_project-3.1.0.dist-info}/METADATA +14 -14
  139. {umap_project-3.0.6.dist-info → umap_project-3.1.0.dist-info}/RECORD +142 -129
  140. {umap_project-3.0.6.dist-info → umap_project-3.1.0.dist-info}/WHEEL +0 -0
  141. {umap_project-3.0.6.dist-info → umap_project-3.1.0.dist-info}/entry_points.txt +0 -0
  142. {umap_project-3.0.6.dist-info → umap_project-3.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -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, datalayer, page):
117
- page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
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=name]")).to_have_count(1)
121
- page.locator("thead button[data-property=name]").click()
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("newname")
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=newname]")).to_have_count(1)
126
- expect(page.locator("table th button[data-property=name]")).to_have_count(0)
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)
@@ -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.user_dashboard, name="user_dashboard"),
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
- staff = User.objects.filter(is_staff=True)
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.objects.all()
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 = self.get_search_queryset() or Map.objects.all()
386
- qs = qs.exclude(share_status__in=[Map.DELETED, Map.BLOCKED])
387
- teams = self.object.teams.all()
388
- qs = (
389
- qs.filter(owner=self.object)
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
- user_dashboard = UserDashboard.as_view()
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.objects.filter(id__in=self.request.GET.getlist("map_id"))
412
- qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
413
- return qs.order_by("-modified_at")
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:
@@ -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
- umapjson = self.object.generate_umapjson(self.request)
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.6
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.2.1
22
+ Requires-Dist: django==5.2.2
23
23
  Requires-Dist: pillow==11.2.1
24
- Requires-Dist: psycopg==3.2.8
24
+ Requires-Dist: psycopg==3.2.9
25
25
  Requires-Dist: rcssmin==1.2.1
26
- Requires-Dist: requests==2.32.3
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.5.6
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.12; extra == 'dev'
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
37
  Requires-Dist: pymdown-extensions==10.15; extra == 'dev'
38
- Requires-Dist: ruff==0.11.9; 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.2; extra == 'docker'
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.4; extra == 'sync'
46
- Requires-Dist: redis==5.2.1; extra == 'sync'
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.1.2; extra == 'test'
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.4; extra == 'test'
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.0; extra == 'test'
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.3.5; extra == 'test'
57
+ Requires-Dist: pytest==8.4.0; extra == 'test'
58
58
  Description-Content-Type: text/markdown
59
59
 
60
60
  # uMap project