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.
- umap/__init__.py +1 -1
- umap/fields.py +3 -1
- umap/locale/br/LC_MESSAGES/django.po +76 -71
- umap/locale/en/LC_MESSAGES/django.po +41 -41
- umap/locale/hu/LC_MESSAGES/django.po +42 -42
- umap/locale/it/LC_MESSAGES/django.po +64 -58
- umap/locale/ms/LC_MESSAGES/django.po +62 -57
- umap/migrations/0018_datalayer_uuid.py +62 -0
- umap/migrations/0019_migrate_internal_remote_datalayers.py +52 -0
- umap/models.py +20 -3
- umap/settings/base.py +1 -0
- umap/settings/dev.py +1 -0
- umap/static/umap/js/modules/browser.js +2 -2
- umap/static/umap/js/modules/global.js +14 -4
- umap/static/umap/js/modules/i18n.js +35 -0
- umap/static/umap/js/modules/leaflet-configure.js +7 -0
- umap/static/umap/js/modules/schema.js +388 -0
- umap/static/umap/js/modules/urls.js +17 -2
- umap/static/umap/js/modules/utils.js +24 -0
- umap/static/umap/js/umap.controls.js +9 -10
- umap/static/umap/js/umap.core.js +5 -5
- umap/static/umap/js/umap.features.js +23 -9
- umap/static/umap/js/umap.forms.js +49 -299
- umap/static/umap/js/umap.icon.js +2 -2
- umap/static/umap/js/umap.js +26 -129
- umap/static/umap/js/umap.layer.js +9 -9
- umap/static/umap/js/umap.popup.js +3 -0
- umap/static/umap/js/umap.share.js +1 -1
- umap/static/umap/locale/am_ET.json +229 -225
- umap/static/umap/locale/ar.json +229 -225
- umap/static/umap/locale/ast.json +229 -225
- umap/static/umap/locale/bg.json +229 -225
- umap/static/umap/locale/br.json +237 -233
- umap/static/umap/locale/ca.json +229 -225
- umap/static/umap/locale/cs_CZ.json +229 -225
- umap/static/umap/locale/da.json +229 -225
- umap/static/umap/locale/de.json +229 -225
- umap/static/umap/locale/el.json +229 -225
- umap/static/umap/locale/en.json +230 -233
- umap/static/umap/locale/en_US.json +229 -225
- umap/static/umap/locale/es.json +229 -225
- umap/static/umap/locale/et.json +229 -225
- umap/static/umap/locale/eu.json +226 -198
- umap/static/umap/locale/fa_IR.json +229 -225
- umap/static/umap/locale/fi.json +229 -225
- umap/static/umap/locale/fr.json +229 -232
- umap/static/umap/locale/gl.json +229 -225
- umap/static/umap/locale/he.json +229 -225
- umap/static/umap/locale/hr.json +229 -225
- umap/static/umap/locale/hu.json +229 -232
- umap/static/umap/locale/id.json +229 -225
- umap/static/umap/locale/is.json +229 -225
- umap/static/umap/locale/it.json +229 -232
- umap/static/umap/locale/ja.json +229 -225
- umap/static/umap/locale/ko.json +229 -225
- umap/static/umap/locale/lt.json +229 -225
- umap/static/umap/locale/ms.json +229 -232
- umap/static/umap/locale/nl.json +232 -228
- umap/static/umap/locale/no.json +229 -225
- umap/static/umap/locale/pl.json +229 -225
- umap/static/umap/locale/pl_PL.json +229 -225
- umap/static/umap/locale/pt.json +229 -225
- umap/static/umap/locale/pt_BR.json +229 -225
- umap/static/umap/locale/pt_PT.json +229 -225
- umap/static/umap/locale/ro.json +229 -225
- umap/static/umap/locale/ru.json +229 -225
- umap/static/umap/locale/sk_SK.json +229 -225
- umap/static/umap/locale/sl.json +229 -225
- umap/static/umap/locale/sr.json +229 -225
- umap/static/umap/locale/sv.json +229 -225
- umap/static/umap/locale/th_TH.json +229 -225
- umap/static/umap/locale/tr.json +229 -225
- umap/static/umap/locale/uk_UA.json +229 -225
- umap/static/umap/locale/vi.json +229 -225
- umap/static/umap/locale/vi_VN.json +229 -225
- umap/static/umap/locale/zh.json +229 -225
- umap/static/umap/locale/zh_CN.json +229 -225
- umap/static/umap/locale/zh_TW.Big5.json +229 -225
- umap/static/umap/locale/zh_TW.json +229 -232
- umap/static/umap/test/index.html +0 -2
- umap/static/umap/{test → unittests}/URLs.js +5 -0
- umap/static/umap/vendors/leaflet/leaflet-src.esm.js +7064 -7064
- umap/static/umap/vendors/photon/leaflet.photon.js +3 -0
- umap/templates/umap/js.html +8 -6
- umap/templatetags/umap_tags.py +3 -2
- umap/tests/integration/test_browser.py +40 -0
- umap/tests/integration/test_collaborative_editing.py +72 -3
- umap/tests/integration/test_export_map.py +226 -9
- umap/tests/integration/test_features_id_generation.py +51 -0
- umap/tests/integration/test_owned_map.py +14 -1
- umap/tests/integration/test_statics.py +3 -3
- umap/tests/integration/test_tilelayer.py +3 -3
- umap/tests/settings.py +3 -3
- umap/tests/test_datalayer_views.py +67 -20
- umap/tests/test_map_views.py +20 -0
- umap/tests/test_merge_features.py +25 -5
- umap/urls.py +12 -12
- umap/utils.py +7 -0
- umap/views.py +56 -47
- umap/wsgi.py +1 -0
- {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/METADATA +9 -9
- {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/RECORD +105 -99
- umap/static/umap/test/Map.Export.js +0 -106
- {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/WHEEL +0 -0
- {umap_project-2.0.4.dist-info → umap_project-2.1.0.dist-info}/entry_points.txt +0 -0
- {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["
|
|
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
|
|
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
|
|
162
|
+
# Get reference version
|
|
163
163
|
url = reverse("datalayer_view", args=(map.pk, datalayer.pk))
|
|
164
164
|
response = client.get(url)
|
|
165
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
umap/tests/test_map_views.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
76
|
-
|
|
75
|
+
path(
|
|
76
|
+
"datalayer/<int:map_id>/<uuid:pk>/",
|
|
77
77
|
views.DataLayerView.as_view(),
|
|
78
78
|
name="datalayer_view",
|
|
79
79
|
),
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
path(
|
|
81
|
+
"datalayer/<int:map_id>/<uuid:pk>/versions/",
|
|
82
82
|
views.DataLayerVersions.as_view(),
|
|
83
83
|
name="datalayer_versions",
|
|
84
84
|
),
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
325
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"
|
|
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"] =
|
|
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
|
-
|
|
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
|
-
"
|
|
671
|
-
|
|
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
|
-
|
|
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
|
|
972
|
-
|
|
973
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
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["
|
|
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
|
|
1028
|
-
|
|
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
|
|
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["
|
|
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
|
|
1053
|
-
return
|
|
1064
|
+
def has_changes_since(self, incoming_version):
|
|
1065
|
+
return incoming_version and self.version != incoming_version
|
|
1054
1066
|
|
|
1055
|
-
def merge(self,
|
|
1067
|
+
def merge(self, reference_version):
|
|
1056
1068
|
"""
|
|
1057
|
-
Attempt to apply the incoming changes to the
|
|
1058
|
-
|
|
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
|
|
1075
|
+
# Use the provided info to find the correct version in our storage.
|
|
1064
1076
|
for name in self.object.get_versions():
|
|
1065
|
-
path =
|
|
1066
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
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
|
|