umap-project 3.1.1__py3-none-any.whl → 3.2.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 (151) hide show
  1. umap/__init__.py +1 -1
  2. umap/locale/en/LC_MESSAGES/django.po +21 -17
  3. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/fr/LC_MESSAGES/django.po +21 -17
  5. umap/management/commands/export_pictogram.py +29 -0
  6. umap/management/commands/migrate_to_S3.py +5 -1
  7. umap/management/commands/purge_old_versions.py +8 -6
  8. umap/settings/__init__.py +21 -0
  9. umap/settings/base.py +1 -0
  10. umap/static/umap/content.css +7 -2
  11. umap/static/umap/css/icon.css +77 -3
  12. umap/static/umap/css/panel.css +31 -1
  13. umap/static/umap/js/modules/browser.js +1 -1
  14. umap/static/umap/js/modules/data/features.js +14 -29
  15. umap/static/umap/js/modules/data/layer.js +248 -136
  16. umap/static/umap/js/modules/facets.js +2 -2
  17. umap/static/umap/js/modules/form/fields.js +56 -19
  18. umap/static/umap/js/modules/formatter.js +36 -8
  19. umap/static/umap/js/modules/importers/opendata.js +23 -6
  20. umap/static/umap/js/modules/managers.js +59 -0
  21. umap/static/umap/js/modules/rendering/icon.js +3 -5
  22. umap/static/umap/js/modules/rendering/layers/classified.js +8 -7
  23. umap/static/umap/js/modules/rendering/map.js +1 -1
  24. umap/static/umap/js/modules/rendering/ui.js +13 -0
  25. umap/static/umap/js/modules/rules.js +76 -23
  26. umap/static/umap/js/modules/schema.js +3 -0
  27. umap/static/umap/js/modules/slideshow.js +1 -1
  28. umap/static/umap/js/modules/sync/updaters.js +1 -6
  29. umap/static/umap/js/modules/tableeditor.js +13 -37
  30. umap/static/umap/js/modules/templates.js +7 -6
  31. umap/static/umap/js/modules/ui/panel.js +7 -0
  32. umap/static/umap/js/modules/umap.js +17 -6
  33. umap/static/umap/js/modules/utils.js +8 -7
  34. umap/static/umap/locale/am_ET.js +43 -6
  35. umap/static/umap/locale/am_ET.json +43 -6
  36. umap/static/umap/locale/ar.js +43 -6
  37. umap/static/umap/locale/ar.json +43 -6
  38. umap/static/umap/locale/ast.js +43 -6
  39. umap/static/umap/locale/ast.json +43 -6
  40. umap/static/umap/locale/bg.js +43 -6
  41. umap/static/umap/locale/bg.json +43 -6
  42. umap/static/umap/locale/br.js +30 -26
  43. umap/static/umap/locale/br.json +30 -26
  44. umap/static/umap/locale/ca.js +50 -13
  45. umap/static/umap/locale/ca.json +50 -13
  46. umap/static/umap/locale/cs_CZ.js +43 -6
  47. umap/static/umap/locale/cs_CZ.json +43 -6
  48. umap/static/umap/locale/da.js +10 -6
  49. umap/static/umap/locale/da.json +10 -6
  50. umap/static/umap/locale/de.js +10 -6
  51. umap/static/umap/locale/de.json +10 -6
  52. umap/static/umap/locale/el.js +20 -10
  53. umap/static/umap/locale/el.json +20 -10
  54. umap/static/umap/locale/en.js +10 -6
  55. umap/static/umap/locale/en.json +10 -6
  56. umap/static/umap/locale/en_US.json +43 -6
  57. umap/static/umap/locale/es.js +10 -6
  58. umap/static/umap/locale/es.json +10 -6
  59. umap/static/umap/locale/et.js +43 -6
  60. umap/static/umap/locale/et.json +43 -6
  61. umap/static/umap/locale/eu.js +43 -6
  62. umap/static/umap/locale/eu.json +43 -6
  63. umap/static/umap/locale/fa_IR.js +43 -6
  64. umap/static/umap/locale/fa_IR.json +43 -6
  65. umap/static/umap/locale/fi.js +43 -6
  66. umap/static/umap/locale/fi.json +43 -6
  67. umap/static/umap/locale/fr.js +10 -6
  68. umap/static/umap/locale/fr.json +10 -6
  69. umap/static/umap/locale/gl.js +43 -6
  70. umap/static/umap/locale/gl.json +43 -6
  71. umap/static/umap/locale/he.js +43 -6
  72. umap/static/umap/locale/he.json +43 -6
  73. umap/static/umap/locale/hr.js +43 -6
  74. umap/static/umap/locale/hr.json +43 -6
  75. umap/static/umap/locale/hu.js +34 -24
  76. umap/static/umap/locale/hu.json +34 -24
  77. umap/static/umap/locale/id.js +43 -6
  78. umap/static/umap/locale/id.json +43 -6
  79. umap/static/umap/locale/is.js +43 -6
  80. umap/static/umap/locale/is.json +43 -6
  81. umap/static/umap/locale/it.js +10 -6
  82. umap/static/umap/locale/it.json +10 -6
  83. umap/static/umap/locale/ja.js +43 -6
  84. umap/static/umap/locale/ja.json +43 -6
  85. umap/static/umap/locale/ko.js +43 -6
  86. umap/static/umap/locale/ko.json +43 -6
  87. umap/static/umap/locale/lt.js +43 -6
  88. umap/static/umap/locale/lt.json +43 -6
  89. umap/static/umap/locale/ms.js +43 -6
  90. umap/static/umap/locale/ms.json +43 -6
  91. umap/static/umap/locale/nl.js +10 -6
  92. umap/static/umap/locale/nl.json +10 -6
  93. umap/static/umap/locale/no.js +43 -6
  94. umap/static/umap/locale/no.json +43 -6
  95. umap/static/umap/locale/pl.js +43 -6
  96. umap/static/umap/locale/pl.json +43 -6
  97. umap/static/umap/locale/pl_PL.json +43 -6
  98. umap/static/umap/locale/pt.js +43 -6
  99. umap/static/umap/locale/pt.json +43 -6
  100. umap/static/umap/locale/pt_BR.js +53 -16
  101. umap/static/umap/locale/pt_BR.json +53 -16
  102. umap/static/umap/locale/pt_PT.js +43 -6
  103. umap/static/umap/locale/pt_PT.json +43 -6
  104. umap/static/umap/locale/ro.js +43 -6
  105. umap/static/umap/locale/ro.json +43 -6
  106. umap/static/umap/locale/ru.js +43 -6
  107. umap/static/umap/locale/ru.json +43 -6
  108. umap/static/umap/locale/sk_SK.js +43 -6
  109. umap/static/umap/locale/sk_SK.json +43 -6
  110. umap/static/umap/locale/sl.js +43 -6
  111. umap/static/umap/locale/sl.json +43 -6
  112. umap/static/umap/locale/sr.js +43 -6
  113. umap/static/umap/locale/sr.json +43 -6
  114. umap/static/umap/locale/sv.js +43 -6
  115. umap/static/umap/locale/sv.json +43 -6
  116. umap/static/umap/locale/th_TH.js +43 -6
  117. umap/static/umap/locale/th_TH.json +43 -6
  118. umap/static/umap/locale/tr.js +43 -6
  119. umap/static/umap/locale/tr.json +43 -6
  120. umap/static/umap/locale/uk_UA.js +43 -6
  121. umap/static/umap/locale/uk_UA.json +43 -6
  122. umap/static/umap/locale/vi.js +43 -6
  123. umap/static/umap/locale/vi.json +43 -6
  124. umap/static/umap/locale/vi_VN.json +43 -6
  125. umap/static/umap/locale/zh.js +43 -6
  126. umap/static/umap/locale/zh.json +43 -6
  127. umap/static/umap/locale/zh_CN.json +43 -6
  128. umap/static/umap/locale/zh_TW.Big5.json +43 -6
  129. umap/static/umap/locale/zh_TW.js +43 -6
  130. umap/static/umap/locale/zh_TW.json +43 -6
  131. umap/static/umap/map.css +239 -65
  132. umap/static/umap/vendors/betterknown/betterknown.mjs +287 -0
  133. umap/storage/fs.py +3 -2
  134. umap/templates/base.html +4 -1
  135. umap/tests/base.py +9 -1
  136. umap/tests/integration/test_basics.py +1 -1
  137. umap/tests/integration/test_conditional_rules.py +62 -20
  138. umap/tests/integration/test_edit_datalayer.py +1 -1
  139. umap/tests/integration/test_edit_marker.py +1 -1
  140. umap/tests/integration/test_export_map.py +10 -0
  141. umap/tests/integration/test_import.py +140 -0
  142. umap/tests/integration/test_optimistic_merge.py +72 -12
  143. umap/tests/integration/test_tableeditor.py +6 -3
  144. umap/utils.py +33 -0
  145. umap/views.py +24 -10
  146. umap_project-3.2.0.dist-info/METADATA +76 -0
  147. {umap_project-3.1.1.dist-info → umap_project-3.2.0.dist-info}/RECORD +150 -148
  148. umap_project-3.1.1.dist-info/METADATA +0 -68
  149. {umap_project-3.1.1.dist-info → umap_project-3.2.0.dist-info}/WHEEL +0 -0
  150. {umap_project-3.1.1.dist-info → umap_project-3.2.0.dist-info}/entry_points.txt +0 -0
  151. {umap_project-3.1.1.dist-info → umap_project-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -557,6 +557,38 @@ def test_import_multipolyline(live_server, page, tilelayer):
557
557
  expect(paths).to_have_count(1)
558
558
 
559
559
 
560
+ def test_import_false_multipoint(live_server, page, tilelayer):
561
+ data = {
562
+ "type": "FeatureCollection",
563
+ "features": [
564
+ {
565
+ "type": "Feature",
566
+ "geometry": {
567
+ "coordinates": [[1.43661447777346, 43.59853484073553]],
568
+ "type": "MultiPoint",
569
+ },
570
+ "properties": {"name": "foo bar"},
571
+ }
572
+ ],
573
+ }
574
+ page.goto(f"{live_server.url}/map/new/")
575
+ page.get_by_title("Open browser").click()
576
+ layers = page.locator(".umap-browser .datalayer")
577
+ markers = page.locator(".leaflet-marker-icon")
578
+ expect(markers).to_have_count(0)
579
+ expect(layers).to_have_count(0)
580
+ button = page.get_by_title("Import data")
581
+ expect(button).to_be_visible()
582
+ button.click()
583
+ textarea = page.locator(".umap-import textarea")
584
+ textarea.fill(json.dumps(data))
585
+ page.locator('select[name="format"]').select_option("geojson")
586
+ page.get_by_role("button", name="Import data", exact=True).click()
587
+ # A layer has been created
588
+ expect(layers).to_have_count(1)
589
+ expect(markers).to_have_count(1)
590
+
591
+
560
592
  def test_should_not_import_empty_coordinates(live_server, page, tilelayer):
561
593
  data = {
562
594
  "type": "FeatureCollection",
@@ -670,6 +702,114 @@ def test_import_csv_with_commas_in_latlon(tilelayer, live_server, page, settings
670
702
  }
671
703
 
672
704
 
705
+ def test_import_csv_with_wkt_geom(tilelayer, live_server, page, settings):
706
+ settings.UMAP_ALLOW_ANONYMOUS = True
707
+ page.goto(f"{live_server.url}/map/new/")
708
+ page.get_by_title("Open browser").click()
709
+ layers = page.locator(".umap-browser .datalayer")
710
+ markers = page.locator(".leaflet-marker-icon")
711
+ paths = page.locator("path")
712
+ page.get_by_title("Import data").click()
713
+ textarea = page.locator(".umap-import textarea")
714
+ textarea.fill(
715
+ "geom;foobar\nPOLYGON ((-64.8 32.3, -65.5 18.3, -80.3 25.2, -64.8 32.3));mypoly\nPOINT(48.35 12.23);mypoint"
716
+ )
717
+ page.locator('select[name="format"]').select_option("csv")
718
+ page.get_by_role("button", name="Import data", exact=True).click()
719
+ expect(layers).to_have_count(1)
720
+ expect(markers).to_have_count(1)
721
+ expect(paths).to_have_count(1)
722
+ with page.expect_response(re.compile(r".*/datalayer/create/.*")):
723
+ page.get_by_role("button", name="Save").click()
724
+ datalayer = DataLayer.objects.last()
725
+ saved_data = json.loads(Path(datalayer.geojson.path).read_text())
726
+ assert saved_data["features"][0]["geometry"] == {
727
+ "coordinates": [
728
+ 48.35,
729
+ 12.23,
730
+ ],
731
+ "type": "Point",
732
+ }
733
+ assert saved_data["features"][1]["geometry"] == {
734
+ "coordinates": [
735
+ [
736
+ [
737
+ -64.8,
738
+ 32.3,
739
+ ],
740
+ [
741
+ -65.5,
742
+ 18.3,
743
+ ],
744
+ [
745
+ -80.3,
746
+ 25.2,
747
+ ],
748
+ [
749
+ -64.8,
750
+ 32.3,
751
+ ],
752
+ ],
753
+ ],
754
+ "type": "Polygon",
755
+ }
756
+
757
+
758
+ def test_import_csv_with_geojson_geom(tilelayer, live_server, page, settings):
759
+ settings.UMAP_ALLOW_ANONYMOUS = True
760
+ page.goto(f"{live_server.url}/map/new/")
761
+ page.get_by_title("Open browser").click()
762
+ layers = page.locator(".umap-browser .datalayer")
763
+ markers = page.locator(".leaflet-marker-icon")
764
+ paths = page.locator("path")
765
+ page.get_by_title("Import data").click()
766
+ textarea = page.locator(".umap-import textarea")
767
+ textarea.fill(
768
+ "geojson;foobar\n"
769
+ '{"coordinates": [[[-64.8,32.3],[-65.5,18.3],[-80.3,25.2],[-64.8,32.3]]],"type": "Polygon"};mypoly\n'
770
+ '{"coordinates": [48.35,12.23],"type": "Point"};mypoint'
771
+ )
772
+ page.locator('select[name="format"]').select_option("csv")
773
+ page.get_by_role("button", name="Import data", exact=True).click()
774
+ expect(layers).to_have_count(1)
775
+ expect(markers).to_have_count(1)
776
+ expect(paths).to_have_count(1)
777
+ with page.expect_response(re.compile(r".*/datalayer/create/.*")):
778
+ page.get_by_role("button", name="Save").click()
779
+ datalayer = DataLayer.objects.last()
780
+ saved_data = json.loads(Path(datalayer.geojson.path).read_text())
781
+ assert saved_data["features"][0]["geometry"] == {
782
+ "coordinates": [
783
+ 48.35,
784
+ 12.23,
785
+ ],
786
+ "type": "Point",
787
+ }
788
+ assert saved_data["features"][1]["geometry"] == {
789
+ "coordinates": [
790
+ [
791
+ [
792
+ -64.8,
793
+ 32.3,
794
+ ],
795
+ [
796
+ -65.5,
797
+ 18.3,
798
+ ],
799
+ [
800
+ -80.3,
801
+ 25.2,
802
+ ],
803
+ [
804
+ -64.8,
805
+ 32.3,
806
+ ],
807
+ ],
808
+ ],
809
+ "type": "Polygon",
810
+ }
811
+
812
+
673
813
  def test_create_remote_data(page, live_server, tilelayer):
674
814
  def handle(route):
675
815
  route.fulfill(
@@ -12,13 +12,13 @@ from ..base import DataLayerFactory, MapFactory
12
12
  DATALAYER_UPDATE = re.compile(r".*/datalayer/update/.*")
13
13
 
14
14
 
15
- def test_created_markers_are_merged(context, live_server, tilelayer):
15
+ def test_created_markers_are_merged(new_page, live_server, tilelayer):
16
16
  # Let's create a new map with an empty datalayer
17
17
  map = MapFactory(name="server-side merge")
18
18
  datalayer = DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
19
19
 
20
20
  # Now navigate to this map and create marker
21
- page_one = context.new_page()
21
+ page_one = new_page("page 1")
22
22
  page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
23
23
 
24
24
  save_p1 = page_one.get_by_role("button", name="Save")
@@ -52,10 +52,20 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
52
52
  "id": str(datalayer.pk),
53
53
  "rank": 0,
54
54
  "remoteData": {},
55
+ "fields": [
56
+ {
57
+ "key": "name",
58
+ "type": "String",
59
+ },
60
+ {
61
+ "key": "description",
62
+ "type": "Text",
63
+ },
64
+ ],
55
65
  }
56
66
 
57
67
  # Now navigate to this map from another tab
58
- page_two = context.new_page()
68
+ page_two = new_page("page 2")
59
69
 
60
70
  page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
61
71
 
@@ -91,6 +101,16 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
91
101
  "id": str(datalayer.pk),
92
102
  "rank": 0,
93
103
  "remoteData": {},
104
+ "fields": [
105
+ {
106
+ "key": "name",
107
+ "type": "String",
108
+ },
109
+ {
110
+ "key": "description",
111
+ "type": "Text",
112
+ },
113
+ ],
94
114
  }
95
115
 
96
116
  # Now create another marker in the first tab
@@ -111,6 +131,16 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
111
131
  "id": str(datalayer.pk),
112
132
  "rank": 0,
113
133
  "remoteData": {},
134
+ "fields": [
135
+ {
136
+ "key": "name",
137
+ "type": "String",
138
+ },
139
+ {
140
+ "key": "description",
141
+ "type": "Text",
142
+ },
143
+ ],
114
144
  }
