umap-project 2.0.4__py3-none-any.whl → 2.1.1__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.
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 +77 -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 +58 -49
  100. umap/wsgi.py +1 -0
  101. {umap_project-2.0.4.dist-info → umap_project-2.1.1.dist-info}/METADATA +9 -9
  102. {umap_project-2.0.4.dist-info → umap_project-2.1.1.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.1.dist-info}/WHEEL +0 -0
  105. {umap_project-2.0.4.dist-info → umap_project-2.1.1.dist-info}/entry_points.txt +0 -0
  106. {umap_project-2.0.4.dist-info → umap_project-2.1.1.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())
@@ -44,6 +44,16 @@ def test_get_with_public_mode(client, settings, datalayer, map):
44
44
  assert j["type"] == "FeatureCollection"
45
45
 
46
46
 
47
+ def test_get_with_x_accel_redirect(client, settings, datalayer, map):
48
+ settings.UMAP_XSENDFILE_HEADER = "X-Accel-Redirect"
49
+ url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
50
+ response = client.get(url)
51
+ assert response.status_code == 200
52
+ assert "X-Accel-Redirect" in response.headers
53
+ assert response["X-Accel-Redirect"].startswith("/internal/datalayer/")
54
+ assert response["X-Accel-Redirect"].endswith(".geojson")
55
+
56
+
47
57
  def test_get_with_open_mode(client, settings, datalayer, map):
48
58
  map.share_status = Map.PUBLIC
49
59
  map.save()
@@ -111,7 +121,7 @@ def test_update(client, datalayer, map, post_data):
111
121
  # Test response is a json
112
122
  j = json.loads(response.content.decode())
113
123
  assert "id" in j
114
- assert datalayer.pk == j["id"]
124
+ assert str(datalayer.pk) == j["id"]
115
125
  assert j["browsable"] is True
116
126
  assert Path(modified_datalayer.geojson.path).exists()
117
127
 
