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
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):
@@ -1007,8 +1020,8 @@ class DataLayerView(GZipMixin, BaseDetailView):
1007
1020
 
1008
1021
  if getattr(settings, "UMAP_XSENDFILE_HEADER", None):
1009
1022
  response = HttpResponse()
1010
- path = path.replace(settings.MEDIA_ROOT, "/internal")
1011
- response[settings.UMAP_XSENDFILE_HEADER] = path
1023
+ internal_path = str(path).replace(settings.MEDIA_ROOT, "/internal")
1024
+ response[settings.UMAP_XSENDFILE_HEADER] = internal_path
1012
1025
  else:
1013
1026
  # Do not use in production
1014
1027
  # (no gzip/cache-control/If-Modified-Since/If-None-Match)
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: umap-project
3
- Version: 2.0.4
3
+ Version: 2.1.1
4
4
  Summary: Create maps with OpenStreetMap layers in a minute and embed them in your site.
5
5
  Author-email: Yohan Boniface <yb@enix.org>
6
6
  Maintainer-email: David Larlet <david@larlet.fr>
@@ -19,7 +19,7 @@ Requires-Python: >=3.10
19
19
  Requires-Dist: django-agnocomplete==2.2.0
20
20
  Requires-Dist: django-environ==0.11.2
21
21
  Requires-Dist: django-probes==1.7.0
22
- Requires-Dist: django==5.0.2
22
+ Requires-Dist: django==5.0.3
23
23
  Requires-Dist: pillow==10.2.0
24
24
  Requires-Dist: psycopg==3.1.18
25
25
  Requires-Dist: rcssmin==1.1.2
@@ -29,22 +29,22 @@ Requires-Dist: social-auth-app-django==5.4.0
29
29
  Requires-Dist: social-auth-core==4.5.3
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: djlint==1.34.1; extra == 'dev'
32
- Requires-Dist: hatch==1.9.3; extra == 'dev'
32
+ Requires-Dist: hatch==1.9.4; extra == 'dev'
33
33
  Requires-Dist: isort==5.13.2; extra == 'dev'
34
- Requires-Dist: mkdocs-material==9.5.11; extra == 'dev'
34
+ Requires-Dist: mkdocs-material==9.5.14; extra == 'dev'
35
35
  Requires-Dist: mkdocs==1.5.3; extra == 'dev'
36
- Requires-Dist: pymdown-extensions==10.7; extra == 'dev'
37
- Requires-Dist: ruff==0.2.2; extra == 'dev'
36
+ Requires-Dist: pymdown-extensions==10.7.1; extra == 'dev'
37
+ Requires-Dist: ruff==0.3.3; extra == 'dev'
38
38
  Requires-Dist: vermin==1.6.0; extra == 'dev'
39
39
  Provides-Extra: docker
40
40
  Requires-Dist: uwsgi==2.0.24; extra == 'docker'
41
41
  Provides-Extra: test
42
42
  Requires-Dist: factory-boy==3.2.1; extra == 'test'
43
43
  Requires-Dist: playwright>=1.39; extra == 'test'
44
- Requires-Dist: pytest-django==4.5.2; extra == 'test'
45
- Requires-Dist: pytest-playwright==0.4.3; extra == 'test'
44
+ Requires-Dist: pytest-django==4.8.0; extra == 'test'
45
+ Requires-Dist: pytest-playwright==0.4.4; extra == 'test'
46
46
  Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'test'
47
- Requires-Dist: pytest==6.2.5; extra == 'test'
47
+ Requires-Dist: pytest==8.0.2; extra == 'test'
48
48
  Description-Content-Type: text/markdown
49
49
 
50
50
  # uMap project