umap-project 2.0.4__py3-none-any.whl → 2.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 (106) hide show
  1. umap/__init__.py +1 -1
  2. umap/fields.py +3 -1
  3. umap/locale/br/LC_MESSAGES/django.po +76 -71
  4. umap/locale/en/LC_MESSAGES/django.po +41 -41
  5. umap/locale/hu/LC_MESSAGES/django.po +42 -42
  6. umap/locale/it/LC_MESSAGES/django.po +64 -58
  7. umap/locale/ms/LC_MESSAGES/django.po +62 -57
  8. umap/migrations/0018_datalayer_uuid.py +62 -0
  9. umap/migrations/0019_migrate_internal_remote_datalayers.py +52 -0
  10. umap/models.py +20 -3
  11. umap/settings/base.py +1 -0
  12. umap/settings/dev.py +1 -0
  13. umap/static/umap/js/modules/browser.js +2 -2
  14. umap/static/umap/js/modules/global.js +14 -4
  15. umap/static/umap/js/modules/i18n.js +35 -0
  16. umap/static/umap/js/modules/leaflet-configure.js +7 -0
  17. umap/static/umap/js/modules/schema.js +388 -0
  18. umap/static/umap/js/modules/urls.js +17 -2
  19. umap/static/umap/js/modules/utils.js +24 -0
  20. umap/static/umap/js/umap.controls.js +9 -10
  21. umap/static/umap/js/umap.core.js +5 -5
  22. umap/static/umap/js/umap.features.js +23 -9
  23. umap/static/umap/js/umap.forms.js +49 -299
  24. umap/static/umap/js/umap.icon.js +2 -2
  25. umap/static/umap/js/umap.js +26 -129
  26. umap/static/umap/js/umap.layer.js +9 -9
  27. umap/static/umap/js/umap.popup.js +3 -0
  28. umap/static/umap/js/umap.share.js +1 -1
  29. umap/static/umap/locale/am_ET.json +229 -225
  30. umap/static/umap/locale/ar.json +229 -225
  31. umap/static/umap/locale/ast.json +229 -225
  32. umap/static/umap/locale/bg.json +229 -225
  33. umap/static/umap/locale/br.json +237 -233
  34. umap/static/umap/locale/ca.json +229 -225
  35. umap/static/umap/locale/cs_CZ.json +229 -225
  36. umap/static/umap/locale/da.json +229 -225
  37. umap/static/umap/locale/de.json +229 -225
  38. umap/static/umap/locale/el.json +229 -225
  39. umap/static/umap/locale/en.json +230 -233
  40. umap/static/umap/locale/en_US.json +229 -225
  41. umap/static/umap/locale/es.json +229 -225
  42. umap/static/umap/locale/et.json +229 -225
  43. umap/static/umap/locale/eu.json +226 -198
  44. umap/static/umap/locale/fa_IR.json +229 -225
  45. umap/static/umap/locale/fi.json +229 -225
  46. umap/static/umap/locale/fr.json +229 -232
  47. umap/static/umap/locale/gl.json +229 -225
  48. umap/static/umap/locale/he.json +229 -225
  49. umap/static/umap/locale/hr.json +229 -225
  50. umap/static/umap/locale/hu.json +229 -232
  51. umap/static/umap/locale/id.json +229 -225
  52. umap/static/umap/locale/is.json +229 -225
  53. umap/static/umap/locale/it.json +229 -232
  54. umap/static/umap/locale/ja.json +229 -225
  55. umap/static/umap/locale/ko.json +229 -225
  56. umap/static/umap/locale/lt.json +229 -225
  57. umap/static/umap/locale/ms.json +229 -232
  58. umap/static/umap/locale/nl.json +232 -228
  59. umap/static/umap/locale/no.json +229 -225
  60. umap/static/umap/locale/pl.json +229 -225
  61. umap/static/umap/locale/pl_PL.json +229 -225
  62. umap/static/umap/locale/pt.json +229 -225
  63. umap/static/umap/locale/pt_BR.json +229 -225
  64. umap/static/umap/locale/pt_PT.json +229 -225
  65. umap/static/umap/locale/ro.json +229 -225
  66. umap/static/umap/locale/ru.json +229 -225
  67. umap/static/umap/locale/sk_SK.json +229 -225
  68. umap/static/umap/locale/sl.json +229 -225
  69. umap/static/umap/locale/sr.json +229 -225
  70. umap/static/umap/locale/sv.json +229 -225
  71. umap/static/umap/locale/th_TH.json +229 -225
  72. umap/static/umap/locale/tr.json +229 -225
  73. umap/static/umap/locale/uk_UA.json +229 -225
  74. umap/static/umap/locale/vi.json +229 -225
  75. umap/static/umap/locale/vi_VN.json +229 -225
  76. umap/static/umap/locale/zh.json +229 -225
  77. umap/static/umap/locale/zh_CN.json +229 -225
  78. umap/static/umap/locale/zh_TW.Big5.json +229 -225
  79. umap/static/umap/locale/zh_TW.json +229 -232
  80. umap/static/umap/test/index.html +0 -2
  81. umap/static/umap/{test → unittests}/URLs.js +5 -0
  82. umap/static/umap/vendors/leaflet/leaflet-src.esm.js +7064 -7064
  83. umap/static/umap/vendors/photon/leaflet.photon.js +3 -0
  84. umap/templates/umap/js.html +8 -6
  85. umap/templatetags/umap_tags.py +3 -2
  86. umap/tests/integration/test_browser.py +40 -0
  87. umap/tests/integration/test_collaborative_editing.py +72 -3
  88. umap/tests/integration/test_export_map.py +226 -9
  89. umap/tests/integration/test_features_id_generation.py +51 -0
  90. umap/tests/integration/test_owned_map.py +14 -1
  91. umap/tests/integration/test_statics.py +3 -3
  92. umap/tests/integration/test_tilelayer.py +3 -3
  93. umap/tests/settings.py +3 -3
  94. umap/tests/test_datalayer_views.py +67 -20
  95. umap/tests/test_map_views.py +20 -0
  96. umap/tests/test_merge_features.py +25 -5
  97. umap/urls.py +12 -12
  98. umap/utils.py +7 -0
  99. umap/views.py +56 -47
  100. umap/wsgi.py +1 -0
  101. {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/METADATA +9 -9
  102. {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/RECORD +105 -99
  103. umap/static/umap/test/Map.Export.js +0 -106
  104. {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/WHEEL +0 -0
  105. {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/entry_points.txt +0 -0
  106. {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -35,7 +35,7 @@ def test_get_with_public_mode(client, settings, datalayer, map):
35
35
  url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
36
36
  response = client.get(url)
37
37
  assert response.status_code == 200
38
- assert response["Last-Modified"] is not None
38
+ assert response["X-Datalayer-Version"] is not None
39
39
  assert response["Cache-Control"] is not None
40
40
  assert "Content-Encoding" not in response
41
41
  j = json.loads(response.content.decode())
@@ -111,7 +111,7 @@ def test_update(client, datalayer, map, post_data):
111
111
  # Test response is a json
112
112
  j = json.loads(response.content.decode())
113
113
  assert "id" in j
114
- assert datalayer.pk == j["id"]
114
+ assert str(datalayer.pk) == j["id"]
115
115
  assert j["browsable"] is True
116
116
  assert Path(modified_datalayer.geojson.path).exists()
117
117
 
@@ -154,48 +154,50 @@ def test_should_not_be_possible_to_delete_with_wrong_map_id_in_url(
154
154
  assert DataLayer.objects.filter(pk=datalayer.pk).exists()
155
155
 
156
156
 
157
- def test_optimistic_concurrency_control_with_good_last_modified(
157
+ def test_optimistic_concurrency_control_with_good_version(
158
158
  client, datalayer, map, post_data
159
159
  ):
160
160
  map.share_status = Map.PUBLIC
161
161
  map.save()
162
- # Get Last-Modified
162
+ # Get reference version
163
163
  url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
164
164
  response = client.get(url)
165
- last_modified = response["Last-Modified"]
165
+ reference_version = response["X-Datalayer-Version"]
166
166
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
167
167
  client.login(username=map.owner.username, password="123123")
168
168
  name = "new name"
169
169
  post_data["name"] = "new name"
170
170
  response = client.post(
171
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=last_modified
171
+ url, post_data, follow=True, HTTP_X_DATALAYER_REFERENCE=reference_version
172
172
  )
173
173
  assert response.status_code == 200
174
174
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
175
175
  assert modified_datalayer.name == name
176
176
 
177
177
 
178
- def test_optimistic_concurrency_control_with_bad_last_modified(
178
+ def test_optimistic_concurrency_control_with_bad_version(
179
179
  client, datalayer, map, post_data
180
180
  ):
181
181
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
182
182
  client.login(username=map.owner.username, password="123123")
183
183
  name = "new name"
184
184
  post_data["name"] = name
185
- response = client.post(url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE="xxx")
185
+ response = client.post(
186
+ url, post_data, follow=True, HTTP_X_DATALAYER_REFERENCE="xxx"
187
+ )
186
188
  assert response.status_code == 412
187
189
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
188
190
  assert modified_datalayer.name != name
189
191
 
190
192
 
191
- def test_optimistic_concurrency_control_with_empty_last_modified(
193
+ def test_optimistic_concurrency_control_with_empty_version(
192
194
  client, datalayer, map, post_data
193
195
  ):
194
196
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
195
197
  client.login(username=map.owner.username, password="123123")
196
198
  name = "new name"
197
199
  post_data["name"] = name
198
- response = client.post(url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=None)
200
+ response = client.post(url, post_data, follow=True, X_DATALAYER_REFERENCE=None)
199
201
  assert response.status_code == 200
200
202
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
201
203
  assert modified_datalayer.name == name
@@ -225,6 +227,41 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
225
227
  assert version in versions["versions"]
226
228
 
227
229
 
230
+ def test_versions_can_return_old_format(client, datalayer, map, settings):
231
+ map.share_status = Map.PUBLIC
232
+ map.save()
233
+ root = datalayer.storage_root()
234
+ datalayer.old_id = 123 # old datalayer id (now replaced by uuid)
235
+ datalayer.save()
236
+
237
+ datalayer.geojson.storage.save(
238
+ "%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
239
+ )
240
+ datalayer.geojson.storage.save(
241
+ "%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
242
+ )
243
+
244
+ # store with the id prefix (rather than the uuid)
245
+ old_format_version = "%s_1440918637.geojson" % datalayer.old_id
246
+ datalayer.geojson.storage.save(
247
+ ("%s/" % root) + old_format_version, ContentFile("{}")
248
+ )
249
+
250
+ url = reverse("datalayer_versions", args=(map.pk, datalayer.pk))
251
+ versions = json.loads(client.get(url).content.decode())
252
+ assert len(versions["versions"]) == 4
253
+ version = {
254
+ "name": old_format_version,
255
+ "size": 2,
256
+ "at": "1440918637",
257
+ }
258
+ assert version in versions["versions"]
259
+
260
+ client.get(
261
+ reverse("datalayer_version", args=(map.pk, datalayer.pk, old_format_version))
262
+ )
263
+
264
+
228
265
  def test_version_should_return_one_version_geojson(client, datalayer, map):
229
266
  map.share_status = Map.PUBLIC
230
267
  map.save()
@@ -444,7 +481,7 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
444
481
  assert response.status_code == 200
445
482
 
446
483
  response = client.get(reverse("datalayer_view", args=(map.pk, datalayer.pk)))
447
- reference_timestamp = response["Last-Modified"]
484
+ reference_version = response.headers.get("X-Datalayer-Version")
448
485
 
449
486
  # Client 1 adds "Point 5, 6" to the existing data
450
487
  client1_feature = {
@@ -454,14 +491,16 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
454
491
  }
455
492
  client1_data = deepcopy(reference_data)
456
493
  client1_data["features"].append(client1_feature)
457
- # Sleep to change the current timestamp (used in the If-Unmodified-Since header)
458
- time.sleep(1)
494
+
459
495
  post_data["geojson"] = SimpleUploadedFile(
460
496
  "foo.json",
461
497
  json.dumps(client1_data).encode("utf-8"),
462
498
  )
463
499
  response = client.post(
464
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
500
+ url,
501
+ post_data,
502
+ follow=True,
503
+ headers={"X-Datalayer-Reference": reference_version},
465
504
  )
466
505
  assert response.status_code == 200
467
506
 
@@ -479,7 +518,10 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
479
518
  json.dumps(client2_data).encode("utf-8"),
480
519
  )
481
520
  response = client.post(
482
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
521
+ url,
522
+ post_data,
523
+ follow=True,
524
+ headers={"X-Datalayer-Reference": reference_version},
483
525
  )
484
526
  assert response.status_code == 200
485
527
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
@@ -513,20 +555,22 @@ def test_optimistic_merge_conflicting_change_raises(
513
555
  assert response.status_code == 200
514
556
 
515
557
  response = client.get(reverse("datalayer_view", args=(map.pk, datalayer.pk)))
516
- reference_timestamp = response["Last-Modified"]
558
+
559
+ reference_version = response.headers.get("X-Datalayer-Version")
517
560
 
518
561
  # First client changes the first feature.
519
562
  client1_data = deepcopy(reference_data)
520
563
  client1_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [5, 6]}
521
564
 
522
- # Sleep to change the current timestamp (used in the If-Unmodified-Since header)
523
- time.sleep(1)
524
565
  post_data["geojson"] = SimpleUploadedFile(
525
566
  "foo.json",
526
567
  json.dumps(client1_data).encode("utf-8"),
527
568
  )
528
569
  response = client.post(
529
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
570
+ url,
571
+ post_data,
572
+ follow=True,
573
+ headers={"X-Datalayer-Reference": reference_version},
530
574
  )
531
575
  assert response.status_code == 200
532
576
 
@@ -539,7 +583,10 @@ def test_optimistic_merge_conflicting_change_raises(
539
583
  json.dumps(client2_data).encode("utf-8"),
540
584
  )
541
585
  response = client.post(
542
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
586
+ url,
587
+ post_data,
588
+ follow=True,
589
+ headers={"X-Datalayer-Reference": reference_version},
543
590
  )
544
591
  assert response.status_code == 412
545
592
 
@@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
7
7
  from django.core import mail
8
8
  from django.core.signing import Signer
9
9
  from django.urls import reverse
10
+ from django.utils import translation
10
11
 
11
12
  from umap.models import DataLayer, Map, Star
12
13
 
@@ -147,6 +148,13 @@ def test_should_not_consider_the_query_string_for_canonical_check(client, map):
147
148
  assert response.status_code == 200
148
149
 
149
150
 
151
+ def test_map_headers(client, map):
152
+ url = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
153
+ response = client.get(url)
154
+ assert response.status_code == 200
155
+ assert response.headers["Access-Control-Allow-Origin"] == "*"
156
+
157
+
150
158
  def test_short_url_should_redirect_to_canonical(client, map):
151
159
  url = reverse("map_short_url", kwargs={"pk": map.pk})
152
160
  canonical = reverse("map", kwargs={"map_id": map.pk, "slug": map.slug})
@@ -803,6 +811,7 @@ def test_oembed_map(client, map, datalayer):
803
811
  url = f"{reverse('map_oembed')}?url=http://testserver{map.get_absolute_url()}"
804
812
  response = client.get(url)
805
813
  assert response.status_code == 200
814
+ assert response.headers["Access-Control-Allow-Origin"] == "*"
806
815
  j = json.loads(response.content.decode())
807
816
  assert j["type"] == "rich"
808
817
  assert j["version"] == "1.0"
@@ -815,6 +824,17 @@ def test_oembed_map(client, map, datalayer):
815
824
  )
816
825
 
817
826
 
827
+ def test_oembed_map_with_non_default_language(client, map, datalayer):
828
+ translation.activate("en")
829
+ path = map.get_absolute_url()
830
+ assert path.startswith("/en/")
831
+ path = path.replace("/en/", "/fr/")
832
+ url = f"{reverse('map_oembed')}?url=http://testserver{path}"
833
+ response = client.get(url)
834
+ assert response.status_code == 200
835
+ translation.activate("en")
836
+
837
+
818
838
  def test_oembed_link(client, map, datalayer):
819
839
  response = client.get(map.get_absolute_url())
820
840
  assert response.status_code == 200
@@ -1,6 +1,6 @@
1
1
  import pytest
2
2
 
3
- from umap.utils import merge_features
3
+ from umap.utils import ConflictError, merge_features
4
4
 
5
5
 
6
6
  def test_adding_one_element():
@@ -50,18 +50,38 @@ def test_removing_same_element():
50
50
 
51
51
 
52
52
  def test_removing_changed_element():
53
- with pytest.raises(ValueError):
53
+ with pytest.raises(ConflictError):
54
54
  merge_features(["A", "B"], ["A", "C"], ["A"])
55
55
 
56
56
 
57
57
  def test_changing_removed_element():
58
- with pytest.raises(ValueError):
58
+ with pytest.raises(ConflictError):
59
59
  merge_features(["A", "B"], ["A"], ["A", "C"])
60
60
 
61
61
 
62
62
  def test_changing_same_element():
63
- with pytest.raises(ValueError):
63
+ with pytest.raises(ConflictError):
64
64
  merge_features(["A", "B"], ["A", "D"], ["A", "C"])
65
65
  # Order does not count
66
- with pytest.raises(ValueError):
66
+ with pytest.raises(ConflictError):
67
67
  merge_features(["A", "B", "C"], ["B", "D", "A"], ["A", "E", "B"])
68
+
69
+
70
+ def test_merge_with_ids_raises():
71
+ # If reference doesn't have ids, but latest and incoming has
72
+ # We need to raise as a conflict
73
+ reference = [
74
+ {"properties": {}, "geometry": "A"},
75
+ {"properties": {}, "geometry": "B"},
76
+ ]
77
+ latest = [
78
+ {"properties": {id: "100"}, "geometry": "A"},
79
+ {"properties": {id: "101"}, "geometry": "B"},
80
+ ]
81
+ incoming = [
82
+ {"properties": {id: "200"}, "geometry": "A"},
83
+ {"properties": {id: "201"}, "geometry": "B"},
84
+ ]
85
+
86
+ with pytest.raises(ConflictError):
87
+ merge_features(reference, latest, incoming)
umap/urls.py CHANGED
@@ -72,18 +72,18 @@ i18n_urls = [
72
72
  ]
73
73
  i18n_urls += decorated_patterns(
74
74
  [can_view_map, cache_control(must_revalidate=True)],
75
- re_path(
76
- r"^datalayer/(?P<map_id>\d+)/(?P<pk>[\d]+)/$",
75
+ path(
76
+ "datalayer/<int:map_id>/<uuid:pk>/",
77
77
  views.DataLayerView.as_view(),
78
78
  name="datalayer_view",
79
79
  ),
80
- re_path(
81
- r"^datalayer/(?P<map_id>\d+)/(?P<pk>[\d]+)/versions/$",
80
+ path(
81
+ "datalayer/<int:map_id>/<uuid:pk>/versions/",
82
82
  views.DataLayerVersions.as_view(),
83
83
  name="datalayer_versions",
84
84
  ),
85
- re_path(
86
- r"^datalayer/(?P<map_id>\d+)/(?P<pk>[\d]+)/(?P<name>[_\w]+.geojson)$",
85
+ path(
86
+ "datalayer/<int:map_id>/<uuid:pk>/<str:name>",
87
87
  views.DataLayerVersion.as_view(),
88
88
  name="datalayer_version",
89
89
  ),
@@ -145,13 +145,13 @@ map_urls = [
145
145
  views.DataLayerCreate.as_view(),
146
146
  name="datalayer_create",
147
147
  ),
148
- re_path(
149
- r"^map/(?P<map_id>[\d]+)/datalayer/delete/(?P<pk>\d+)/$",
148
+ path(
149
+ "map/<int:map_id>/datalayer/delete/<uuid:pk>/",
150
150
  views.DataLayerDelete.as_view(),
151
151
  name="datalayer_delete",
152
152
  ),
153
- re_path(
154
- r"^map/(?P<map_id>[\d]+)/datalayer/permissions/(?P<pk>\d+)/$",
153
+ path(
154
+ "map/<int:map_id>/datalayer/permissions/<uuid:pk>/",
155
155
  views.UpdateDataLayerPermissions.as_view(),
156
156
  name="datalayer_permissions",
157
157
  ),
@@ -165,8 +165,8 @@ if settings.DEFAULT_FROM_EMAIL:
165
165
  )
166
166
  )
167
167
  datalayer_urls = [
168
- re_path(
169
- r"^map/(?P<map_id>[\d]+)/datalayer/update/(?P<pk>\d+)/$",
168
+ path(
169
+ "map/<int:map_id>/datalayer/update/<uuid:pk>/",
170
170
  views.DataLayerUpdate.as_view(),
171
171
  name="datalayer_update",
172
172
  ),
umap/utils.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import gzip
2
+ import json
2
3
  import os
3
4
 
4
5
  from django.conf import settings
6
+ from django.core.serializers.json import DjangoJSONEncoder
5
7
  from django.urls import URLPattern, URLResolver, get_resolver
6
8
 
7
9
 
@@ -162,3 +164,8 @@ def merge_features(reference: list, latest: list, incoming: list):
162
164
  merged.append(item)
163
165
 
164
166
  return merged
167
+
168
+
169
+ def json_dumps(obj, **kwargs):
170
+ """Utility using the Django JSON Encoder when dumping objects"""
171
+ return json.dumps(obj, cls=DjangoJSONEncoder, **kwargs)
umap/views.py CHANGED
@@ -38,11 +38,11 @@ from django.http import (
38
38
  from django.middleware.gzip import re_accepts_gzip
39
39
  from django.shortcuts import get_object_or_404
40
40
  from django.urls import resolve, reverse, reverse_lazy
41
+ from django.utils import translation
41
42
  from django.utils.encoding import smart_bytes
42
43
  from django.utils.http import http_date
43
44
  from django.utils.timezone import make_aware
44
45
  from django.utils.translation import gettext as _
45
- from django.utils.translation import to_locale
46
46
  from django.views.decorators.cache import cache_control
47
47
  from django.views.decorators.http import require_GET
48
48
  from django.views.generic import DetailView, TemplateView, View
@@ -67,7 +67,14 @@ from .forms import (
67
67
  UserProfileForm,
68
68
  )
69
69
  from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer
70
- from .utils import ConflictError, _urls_for_js, gzip_file, is_ajax, merge_features
70
+ from .utils import (
71
+ ConflictError,
72
+ _urls_for_js,
73
+ gzip_file,
74
+ is_ajax,
75
+ json_dumps,
76
+ merge_features,
77
+ )
71
78
 
72
79
  User = get_user_model()
73
80
 
@@ -315,14 +322,14 @@ class UserDownload(DetailView, SearchMixin):
315
322
  with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
316
323
  for map_ in self.get_maps():
317
324
  umapjson = map_.generate_umapjson(self.request)
318
- geojson_file = io.StringIO(json.dumps(umapjson))
325
+ geojson_file = io.StringIO(json_dumps(umapjson))
319
326
  file_name = f"umap_backup_{map_.slug}_{map_.pk}.umap"
320
327
  zip_file.writestr(file_name, geojson_file.getvalue())
321
328
 
322
329
  response = HttpResponse(zip_buffer.getvalue(), content_type="application/zip")
323
- response[
324
- "Content-Disposition"
325
- ] = 'attachment; filename="umap_backup_complete.zip"'
330
+ response["Content-Disposition"] = (
331
+ 'attachment; filename="umap_backup_complete.zip"'
332
+ )
326
333
  return response
327
334
 
328
335
 
@@ -354,7 +361,7 @@ class MapsShowCase(View):
354
361
  }
355
362
 
356
363
  geojson = {"type": "FeatureCollection", "features": [make(m) for m in maps]}
357
- return HttpResponse(smart_bytes(json.dumps(geojson)))
364
+ return HttpResponse(smart_bytes(json_dumps(geojson)))
358
365
 
359
366
 
360
367
  showcase = MapsShowCase.as_view()
@@ -441,7 +448,7 @@ ajax_proxy = AjaxProxy.as_view()
441
448
 
442
449
 
443
450
  def simple_json_response(**kwargs):
444
- return HttpResponse(json.dumps(kwargs), content_type="application/json")
451
+ return HttpResponse(json_dumps(kwargs), content_type="application/json")
445
452
 
446
453
 
447
454
  # ############## #
@@ -488,7 +495,7 @@ class MapDetailMixin:
488
495
  "urls": _urls_for_js(),
489
496
  "tilelayers": TileLayer.get_list(),
490
497
  "editMode": self.edit_mode,
491
- "default_iconUrl": "%sumap/img/marker.svg" % settings.STATIC_URL, # noqa
498
+ "schema": Map.extra_schema,
492
499
  "umap_id": self.get_umap_id(),
493
500
  "starred": self.is_starred(),
494
501
  "licences": dict((l.name, l.json) for l in Licence.objects.all()),
@@ -529,7 +536,7 @@ class MapDetailMixin:
529
536
  if hasattr(self.request, "LANGUAGE_CODE"):
530
537
  lang = self.request.LANGUAGE_CODE
531
538
  properties["lang"] = lang
532
- locale = to_locale(lang)
539
+ locale = translation.to_locale(lang)
533
540
  properties["locale"] = locale
534
541
  context["locale"] = locale
535
542
  geojson = self.get_geojson()
@@ -537,7 +544,7 @@ class MapDetailMixin:
537
544
  geojson["properties"] = {}
538
545
  geojson["properties"].update(properties)
539
546
  geojson["properties"]["datalayers"] = self.get_datalayers()
540
- context["map_settings"] = json.dumps(geojson, indent=settings.DEBUG)
547
+ context["map_settings"] = json_dumps(geojson, indent=settings.DEBUG)
541
548
  self.set_preconnect(geojson["properties"], context)
542
549
  return context
543
550
 
@@ -608,7 +615,9 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
608
615
  if request.META.get("QUERY_STRING"):
609
616
  canonical = "?".join([canonical, request.META["QUERY_STRING"]])
610
617
  return HttpResponsePermanentRedirect(canonical)
611
- return super(MapView, self).get(request, *args, **kwargs)
618
+ response = super(MapView, self).get(request, *args, **kwargs)
619
+ response["Access-Control-Allow-Origin"] = "*"
620
+ return response
612
621
 
613
622
  def get_canonical_url(self):
614
623
  return self.object.get_absolute_url()
@@ -666,9 +675,9 @@ class MapDownload(DetailView):
666
675
  def render_to_response(self, context, *args, **kwargs):
667
676
  umapjson = self.object.generate_umapjson(self.request)
668
677
  response = simple_json_response(**umapjson)
669
- response[
670
- "Content-Disposition"
671
- ] = f'attachment; filename="umap_backup_{self.object.slug}.umap"'
678
+ response["Content-Disposition"] = (
679
+ f'attachment; filename="umap_backup_{self.object.slug}.umap"'
680
+ )
672
681
  return response
673
682
 
674
683
 
@@ -692,6 +701,8 @@ class MapOEmbed(View):
692
701
  raise Http404("Host not allowed.")
693
702
 
694
703
  url_path = parsed_url.path
704
+ lang = translation.get_language_from_path(url_path)
705
+ translation.activate(lang)
695
706
  view, args, kwargs = resolve(url_path)
696
707
  if "slug" not in kwargs or "map_id" not in kwargs:
697
708
  raise Http404("Invalid URL path.")
@@ -715,7 +726,9 @@ class MapOEmbed(View):
715
726
  f'<p><a href="//{netloc}{map_url}">{label}</a></p>'
716
727
  )
717
728
  data["html"] = html
718
- return simple_json_response(**data)
729
+ response = simple_json_response(**data)
730
+ response["Access-Control-Allow-Origin"] = "*"
731
+ return response
719
732
 
720
733
 
721
734
  class MapViewGeoJSON(MapView):
@@ -962,20 +975,20 @@ class GZipMixin(object):
962
975
 
963
976
  @property
964
977
  def path(self):
965
- return self.object.geojson.path
978
+ return Path(self.object.geojson.path)
966
979
 
967
980
  @property
968
981
  def gzip_path(self):
969
982
  return Path(f"{self.path}{self.EXT}")
970
983
 
971
- def compute_last_modified(self, path):
972
- stat = os.stat(path)
973
- return http_date(stat.st_mtime)
984
+ def read_version(self, path):
985
+ # Remove optional .gz, then .geojson, then return the trailing version from path.
986
+ return str(path.with_suffix("").with_suffix("")).split("_")[-1]
974
987
 
975
988
  @property
976
- def last_modified(self):
989
+ def version(self):
977
990
  # Prior to 1.3.0 we did not set gzip mtime as geojson mtime,
978
- # but we switched from If-Match header to IF-Unmodified-Since
991
+ # but we switched from If-Match header to If-Unmodified-Since
979
992
  # and when users accepts gzip their last modified value is the gzip
980
993
  # (when umap is served by nginx and X-Accel-Redirect)
981
994
  # one, so we need to compare with that value in that case.
@@ -985,7 +998,7 @@ class GZipMixin(object):
985
998
  if self.accepts_gzip and self.gzip_path.exists()
986
999
  else self.path
987
1000
  )
988
- return self.compute_last_modified(path)
1001
+ return self.read_version(path)
989
1002
 
990
1003
  @property
991
1004
  def accepts_gzip(self):
@@ -1016,7 +1029,7 @@ class DataLayerView(GZipMixin, BaseDetailView):
1016
1029
  with open(path, "rb") as f:
1017
1030
  # Should not be used in production!
1018
1031
  response = HttpResponse(f.read(), content_type="application/geo+json")
1019
- response["Last-Modified"] = self.last_modified
1032
+ response["X-Datalayer-Version"] = self.version
1020
1033
  response["Content-Length"] = statobj.st_size
1021
1034
  return response
1022
1035
 
@@ -1024,9 +1037,8 @@ class DataLayerView(GZipMixin, BaseDetailView):
1024
1037
  class DataLayerVersion(DataLayerView):
1025
1038
  @property
1026
1039
  def path(self):
1027
- return "{root}/{path}".format(
1028
- root=settings.MEDIA_ROOT,
1029
- path=self.object.get_version_path(self.kwargs["name"]),
1040
+ return Path(settings.MEDIA_ROOT) / self.object.get_version_path(
1041
+ self.kwargs["name"]
1030
1042
  )
1031
1043
 
1032
1044
 
@@ -1037,11 +1049,11 @@ class DataLayerCreate(FormLessEditMixin, GZipMixin, CreateView):
1037
1049
  def form_valid(self, form):
1038
1050
  form.instance.map = self.kwargs["map_inst"]
1039
1051
  self.object = form.save()
1040
- # Simple response with only metadatas (including new id)
1052
+ # Simple response with only metadata (including new id)
1041
1053
  response = simple_json_response(
1042
1054
  **self.object.metadata(self.request.user, self.request)
1043
1055
  )
1044
- response["Last-Modified"] = self.last_modified
1056
+ response["X-Datalayer-Version"] = self.version
1045
1057
  return response
1046
1058
 
1047
1059
 
@@ -1049,30 +1061,29 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
1049
1061
  model = DataLayer
1050
1062
  form_class = DataLayerForm
1051
1063
 
1052
- def has_been_modified_since(self, if_unmodified_since):
1053
- return if_unmodified_since and self.last_modified != if_unmodified_since
1064
+ def has_changes_since(self, incoming_version):
1065
+ return incoming_version and self.version != incoming_version
1054
1066
 
1055
- def merge(self, if_unmodified_since):
1067
+ def merge(self, reference_version):
1056
1068
  """
1057
- Attempt to apply the incoming changes to the document the client was using, and
1058
- then merge it with the last document we have on storage.
1069
+ Attempt to apply the incoming changes to the reference, and then merge it
1070
+ with the last document we have on storage.
1059
1071
 
1060
1072
  Returns either None (if the merge failed) or the merged python GeoJSON object.
1061
1073
  """
1062
1074
 
1063
- # Use If-Modified-Since to find the correct version in our storage.
1075
+ # Use the provided info to find the correct version in our storage.
1064
1076
  for name in self.object.get_versions():
1065
- path = os.path.join(settings.MEDIA_ROOT, self.object.get_version_path(name))
1066
- if if_unmodified_since == self.compute_last_modified(path):
1077
+ path = Path(settings.MEDIA_ROOT) / self.object.get_version_path(name)
1078
+ if reference_version == self.read_version(path):
1067
1079
  with open(path) as f:
1068
1080
  reference = json.loads(f.read())
1069
1081
  break
1070
1082
  else:
1071
1083
  # If the document is not found, we can't merge.
1072
1084
  return None
1073
-
1074
1085
  # New data received in the request.
1075
- entrant = json.loads(self.request.FILES["geojson"].read())
1086
+ incoming = json.loads(self.request.FILES["geojson"].read())
1076
1087
 
1077
1088
  # Latest known version of the data.
1078
1089
  with open(self.path) as f:
@@ -1082,7 +1093,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
1082
1093
  merged_features = merge_features(
1083
1094
  reference.get("features", []),
1084
1095
  latest.get("features", []),
1085
- entrant.get("features", []),
1096
+ incoming.get("features", []),
1086
1097
  )
1087
1098
  latest["features"] = merged_features
1088
1099
  return latest
@@ -1097,16 +1108,15 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
1097
1108
  if not self.object.can_edit(user=self.request.user, request=self.request):
1098
1109
  return HttpResponseForbidden()
1099
1110
 
1100
- ius_header = self.request.META.get("HTTP_IF_UNMODIFIED_SINCE")
1101
-
1102
- if self.has_been_modified_since(ius_header):
1103
- merged = self.merge(ius_header)
1111
+ reference_version = self.request.headers.get("X-Datalayer-Reference")
1112
+ if self.has_changes_since(reference_version):
1113
+ merged = self.merge(reference_version)
1104
1114
  if not merged:
1105
1115
  return HttpResponse(status=412)
1106
1116
 
1107
1117
  # Replace the uploaded file by the merged version.
1108
1118
  self.request.FILES["geojson"].file = BytesIO(
1109
- json.dumps(merged).encode("utf-8")
1119
+ json_dumps(merged).encode("utf-8")
1110
1120
  )
1111
1121
 
1112
1122
  # Mark the data to be reloaded by form_valid
@@ -1120,8 +1130,7 @@ class DataLayerUpdate(FormLessEditMixin, GZipMixin, UpdateView):
1120
1130
  data["geojson"] = json.loads(self.object.geojson.read().decode())
1121
1131
  self.request.session["needs_reload"] = False
1122
1132
  response = simple_json_response(**data)
1123
-
1124
- response["Last-Modified"] = self.last_modified
1133
+ response["X-Datalayer-Version"] = self.version
1125
1134
  return response
1126
1135
 
1127
1136
 
umap/wsgi.py CHANGED
@@ -13,6 +13,7 @@ middleware here, or combine a Django application with an application of another
13
13
  framework.
14
14
 
15
15
  """
16
+
16
17
  import os
17
18
 
18
19
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "umap.settings")