@@ -154,48 +164,50 @@ def test_should_not_be_possible_to_delete_with_wrong_map_id_in_url(
154
164
  assert DataLayer.objects.filter(pk=datalayer.pk).exists()
155
165
 
156
166
 
157
- def test_optimistic_concurrency_control_with_good_last_modified(
167
+ def test_optimistic_concurrency_control_with_good_version(
158
168
  client, datalayer, map, post_data
159
169
  ):
160
170
  map.share_status = Map.PUBLIC
161
171
  map.save()
162
- # Get Last-Modified
172
+ # Get reference version
163
173
  url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
164
174
  response = client.get(url)
165
- last_modified = response["Last-Modified"]
175
+ reference_version = response["X-Datalayer-Version"]
166
176
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
167
177
  client.login(username=map.owner.username, password="123123")
168
178
  name = "new name"
169
179
  post_data["name"] = "new name"
170
180
  response = client.post(
171
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=last_modified
181
+ url, post_data, follow=True, HTTP_X_DATALAYER_REFERENCE=reference_version
172
182
  )
173
183
  assert response.status_code == 200
174
184
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
175
185
  assert modified_datalayer.name == name
176
186
 
177
187
 
178
- def test_optimistic_concurrency_control_with_bad_last_modified(
188
+ def test_optimistic_concurrency_control_with_bad_version(
179
189
  client, datalayer, map, post_data
180
190
  ):
181
191
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
182
192
  client.login(username=map.owner.username, password="123123")
183
193
  name = "new name"
184
194
  post_data["name"] = name
185
- response = client.post(url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE="xxx")
195
+ response = client.post(
196
+ url, post_data, follow=True, HTTP_X_DATALAYER_REFERENCE="xxx"
197
+ )
186
198
  assert response.status_code == 412
187
199
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
188
200
  assert modified_datalayer.name != name
189
201
 
190
202
 
191
- def test_optimistic_concurrency_control_with_empty_last_modified(
203
+ def test_optimistic_concurrency_control_with_empty_version(
192
204
  client, datalayer, map, post_data
193
205
  ):
194
206
  url = reverse("datalayer_update", args=(map.pk, datalayer.pk))
195
207
  client.login(username=map.owner.username, password="123123")
196
208
  name = "new name"
197
209
  post_data["name"] = name
198
- response = client.post(url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=None)
210
+ response = client.post(url, post_data, follow=True, X_DATALAYER_REFERENCE=None)
199
211
  assert response.status_code == 200
200
212
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
201
213
  assert modified_datalayer.name == name
@@ -225,6 +237,41 @@ def test_versions_should_return_versions(client, datalayer, map, settings):
225
237
  assert version in versions["versions"]
226
238
 
227
239
 
240
+ def test_versions_can_return_old_format(client, datalayer, map, settings):
241
+ map.share_status = Map.PUBLIC
242
+ map.save()
243
+ root = datalayer.storage_root()
244
+ datalayer.old_id = 123 # old datalayer id (now replaced by uuid)
245
+ datalayer.save()
246
+
247
+ datalayer.geojson.storage.save(
248
+ "%s/%s_1440924889.geojson" % (root, datalayer.pk), ContentFile("{}")
249
+ )
250
+ datalayer.geojson.storage.save(
251
+ "%s/%s_1440923687.geojson" % (root, datalayer.pk), ContentFile("{}")
252
+ )
253
+
254
+ # store with the id prefix (rather than the uuid)
255
+ old_format_version = "%s_1440918637.geojson" % datalayer.old_id
256
+ datalayer.geojson.storage.save(
257
+ ("%s/" % root) + old_format_version, ContentFile("{}")
258
+ )
259
+
260
+ url = reverse("datalayer_versions", args=(map.pk, datalayer.pk))
261
+ versions = json.loads(client.get(url).content.decode())
262
+ assert len(versions["versions"]) == 4
263
+ version = {
264
+ "name": old_format_version,
265
+ "size": 2,
266
+ "at": "1440918637",
267
+ }
268
+ assert version in versions["versions"]
269
+
270
+ client.get(
271
+ reverse("datalayer_version", args=(map.pk, datalayer.pk, old_format_version))
272
+ )
273
+
274
+
228
275
  def test_version_should_return_one_version_geojson(client, datalayer, map):
229
276
  map.share_status = Map.PUBLIC
230
277
  map.save()
@@ -444,7 +491,7 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
444
491
  assert response.status_code == 200
445
492
 
446
493
  response = client.get(reverse("datalayer_view", args=(map.pk, datalayer.pk)))
447
- reference_timestamp = response["Last-Modified"]
494
+ reference_version = response.headers.get("X-Datalayer-Version")
448
495
 
449
496
  # Client 1 adds "Point 5, 6" to the existing data
450
497
  client1_feature = {
@@ -454,14 +501,16 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
454
501
  }
455
502
  client1_data = deepcopy(reference_data)
456
503
  client1_data["features"].append(client1_feature)
457
- # Sleep to change the current timestamp (used in the If-Unmodified-Since header)
458
- time.sleep(1)
504
+
459
505
  post_data["geojson"] = SimpleUploadedFile(
460
506
  "foo.json",
461
507
  json.dumps(client1_data).encode("utf-8"),
462
508
  )
463
509
  response = client.post(
464
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
510
+ url,
511
+ post_data,
512
+ follow=True,
513
+ headers={"X-Datalayer-Reference": reference_version},
465
514
  )
466
515
  assert response.status_code == 200
467
516
 
@@ -479,7 +528,10 @@ def test_optimistic_merge_both_added(client, datalayer, map, reference_data):
479
528
  json.dumps(client2_data).encode("utf-8"),
480
529
  )
481
530
  response = client.post(
482
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
531
+ url,
532
+ post_data,
533
+ follow=True,
534
+ headers={"X-Datalayer-Reference": reference_version},
483
535
  )
484
536
  assert response.status_code == 200
485
537
  modified_datalayer = DataLayer.objects.get(pk=datalayer.pk)
@@ -513,20 +565,22 @@ def test_optimistic_merge_conflicting_change_raises(
513
565
  assert response.status_code == 200
514
566
 
515
567
  response = client.get(reverse("datalayer_view", args=(map.pk, datalayer.pk)))
516
- reference_timestamp = response["Last-Modified"]
568
+
569
+ reference_version = response.headers.get("X-Datalayer-Version")
517
570
 
518
571
  # First client changes the first feature.
519
572
  client1_data = deepcopy(reference_data)
520
573
  client1_data["features"][0]["geometry"] = {"type": "Point", "coordinates": [5, 6]}
521
574
 
522
- # Sleep to change the current timestamp (used in the If-Unmodified-Since header)
523
- time.sleep(1)
524
575
  post_data["geojson"] = SimpleUploadedFile(
525
576
  "foo.json",
526
577
  json.dumps(client1_data).encode("utf-8"),
527
578
  )
528
579
  response = client.post(
529
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
580
+ url,
581
+ post_data,
582
+ follow=True,
583
+ headers={"X-Datalayer-Reference": reference_version},
530
584
  )
531
585
  assert response.status_code == 200
532
586
 
@@ -539,7 +593,10 @@ def test_optimistic_merge_conflicting_change_raises(
539
593
  json.dumps(client2_data).encode("utf-8"),
540
594
  )
541
595
  response = client.post(
542
- url, post_data, follow=True, HTTP_IF_UNMODIFIED_SINCE=reference_timestamp
596
+ url,
597
+ post_data,
598
+ follow=True,
599
+ headers={"X-Datalayer-Reference": reference_version},
543
600
  )
544
601
  assert response.status_code == 412
545
602
 
@@ -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)