115
145
 
116
146
  # And again
@@ -131,6 +161,16 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
131
161
  "id": str(datalayer.pk),
132
162
  "rank": 0,
133
163
  "remoteData": {},
164
+ "fields": [
165
+ {
166
+ "key": "name",
167
+ "type": "String",
168
+ },
169
+ {
170
+ "key": "description",
171
+ "type": "Text",
172
+ },
173
+ ],
134
174
  }
135
175
  expect(marker_pane_p1).to_have_count(4)
136
176
 
@@ -153,20 +193,30 @@ def test_created_markers_are_merged(context, live_server, tilelayer):
153
193
  "id": str(datalayer.pk),
154
194
  "rank": 0,
155
195
  "remoteData": {},
196
+ "fields": [
197
+ {
198
+ "key": "name",
199
+ "type": "String",
200
+ },
201
+ {
202
+ "key": "description",
203
+ "type": "Text",
204
+ },
205
+ ],
156
206
  }
157
207
  expect(marker_pane_p2).to_have_count(5)
158
208
 
159
209
 
160
- def test_empty_datalayers_can_be_merged(context, live_server, tilelayer):
210
+ def test_empty_datalayers_can_be_merged(new_page, live_server, tilelayer):
161
211
  # Let's create a new map with an empty datalayer
162
212
  map = MapFactory(name="server-side merge")
163
213
  DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
164
214
 
165
215
  # Open two tabs at the same time, on the same empty map
166
- page_one = context.new_page()
216
+ page_one = new_page("page 1")
167
217
  page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
168
218
 
169
- page_two = context.new_page()
219
+ page_two = new_page("page 2")
170
220
  page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
171
221
 
172
222
  save_p1 = page_one.get_by_role("button", name="Save")
@@ -213,15 +263,15 @@ def test_empty_datalayers_can_be_merged(context, live_server, tilelayer):
213
263
  expect(marker_pane_p2).to_have_count(2)
214
264
 
215
265
 
216
- def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
266
+ def test_same_second_edit_doesnt_conflict(new_page, live_server, tilelayer):
217
267
  # Let's create a new map with an empty datalayer
218
268
  map = MapFactory(name="server-side merge")
219
269
  datalayer = DataLayerFactory(map=map, edit_status=DataLayer.ANONYMOUS, data={})
220
270
 
221
271
  # Open the created map on two pages.
222
- page_one = context.new_page()
272
+ page_one = new_page("page 1")
223
273
  page_one.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
224
- page_two = context.new_page()
274
+ page_two = new_page("page 2")
225
275
  page_two.goto(f"{live_server.url}{map.get_absolute_url()}?edit")
226
276
 
227
277
  save_p1 = page_one.get_by_role("button", name="Save")
@@ -280,14 +330,24 @@ def test_same_second_edit_doesnt_conflict(context, live_server, tilelayer):
280
330
  "id": str(datalayer.pk),
281
331
  "rank": 0,
282
332
  "remoteData": {},
333
+ "fields": [
334
+ {
335
+ "key": "name",
336
+ "type": "String",
337
+ },
338
+ {
339
+ "key": "description",
340
+ "type": "Text",
341
+ },
342
+ ],
283
343
  }
284
344
 
285
345
 
286
- def test_should_display_alert_on_conflict(context, live_server, datalayer, openmap):
346
+ def test_should_display_alert_on_conflict(new_page, live_server, datalayer, openmap):
287
347
  # Open the map on two pages.
288
- page_one = context.new_page()
348
+ page_one = new_page("page 1")
289
349
  page_one.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
290
- page_two = context.new_page()
350
+ page_two = new_page("page 2")
291
351
  page_two.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
292
352
 
293
353
  # Change name on page one and save
@@ -69,7 +69,9 @@ def test_table_editor(live_server, openmap, datalayer, page):
69
69
  page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
70
70
  page.get_by_role("button", name="Manage layers").click()
71
71
  page.locator(".panel").get_by_title("Edit properties in a table").click()
72
- page.get_by_text("Add a new property").click()
72
+ page.locator("td[data-property=description]").dblclick()
73
+ page.locator('textarea[name="description"]').fill("nice new description")
74
+ page.get_by_text("Add a new field").click()
73
75
  page.locator("dialog").locator("input").fill("newprop")
74
76
  page.locator("dialog").get_by_role("button", name="OK").click()
75
77
  page.locator("td").nth(2).dblclick()
@@ -83,6 +85,7 @@ def test_table_editor(live_server, openmap, datalayer, page):
83
85
  page.get_by_role("button", name="Save").click()
84
86
  saved = DataLayer.objects.last()
85
87
  data = json.loads(Path(saved.geojson.path).read_text())
88
+ assert data["features"][0]["properties"]["description"] == "nice new description"
86
89
  assert data["features"][0]["properties"]["newprop"] == "newvalue"
87
90
  assert "name" not in data["features"][0]["properties"]
88
91
 
@@ -91,7 +94,7 @@ def test_cannot_add_existing_property_name(live_server, openmap, datalayer, page
91
94
  page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
92
95
  page.get_by_role("button", name="Manage layers").click()
93
96
  page.locator(".panel").get_by_title("Edit properties in a table").click()
94
- page.get_by_text("Add a new property").click()
97
+ page.get_by_text("Add a new field").click()
95
98
  page.locator("dialog").locator("input").fill("name")
96
99
  page.get_by_role("button", name="OK").click()
97
100
  expect(page.get_by_role("dialog")).to_contain_text(
@@ -104,7 +107,7 @@ def test_cannot_add_property_with_a_dot(live_server, openmap, datalayer, page):
104
107
  page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
105
108
  page.get_by_role("button", name="Manage layers").click()
106
109
  page.locator(".panel").get_by_title("Edit properties in a table").click()
107
- page.get_by_text("Add a new property").click()
110
+ page.get_by_text("Add a new field").click()
108
111
  page.locator("dialog").locator("input").fill("foo.bar")
109
112
  page.get_by_role("button", name="OK").click()
110
113
  expect(page.get_by_role("dialog")).to_contain_text(
umap/utils.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import gzip
2
2
  import json
3
3
  import os
4
+ from pathlib import Path
4
5
 
5
6
  from django.conf import settings
7
+ from django.contrib.staticfiles import finders
6
8
  from django.core.serializers.json import DjangoJSONEncoder
7
9
  from django.urls import URLPattern, URLResolver, get_resolver
8
10
 
@@ -185,3 +187,34 @@ def merge_features(reference: list, latest: list, incoming: list):
185
187
  def json_dumps(obj, **kwargs):
186
188
  """Utility using the Django JSON Encoder when dumping objects"""
187
189
  return json.dumps(obj, cls=DjangoJSONEncoder, **kwargs)
190
+
191
+
192
+ def collect_pictograms():
193
+ pictograms = {}
194
+
195
+ for name, definition in settings.UMAP_PICTOGRAMS_COLLECTIONS.items():
196
+ root = Path(definition["path"])
197
+ subfolder = "pictograms"
198
+ if not root.is_absolute():
199
+ root = Path(finders.find(root).removesuffix(definition["path"]))
200
+ subfolder = definition["path"]
201
+ categories = {}
202
+ for path in (root / subfolder).iterdir():
203
+ if path.is_dir():
204
+ categories[path.name] = []
205
+ for subpath in path.iterdir():
206
+ if subpath.is_dir() or subpath.name.startswith("."):
207
+ continue
208
+ src = subpath.relative_to(root)
209
+ categories[path.name].append(
210
+ {
211
+ "name": subpath.stem,
212
+ "src": f"{settings.STATIC_URL}{src}",
213
+ }
214
+ )
215
+ pictograms[name] = {
216
+ "attribution": definition.get("attribution"),
217
+ "categories": categories,
218
+ }
219
+
220
+ return pictograms
umap/views.py CHANGED
@@ -26,6 +26,7 @@ from django.core.mail import send_mail
26
26
  from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
27
27
  from django.core.signing import BadSignature, Signer, TimestampSigner
28
28
  from django.core.validators import URLValidator, ValidationError
29
+ from django.db.models import QuerySet
29
30
  from django.http import (
30
31
  Http404,
31
32
  HttpResponse,
@@ -70,6 +71,7 @@ from .models import DataLayer, Licence, Map, Pictogram, Star, Team, TileLayer
70
71
  from .utils import (
71
72
  ConflictError,
72
73
  _urls_for_js,
74
+ collect_pictograms,
73
75
  gzip_file,
74
76
  is_ajax,
75
77
  json_dumps,
@@ -330,10 +332,9 @@ class TeamMaps(PaginatorMixin, DetailView):
330
332
 
331
333
 
332
334
  class SearchMixin:
333
- def get_search_queryset(self, qs=None, **kwargs):
335
+ def get_search_queryset(self, qs, **kwargs):
334
336
  q = self.request.GET.get("q")
335
337
  tags = [t for t in self.request.GET.getlist("tags") if t]
336
- qs = qs or Map.public.all()
337
338
  if q:
338
339
  vector = SearchVector("name", config=settings.UMAP_SEARCH_CONFIGURATION)
339
340
  query = SearchQuery(
@@ -351,10 +352,10 @@ class Search(PaginatorMixin, TemplateView, PublicMapsMixin, SearchMixin):
351
352
  list_template_name = "umap/map_list.html"
352
353
 
353
354
  def get_context_data(self, **kwargs):
354
- qs = self.get_search_queryset()
355
+ qs = self.get_search_queryset(Map.public.all())
355
356
  qs_count = 0
356
357
  results = []
357
- if qs is not None:
358
+ if isinstance(qs, QuerySet):
358
359
  qs = qs.filter(share_status=Map.PUBLIC).order_by("-modified_at")
359
360
  qs_count = qs.count()
360
361
  results = self.paginate(qs)
@@ -382,7 +383,7 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
382
383
  def get_maps(self):
383
384
  qs = Map.private.filter(is_template=False)
384
385
  search_qs = self.get_search_queryset(qs)
385
- if search_qs is not None:
386
+ if isinstance(search_qs, QuerySet):
386
387
  qs = search_qs
387
388
  qs = qs.for_user(self.object)
388
389
  return qs.order_by("-modified_at")
@@ -404,7 +405,7 @@ class UserTemplates(PaginatorMixin, DetailView, SearchMixin):
404
405
  def get_maps(self):
405
406
  qs = Map.private.filter(is_template=True)
406
407
  search_qs = self.get_search_queryset(qs)
407
- if search_qs is not None:
408
+ if isinstance(search_qs, QuerySet):
408
409
  qs = search_qs
409
410
  qs = qs.for_user(self.object)
410
411
  return qs.order_by("-modified_at")
@@ -1389,8 +1390,21 @@ class PictogramJSONList(ListView):
1389
1390
  model = Pictogram
1390
1391
 
1391
1392
  def render_to_response(self, context, **response_kwargs):
1392
- content = [p.json for p in Pictogram.objects.all()]
1393
- return simple_json_response(pictogram_list=content)
1393
+ if settings.UMAP_PICTOGRAMS_COLLECTIONS:
1394
+ content = collect_pictograms()
1395
+ else:
1396
+ categories = {}
1397
+ for picto in Pictogram.objects.all():
1398
+ category = picto.category or _("Generic")
1399
+ categories.setdefault(category, [])
1400
+ categories[category].append(picto.json)
1401
+ content = {
1402
+ settings.SITE_NAME: {
1403
+ "attribution": settings.SITE_NAME,
1404
+ "categories": categories,
1405
+ }
1406
+ }
1407
+ return simple_json_response(data=content)
1394
1408
 
1395
1409
 
1396
1410
  # ############## #
@@ -1488,10 +1502,10 @@ class TemplateList(ListView):
1488
1502
  source = self.request.GET.get("source")
1489
1503
  if source == "mine":
1490
1504
  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
1505
  elif source == "staff":
1494
1506
  qs = Map.public.starred_by_staff().filter(is_template=True)
1507
+ else:
1508
+ qs = Map.public.filter(is_template=True)
1495
1509
  templates = [
1496
1510
  {
1497
1511
  "id": m.id,
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: umap-project
3
+ Version: 3.2.0
4
+ Summary: Create maps with OpenStreetMap layers in a minute and embed them in your site.
5
+ Author-email: Yohan Boniface <yb@enix.org>
6
+ Maintainer-email: David Larlet <david@larlet.fr>
7
+ License-File: LICENSE
8
+ Keywords: django,geodjango,leaflet,map,openstreetmap
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: django-agnocomplete==2.2.0
20
+ Requires-Dist: django-environ==0.12.0
21
+ Requires-Dist: django-probes==1.7.0
22
+ Requires-Dist: django==5.2.4
23
+ Requires-Dist: pillow==11.3.0
24
+ Requires-Dist: psycopg==3.2.9
25
+ Requires-Dist: rcssmin==1.2.1
26
+ Requires-Dist: requests==2.32.4
27
+ Requires-Dist: rjsmin==1.2.4
28
+ Requires-Dist: social-auth-app-django==5.4.3
29
+ Requires-Dist: social-auth-core==4.5.6
30
+ Provides-Extra: dev
31
+ Requires-Dist: djlint==1.36.4; extra == 'dev'
32
+ Requires-Dist: hatch==1.14.1; extra == 'dev'
33
+ Requires-Dist: isort==6.0.1; extra == 'dev'
34
+ Requires-Dist: mkdocs-material==9.6.15; extra == 'dev'
35
+ Requires-Dist: mkdocs-static-i18n==1.3.0; extra == 'dev'
36
+ Requires-Dist: mkdocs==1.6.1; extra == 'dev'
37
+ Requires-Dist: pymdown-extensions==10.16; extra == 'dev'
38
+ Requires-Dist: ruff==0.12.2; extra == 'dev'
39
+ Requires-Dist: vermin==1.6.0; extra == 'dev'
40
+ Provides-Extra: docker
41
+ Requires-Dist: uvicorn==0.35.0; extra == 'docker'
42
+ Provides-Extra: s3
43
+ Requires-Dist: django-storages[s3]==1.14.6; extra == 's3'
44
+ Provides-Extra: sync
45
+ Requires-Dist: pydantic==2.11.7; extra == 'sync'
46
+ Requires-Dist: redis==6.2.0; extra == 'sync'
47
+ Requires-Dist: websockets==15.0.1; extra == 'sync'
48
+ Provides-Extra: test
49
+ Requires-Dist: daphne==4.2.0; extra == 'test'
50
+ Requires-Dist: factory-boy==3.3.3; extra == 'test'
51
+ Requires-Dist: moto[s3]==5.1.6; extra == 'test'
52
+ Requires-Dist: playwright>=1.39; extra == 'test'
53
+ Requires-Dist: pytest-django==4.11.1; extra == 'test'
54
+ Requires-Dist: pytest-playwright==0.7.0; extra == 'test'
55
+ Requires-Dist: pytest-rerunfailures==15.1; extra == 'test'
56
+ Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'test'
57
+ Requires-Dist: pytest==8.4.1; extra == 'test'
58
+ Description-Content-Type: text/markdown
59
+
60
+ [![Matrix](https://img.shields.io/matrix/umap:matrix.org?server_fqdn=matrix.org&logo=matrix)](https://matrix.to/#/#umap:matrix.org)
61
+ [![Forum](https://img.shields.io/discourse/users?server=https://forum.openstreetmap.fr&label=Forum&logo=discourse)](https://forum.openstreetmap.fr/c/utiliser/umap/29)
62
+ [![Mailing list](https://img.shields.io/badge/Mailing%20list-Subscribe-598dc1?labelColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAZCAYAAAAmNZ4aAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH5AICEi0F3iqxoQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAATBSURBVEjHrZbPb5xHHcY/z+z6R+06cRzWJQGCSgKhxPturSIOwIFSIeCEOHHqMZeqkUoLUiH1rvddlEOFxAn4HxAXblwIoEoICRrlfScFi0PVxo0rQtYBJ3ZiZ3ceDruOYid2kqojzeH9vpr5zPc7zzwzAqjXFySpKinYrgABhA0SgBPgYU+wHZMlp6LomMdseuaZhUq1yglJc5I+a/s48BRo1Pa4pHFwD3zb1qbEJnAbWAU+BD4AroA2JN8Ar9tat30zxs6dPcGNxsIYuAaqpaTLMeZdgPn5xbF+P83YOhyCD4EPpRRmJWrArM00+KCkafA0aEvyNWDNZhVYAXXttCKFFfAV26sxdraG4GYNdAS8YfMtiaWiyP/4qCV79tn2ZL+fJiVPSZqymZaYsTkkMZJS2pDCJvgmcA3oSr33lWWtY+A5yechPGnzKni5LPNf8zG1+fnFkZT6o7bmwWtS/1KwMVADpoqifc3mnK16lrUWPi5wr+fRlPSizaiEoRICpDvAUQgTADG2bwKvgz+fZc2fnDrVDh8V2Gi0lGXNOeBXwFKM+fmUwrwdKiEEtoDDNk9sD4ixvSbpR6DnKpX+mXq9Nfr40OYBm9MS50JIrRjzPwFIZGAFu7IFmgBP3juwKNpXbf8Q+Ibk04+Zad3Wm7a/VK3e+F5RdN4bxj8peVyCYPu27RFb07sniDFftvUK6Pl6vfXyo0CzrPl92z8Gn48xf+Xtt39x11xsnrNZB1KIsd2TSBIHs6yl++Ht9yWfkfzVer314kOgp4Hv2vy8LPPf7P5v+yug67bSUDjqgmd093tnK4r8Q4mXJV5oNFrPPxjaehU4Dn4txrzcw6/mwO9KqR+GK+kCnwFV9sqmKPJV4DXwt7Os+ald0EXbt8oyf70sOzcevO8LRyQfs7VcFB0HgBBYBo7aVPcrZVm2u6BfAl+v18+ODCd8KQR+H+P+hpOSjgPrIbgLDEA27wLTDwM3Gm8crFRuXen1xt+SQi3LmjWJ3/b7Xn+Y6ELwF22t2fovQJifPzsGlcugCYmJfc7lnK3DFy68mcoyX+n3fX1rqxcvXsyvSkxnWfPk3n6+WLE1ByzZDMAphcmybF21XQVN7qHWF2xGUqq+tx17552f3VpaOpcGW5BfAcbr9daXHzTeTjOgI7bKGNtbAKEoOqtDR+kCUw9Q63dAG7bKS5daae9ieknygUajdfJ+sJ4CT4D/ebf0gzL+NEh6C3xslwN9Dbhj+28xtvv7C6+zKRFtvlCvNz+xE8yngbUQvLQDXBTnUkr+O+jYPZmetF2z019izHuP4lpFkf9H4nIIZFnW2lb9iOSnbV0sis76DjBApbJxQfKTQ+hhm8/Z4c8xdm49jk8XRbuwCcCw5GEMNAv+ww4r2SWiH0D4ne0TkpbLcvF/H/1KbH4TKEHJ9pmyzNv7gBeOQhgdmInHgVmJGYnplDwlhRFwFSzQFrBp+3oIWrXpAv+WWC6KfGVYuRPDl2mtLNt/3RM8WOnZgymFGUmzwCHgwFDtB+65Pp8AxobdkoOtChAkG3TH5l9AtP2PEFguik5/XzDAqVNvqFIR0mAa2wohkJKlwUNbkofjhZ2242GQoGSTJPopuR9j575j+H+W9nlG4uJsnAAAAABJRU5ErkJggg==)](https://lists.openstreetmap.org/listinfo/umap)
63
+
64
+ [![Liberapay](https://img.shields.io/liberapay/patrons/uMap.svg?logo=liberapay)](https://liberapay.com/uMap)
65
+ [![OpenCollective](https://img.shields.io/opencollective/all/uMap?logo=opencollective)](https://opencollective.com/uMap)
66
+ [![GitHub Sponsors](https://img.shields.io/github/sponsors/umap-project?logo=github)](https://github.com/sponsors/umap-project)
67
+
68
+ # uMap project
69
+
70
+ uMap lets you create maps with OpenStreetMap layers within minutes and embed them in your site.
71
+ *Because we think that the more OSM will be used, the more OSM will be improved.*
72
+ Built on top of Django and Leaflet.
73
+
74
+ - Have a look at [our website](https://umap-project.org) for an introduction
75
+ - See [our docs](https://docs.umap-project.org/) for technical information
76
+ - Come [chat with us on matrix](https://matrix.to/#/#umap:matrix.org), start a thread on [the forum](https://forum.openstreetmap.fr/c/utiliser/umap/29) or join [the mailing list](https://lists.openstreetmap.org/listinfo/umap)