umap-project 2.7.3__py3-none-any.whl → 2.8.0a0__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 (279) hide show
  1. umap/__init__.py +1 -1
  2. umap/forms.py +4 -14
  3. umap/locale/am_ET/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/am_ET/LC_MESSAGES/django.po +278 -151
  5. umap/locale/ar/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/ar/LC_MESSAGES/django.po +335 -141
  7. umap/locale/bg/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/bg/LC_MESSAGES/django.po +279 -152
  9. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/br/LC_MESSAGES/django.po +95 -79
  11. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/ca/LC_MESSAGES/django.po +85 -68
  13. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/cs_CZ/LC_MESSAGES/django.po +78 -66
  15. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  16. umap/locale/da/LC_MESSAGES/django.po +280 -153
  17. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/de/LC_MESSAGES/django.po +80 -64
  19. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/el/LC_MESSAGES/django.po +82 -66
  21. umap/locale/en/LC_MESSAGES/django.po +73 -61
  22. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  23. umap/locale/es/LC_MESSAGES/django.po +75 -63
  24. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  25. umap/locale/et/LC_MESSAGES/django.po +280 -153
  26. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  27. umap/locale/eu/LC_MESSAGES/django.po +82 -66
  28. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  29. umap/locale/fa_IR/LC_MESSAGES/django.po +80 -64
  30. umap/locale/fi/LC_MESSAGES/django.mo +0 -0
  31. umap/locale/fi/LC_MESSAGES/django.po +278 -151
  32. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  33. umap/locale/fr/LC_MESSAGES/django.po +75 -63
  34. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  35. umap/locale/gl/LC_MESSAGES/django.po +280 -153
  36. umap/locale/he/LC_MESSAGES/django.mo +0 -0
  37. umap/locale/he/LC_MESSAGES/django.po +281 -154
  38. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  39. umap/locale/hu/LC_MESSAGES/django.po +80 -64
  40. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  41. umap/locale/is/LC_MESSAGES/django.po +280 -153
  42. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  43. umap/locale/it/LC_MESSAGES/django.po +82 -66
  44. umap/locale/ja/LC_MESSAGES/django.mo +0 -0
  45. umap/locale/ja/LC_MESSAGES/django.po +280 -153
  46. umap/locale/ko/LC_MESSAGES/django.mo +0 -0
  47. umap/locale/ko/LC_MESSAGES/django.po +280 -153
  48. umap/locale/lt/LC_MESSAGES/django.mo +0 -0
  49. umap/locale/lt/LC_MESSAGES/django.po +280 -153
  50. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  51. umap/locale/ms/LC_MESSAGES/django.po +82 -66
  52. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  53. umap/locale/nl/LC_MESSAGES/django.po +280 -153
  54. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  55. umap/locale/pl/LC_MESSAGES/django.po +82 -66
  56. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  57. umap/locale/pt/LC_MESSAGES/django.po +75 -63
  58. umap/locale/pt_BR/LC_MESSAGES/django.mo +0 -0
  59. umap/locale/pt_BR/LC_MESSAGES/django.po +280 -153
  60. umap/locale/pt_PT/LC_MESSAGES/django.mo +0 -0
  61. umap/locale/pt_PT/LC_MESSAGES/django.po +280 -153
  62. umap/locale/ru/LC_MESSAGES/django.mo +0 -0
  63. umap/locale/ru/LC_MESSAGES/django.po +280 -153
  64. umap/locale/sk_SK/LC_MESSAGES/django.mo +0 -0
  65. umap/locale/sk_SK/LC_MESSAGES/django.po +280 -153
  66. umap/locale/sl/LC_MESSAGES/django.mo +0 -0
  67. umap/locale/sl/LC_MESSAGES/django.po +280 -153
  68. umap/locale/sr/LC_MESSAGES/django.mo +0 -0
  69. umap/locale/sr/LC_MESSAGES/django.po +280 -153
  70. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  71. umap/locale/sv/LC_MESSAGES/django.po +81 -65
  72. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  73. umap/locale/th_TH/LC_MESSAGES/django.po +257 -185
  74. umap/locale/tr/LC_MESSAGES/django.mo +0 -0
  75. umap/locale/tr/LC_MESSAGES/django.po +280 -153
  76. umap/locale/uk_UA/LC_MESSAGES/django.mo +0 -0
  77. umap/locale/uk_UA/LC_MESSAGES/django.po +280 -153
  78. umap/locale/vi/LC_MESSAGES/django.mo +0 -0
  79. umap/locale/vi/LC_MESSAGES/django.po +278 -151
  80. umap/locale/zh/LC_MESSAGES/django.mo +0 -0
  81. umap/locale/zh/LC_MESSAGES/django.po +278 -151
  82. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  83. umap/locale/zh_TW/LC_MESSAGES/django.po +97 -81
  84. umap/management/commands/empty_trash.py +32 -0
  85. umap/management/commands/migrate_to_S3.py +29 -0
  86. umap/migrations/0023_alter_datalayer_uuid.py +19 -0
  87. umap/migrations/0024_alter_map_share_status.py +30 -0
  88. umap/migrations/0025_alter_datalayer_geojson.py +24 -0
  89. umap/models.py +68 -116
  90. umap/settings/base.py +22 -2
  91. umap/static/umap/base.css +3 -603
  92. umap/static/umap/content.css +5 -3
  93. umap/static/umap/css/bar.css +202 -0
  94. umap/static/umap/css/form.css +617 -0
  95. umap/static/umap/css/icon.css +21 -1
  96. umap/static/umap/css/popup.css +125 -0
  97. umap/static/umap/img/16-white.svg +16 -4
  98. umap/static/umap/img/16.svg +1 -1
  99. umap/static/umap/img/source/16-white.svg +46 -45
  100. umap/static/umap/img/source/16.svg +1 -753
  101. umap/static/umap/js/components/fragment.js +3 -1
  102. umap/static/umap/js/modules/browser.js +20 -19
  103. umap/static/umap/js/modules/caption.js +21 -22
  104. umap/static/umap/js/modules/data/features.js +101 -74
  105. umap/static/umap/js/modules/data/layer.js +157 -137
  106. umap/static/umap/js/modules/facets.js +9 -9
  107. umap/static/umap/js/modules/formatter.js +5 -5
  108. umap/static/umap/js/modules/global.js +4 -52
  109. umap/static/umap/js/modules/help.js +18 -21
  110. umap/static/umap/js/modules/importer.js +71 -39
  111. umap/static/umap/js/modules/importers/cadastrefr.js +4 -0
  112. umap/static/umap/js/modules/importers/geodatamine.js +3 -3
  113. umap/static/umap/js/modules/importers/overpass.js +5 -0
  114. umap/static/umap/js/modules/permissions.js +85 -87
  115. umap/static/umap/js/modules/rendering/layers/base.js +15 -15
  116. umap/static/umap/js/modules/rendering/layers/classified.js +1 -1
  117. umap/static/umap/js/modules/rendering/layers/cluster.js +1 -1
  118. umap/static/umap/js/modules/rendering/layers/heat.js +1 -1
  119. umap/static/umap/js/modules/rendering/map.js +390 -0
  120. umap/static/umap/js/modules/rendering/popup.js +10 -9
  121. umap/static/umap/js/modules/rendering/template.js +35 -12
  122. umap/static/umap/js/modules/rendering/ui.js +57 -12
  123. umap/static/umap/js/modules/rules.js +22 -25
  124. umap/static/umap/js/modules/saving.js +47 -0
  125. umap/static/umap/js/modules/schema.js +5 -0
  126. umap/static/umap/js/modules/share.js +21 -24
  127. umap/static/umap/js/modules/slideshow.js +24 -20
  128. umap/static/umap/js/modules/sync/updaters.js +7 -9
  129. umap/static/umap/js/modules/tableeditor.js +20 -19
  130. umap/static/umap/js/modules/ui/bar.js +196 -0
  131. umap/static/umap/js/modules/ui/panel.js +10 -9
  132. umap/static/umap/js/modules/umap.js +1668 -0
  133. umap/static/umap/js/modules/urls.js +2 -2
  134. umap/static/umap/js/modules/utils.js +20 -6
  135. umap/static/umap/js/umap.controls.js +74 -301
  136. umap/static/umap/js/umap.core.js +29 -50
  137. umap/static/umap/js/umap.forms.js +34 -27
  138. umap/static/umap/keycloak.png +0 -0
  139. umap/static/umap/locale/am_ET.js +26 -10
  140. umap/static/umap/locale/am_ET.json +26 -10
  141. umap/static/umap/locale/ar.js +26 -10
  142. umap/static/umap/locale/ar.json +26 -10
  143. umap/static/umap/locale/ast.js +26 -10
  144. umap/static/umap/locale/ast.json +26 -10
  145. umap/static/umap/locale/bg.js +26 -10
  146. umap/static/umap/locale/bg.json +26 -10
  147. umap/static/umap/locale/br.js +27 -20
  148. umap/static/umap/locale/br.json +27 -20
  149. umap/static/umap/locale/ca.js +32 -29
  150. umap/static/umap/locale/ca.json +32 -29
  151. umap/static/umap/locale/cs_CZ.js +24 -17
  152. umap/static/umap/locale/cs_CZ.json +24 -17
  153. umap/static/umap/locale/da.js +26 -10
  154. umap/static/umap/locale/da.json +26 -10
  155. umap/static/umap/locale/de.js +21 -14
  156. umap/static/umap/locale/de.json +21 -14
  157. umap/static/umap/locale/el.js +28 -12
  158. umap/static/umap/locale/el.json +28 -12
  159. umap/static/umap/locale/en.js +12 -9
  160. umap/static/umap/locale/en.json +12 -9
  161. umap/static/umap/locale/en_US.json +26 -10
  162. umap/static/umap/locale/es.js +16 -13
  163. umap/static/umap/locale/es.json +16 -13
  164. umap/static/umap/locale/et.js +26 -10
  165. umap/static/umap/locale/et.json +26 -10
  166. umap/static/umap/locale/eu.js +16 -9
  167. umap/static/umap/locale/eu.json +16 -9
  168. umap/static/umap/locale/fa_IR.js +16 -9
  169. umap/static/umap/locale/fa_IR.json +16 -9
  170. umap/static/umap/locale/fi.js +26 -10
  171. umap/static/umap/locale/fi.json +26 -10
  172. umap/static/umap/locale/fr.js +12 -9
  173. umap/static/umap/locale/fr.json +12 -9
  174. umap/static/umap/locale/gl.js +26 -10
  175. umap/static/umap/locale/gl.json +26 -10
  176. umap/static/umap/locale/he.js +26 -10
  177. umap/static/umap/locale/he.json +26 -10
  178. umap/static/umap/locale/hr.js +26 -10
  179. umap/static/umap/locale/hr.json +26 -10
  180. umap/static/umap/locale/hu.js +16 -9
  181. umap/static/umap/locale/hu.json +16 -9
  182. umap/static/umap/locale/id.js +26 -10
  183. umap/static/umap/locale/id.json +26 -10
  184. umap/static/umap/locale/is.js +26 -10
  185. umap/static/umap/locale/is.json +26 -10
  186. umap/static/umap/locale/it.js +26 -10
  187. umap/static/umap/locale/it.json +26 -10
  188. umap/static/umap/locale/ja.js +26 -10
  189. umap/static/umap/locale/ja.json +26 -10
  190. umap/static/umap/locale/ko.js +26 -10
  191. umap/static/umap/locale/ko.json +26 -10
  192. umap/static/umap/locale/lt.js +26 -10
  193. umap/static/umap/locale/lt.json +26 -10
  194. umap/static/umap/locale/ms.js +28 -12
  195. umap/static/umap/locale/ms.json +28 -12
  196. umap/static/umap/locale/nl.js +28 -12
  197. umap/static/umap/locale/nl.json +28 -12
  198. umap/static/umap/locale/no.js +26 -10
  199. umap/static/umap/locale/no.json +26 -10
  200. umap/static/umap/locale/pl.js +28 -12
  201. umap/static/umap/locale/pl.json +28 -12
  202. umap/static/umap/locale/pl_PL.json +26 -10
  203. umap/static/umap/locale/pt.js +16 -9
  204. umap/static/umap/locale/pt.json +16 -9
  205. umap/static/umap/locale/pt_BR.js +26 -10
  206. umap/static/umap/locale/pt_BR.json +26 -10
  207. umap/static/umap/locale/pt_PT.js +16 -9
  208. umap/static/umap/locale/pt_PT.json +16 -9
  209. umap/static/umap/locale/ro.js +26 -10
  210. umap/static/umap/locale/ro.json +26 -10
  211. umap/static/umap/locale/ru.js +26 -10
  212. umap/static/umap/locale/ru.json +26 -10
  213. umap/static/umap/locale/si.js +7 -7
  214. umap/static/umap/locale/si.json +7 -7
  215. umap/static/umap/locale/sk_SK.js +26 -10
  216. umap/static/umap/locale/sk_SK.json +26 -10
  217. umap/static/umap/locale/sl.js +26 -10
  218. umap/static/umap/locale/sl.json +26 -10
  219. umap/static/umap/locale/sr.js +26 -10
  220. umap/static/umap/locale/sr.json +26 -10
  221. umap/static/umap/locale/sv.js +27 -11
  222. umap/static/umap/locale/sv.json +27 -11
  223. umap/static/umap/locale/th_TH.js +28 -12
  224. umap/static/umap/locale/th_TH.json +28 -12
  225. umap/static/umap/locale/tr.js +26 -10
  226. umap/static/umap/locale/tr.json +26 -10
  227. umap/static/umap/locale/uk_UA.js +26 -10
  228. umap/static/umap/locale/uk_UA.json +26 -10
  229. umap/static/umap/locale/vi.js +26 -10
  230. umap/static/umap/locale/vi.json +26 -10
  231. umap/static/umap/locale/vi_VN.json +26 -10
  232. umap/static/umap/locale/zh.js +26 -10
  233. umap/static/umap/locale/zh.json +26 -10
  234. umap/static/umap/locale/zh_CN.json +26 -10
  235. umap/static/umap/locale/zh_TW.Big5.json +26 -10
  236. umap/static/umap/locale/zh_TW.js +34 -27
  237. umap/static/umap/locale/zh_TW.json +34 -27
  238. umap/static/umap/map.css +5 -364
  239. umap/static/umap/unittests/URLs.js +15 -15
  240. umap/static/umap/unittests/utils.js +23 -1
  241. umap/static/umap/vars.css +2 -0
  242. umap/static/umap/vendors/formbuilder/Leaflet.FormBuilder.js +5 -1
  243. umap/storage.py +152 -0
  244. umap/templates/registration/login.html +7 -6
  245. umap/templates/umap/css.html +3 -0
  246. umap/templates/umap/js.html +1 -2
  247. umap/templates/umap/map_init.html +4 -5
  248. umap/templates/umap/user_dashboard.html +18 -19
  249. umap/tests/base.py +5 -1
  250. umap/tests/integration/conftest.py +2 -1
  251. umap/tests/integration/test_anonymous_owned_map.py +18 -10
  252. umap/tests/integration/test_browser.py +16 -1
  253. umap/tests/integration/test_dashboard.py +1 -1
  254. umap/tests/integration/test_edit_datalayer.py +18 -7
  255. umap/tests/integration/test_import.py +8 -5
  256. umap/tests/integration/test_optimistic_merge.py +31 -8
  257. umap/tests/integration/test_owned_map.py +22 -16
  258. umap/tests/integration/test_popup.py +44 -0
  259. umap/tests/integration/test_save.py +35 -0
  260. umap/tests/integration/test_view_marker.py +12 -0
  261. umap/tests/integration/test_view_polyline.py +257 -0
  262. umap/tests/integration/test_websocket_sync.py +81 -9
  263. umap/tests/test_datalayer.py +6 -7
  264. umap/tests/test_datalayer_s3.py +135 -0
  265. umap/tests/test_datalayer_views.py +28 -10
  266. umap/tests/test_empty_trash.py +34 -0
  267. umap/tests/test_map.py +12 -3
  268. umap/tests/test_map_views.py +69 -37
  269. umap/tests/test_views.py +53 -0
  270. umap/urls.py +3 -3
  271. umap/views.py +107 -76
  272. {umap_project-2.7.3.dist-info → umap_project-2.8.0a0.dist-info}/METADATA +16 -13
  273. {umap_project-2.7.3.dist-info → umap_project-2.8.0a0.dist-info}/RECORD +276 -262
  274. umap/management/commands/purge_purgatory.py +0 -28
  275. umap/static/umap/js/umap.js +0 -1903
  276. umap/tests/test_purge_purgatory.py +0 -25
  277. {umap_project-2.7.3.dist-info → umap_project-2.8.0a0.dist-info}/WHEEL +0 -0
  278. {umap_project-2.7.3.dist-info → umap_project-2.8.0a0.dist-info}/entry_points.txt +0 -0
  279. {umap_project-2.7.3.dist-info → umap_project-2.8.0a0.dist-info}/licenses/LICENSE +0 -0
umap/storage.py CHANGED
@@ -1,9 +1,17 @@
1
+ import operator
2
+ import os
3
+ import shutil
4
+ import time
5
+ from gzip import GzipFile
1
6
  from pathlib import Path
2
7
 
8
+ from botocore.exceptions import ClientError
3
9
  from django.conf import settings
4
10
  from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
11
+ from django.core.files.storage import FileSystemStorage
5
12
  from rcssmin import cssmin
6
13
  from rjsmin import jsmin
14
+ from storages.backends.s3 import S3Storage
7
15
 
8
16
 
9
17
  class UmapManifestStaticFilesStorage(ManifestStaticFilesStorage):
@@ -62,3 +70,147 @@ class UmapManifestStaticFilesStorage(ManifestStaticFilesStorage):
62
70
  minified = cssmin(initial)
63
71
  path.write_text(minified)
64
72
  yield original_path, processed_path, True
73
+
74
+
75
+ class UmapS3(S3Storage):
76
+ gzip = True
77
+
78
+ def get_reference_version(self, instance):
79
+ metadata = self.connection.meta.client.head_object(
80
+ Bucket=self.bucket_name, Key=instance.geojson.name
81
+ )
82
+ # Do not fail if bucket does not handle versioning
83
+ return metadata.get("VersionId", metadata["ETag"])
84
+
85
+ def make_filename(self, instance):
86
+ return f"{str(instance.pk)}.geojson"
87
+
88
+ def list_versions(self, instance):
89
+ response = self.connection.meta.client.list_object_versions(
90
+ Bucket=self.bucket_name, Prefix=instance.geojson.name
91
+ )
92
+ return [
93
+ {
94
+ "ref": version["VersionId"],
95
+ "at": version["LastModified"].timestamp() * 1000,
96
+ "size": version["Size"],
97
+ }
98
+ for version in response["Versions"]
99
+ ]
100
+
101
+ def get_version(self, ref, instance):
102
+ try:
103
+ data = self.connection.meta.client.get_object(
104
+ Bucket=self.bucket_name,
105
+ Key=instance.geojson.name,
106
+ VersionId=ref,
107
+ )
108
+ except ClientError:
109
+ raise ValueError(f"Invalid version reference: {ref}")
110
+ return GzipFile(mode="r", fileobj=data["Body"]).read()
111
+
112
+ def get_version_path(self, ref, instance):
113
+ return self.url(instance.geojson.name, parameters={"VersionId": ref})
114
+
115
+ def onDatalayerSave(self, instance):
116
+ pass
117
+
118
+ def onDatalayerDelete(self, instance):
119
+ return self.connection.meta.client.delete_object(
120
+ Bucket=self.bucket_name,
121
+ Key=instance.geojson.name,
122
+ )
123
+
124
+
125
+ class UmapFileSystem(FileSystemStorage):
126
+ def get_reference_version(self, instance):
127
+ return self._extract_version_ref(instance.geojson.name)
128
+
129
+ def make_filename(self, instance):
130
+ root = self._base_path(instance)
131
+ name = "%s_%s.geojson" % (instance.pk, int(time.time() * 1000))
132
+ return root / name
133
+
134
+ def list_versions(self, instance):
135
+ root = self._base_path(instance)
136
+ names = self.listdir(root)[1]
137
+ names = [name for name in names if self._is_valid_version(name, instance)]
138
+ versions = [self._version_metadata(name, instance) for name in names]
139
+ versions.sort(reverse=True, key=operator.itemgetter("at"))
140
+ return versions
141
+
142
+ def get_version(self, ref, instance):
143
+ with self.open(self.get_version_path(ref, instance), "r") as f:
144
+ return f.read()
145
+
146
+ def get_version_path(self, ref, instance):
147
+ base_path = Path(settings.MEDIA_ROOT) / self._base_path(instance)
148
+ fullpath = base_path / f"{instance.pk}_{ref}.geojson"
149
+ if instance.old_id and not fullpath.exists():
150
+ fullpath = base_path / f"{instance.old_id}_{ref}.geojson"
151
+ if not fullpath.exists():
152
+ raise ValueError(f"Invalid version reference: {ref}")
153
+ return fullpath
154
+
155
+ def onDatalayerSave(self, instance):
156
+ self._purge_gzip(instance)
157
+ self._purge_old_versions(instance, keep=settings.UMAP_KEEP_VERSIONS)
158
+
159
+ def onDatalayerDelete(self, instance):
160
+ self._purge_gzip(instance)
161
+ self._purge_old_versions(instance, keep=None)
162
+
163
+ def _extract_version_ref(self, path):
164
+ version = path.split(".")[0]
165
+ if "_" in version:
166
+ return version.split("_")[-1]
167
+ return version
168
+
169
+ def _base_path(self, instance):
170
+ path = ["datalayer", str(instance.map.pk)[-1]]
171
+ if len(str(instance.map.pk)) > 1:
172
+ path.append(str(instance.map.pk)[-2])
173
+ path.append(str(instance.map.pk))
174
+ return Path(os.path.join(*path))
175
+
176
+ def _is_valid_version(self, name, instance):
177
+ valid_prefixes = [name.startswith("%s_" % instance.pk)]
178
+ if instance.old_id:
179
+ valid_prefixes.append(name.startswith("%s_" % instance.old_id))
180
+ return any(valid_prefixes) and name.endswith(".geojson")
181
+
182
+ def _version_metadata(self, name, instance):
183
+ ref = self._extract_version_ref(name)
184
+ return {
185
+ "name": name,
186
+ "ref": ref,
187
+ "at": ref,
188
+ "size": self.size(self._base_path(instance) / name),
189
+ }
190
+
191
+ def _purge_old_versions(self, instance, keep=None):
192
+ root = self._base_path(instance)
193
+ versions = self.list_versions(instance)
194
+ if keep is not None:
195
+ versions = versions[keep:]
196
+ for version in versions:
197
+ name = version["name"]
198
+ # Should not be in the list, but ensure to not delete the file
199
+ # currently used in database
200
+ if keep is not None and instance.geojson.name.endswith(name):
201
+ continue
202
+ try:
203
+ self.delete(root / name)
204
+ except FileNotFoundError:
205
+ pass
206
+
207
+ def _purge_gzip(self, instance):
208
+ root = self._base_path(instance)
209
+ names = self.listdir(root)[1]
210
+ prefixes = [f"{instance.pk}_"]
211
+ if instance.old_id:
212
+ prefixes.append(f"{instance.old_id}_")
213
+ prefixes = tuple(prefixes)
214
+ for name in names:
215
+ if name.startswith(prefixes) and name.endswith(".gz"):
216
+ self.delete(root / name)
@@ -19,10 +19,11 @@
19
19
  <header class="umap-nav">
20
20
  {% include "umap/branding.html" %}
21
21
  </header>
22
+ <h2>{% trans "To save and easily find your maps, identify yourself." %}</h2>
22
23
  {% if ENABLE_ACCOUNT_LOGIN %}
23
- <h2>
24
- {% trans "Please log in with your account" %}
25
- </h2>
24
+ <h3>
25
+ {% trans "Please log in with your account:" %}
26
+ </h3>
26
27
  <div>
27
28
  {% if form.non_field_errors %}
28
29
  <ul class="form-errors">
@@ -47,9 +48,9 @@
47
48
  </div>
48
49
  {% endif %}
49
50
  {% if backends.backends|length %}
50
- <h2>
51
- {% trans "Please choose a provider" %}
52
- </h2>
51
+ <h3>
52
+ {% trans "Please choose a provider:" %}
53
+ </h3>
53
54
  <div>
54
55
  <ul class="login-grid block-grid">
55
56
  {% for name in backends.backends %}
@@ -24,6 +24,8 @@
24
24
  <link rel="stylesheet" href="{% static 'umap/font.css' %}" />
25
25
  <link rel="stylesheet" href="{% static 'umap/css/icon.css' %}" />
26
26
  <link rel="stylesheet" href="{% static 'umap/base.css' %}" />
27
+ <link rel="stylesheet" href="{% static 'umap/css/popup.css' %}" />
28
+ <link rel="stylesheet" href="{% static 'umap/css/form.css' %}" />
27
29
  <link rel="stylesheet" href="{% static 'umap/content.css' %}" />
28
30
  <link rel="stylesheet" href="{% static 'umap/nav.css' %}" />
29
31
  <link rel="stylesheet" href="{% static 'umap/map.css' %}" />
@@ -35,4 +37,5 @@
35
37
  <link rel="stylesheet" href="{% static 'umap/css/dialog.css' %}" />
36
38
  <link rel="stylesheet" href="{% static 'umap/css/importers.css' %}" />
37
39
  <link rel="stylesheet" href="{% static 'umap/css/tableeditor.css' %}" />
40
+ <link rel="stylesheet" href="{% static 'umap/css/bar.css' %}" />
38
41
  <link rel="stylesheet" href="{% static 'umap/theme.css' %}" />
@@ -41,5 +41,4 @@
41
41
  <script src="{% static 'umap/js/umap.core.js' %}" defer></script>
42
42
  <script src="{% static 'umap/js/umap.forms.js' %}" defer></script>
43
43
  <script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
44
- <script src="{% static 'umap/js/umap.js' %}" defer></script>
45
- <script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
44
+ <script type="module" src="{% static 'umap/js/components/fragment.js' %}" defer></script>
@@ -1,12 +1,11 @@
1
- {% load umap_tags %}
1
+ {% load umap_tags static %}
2
2
 
3
3
  {% include "umap/messages.html" %}
4
4
  <div id="map">
5
5
  </div>
6
6
  <!-- djlint:off -->
7
- <script defer type="text/javascript">
8
- window.addEventListener('DOMContentLoaded', (event) => {
9
- U.MAP = new U.Map("map", {{ map_settings|notag|safe }})
10
- })
7
+ <script defer type="module">
8
+ import Umap from '{% static "umap/js/modules/umap.js" %}'
9
+ U.MAP = new Umap("map", {{ map_settings|notag|safe }})
11
10
  </script>
12
11
  <!-- djlint:on -->
@@ -1,6 +1,6 @@
1
1
  {% extends "umap/content.html" %}
2
2
 
3
- {% load i18n %}
3
+ {% load i18n static %}
4
4
 
5
5
  {% block head_title %}
6
6
  {{ SITE_NAME }} - {% trans "My Dashboard" %}
@@ -45,23 +45,22 @@
45
45
  {% endblock maincontent %}
46
46
  {% block bottom_js %}
47
47
  {{ block.super }}
48
- <script type="text/javascript">
49
- !(function () {
50
- const CACHE = {}
51
- for (const mapOpener of document.querySelectorAll("button.map-opener")) {
52
- mapOpener.addEventListener('click', (event) => {
53
- const button = event.target.closest('button')
54
- button.nextElementSibling.showModal()
55
- const mapId = button.dataset.mapId
56
- if (!document.querySelector(`#${mapId}_target`).children.length) {
57
- const previewSettings = JSON.parse(document.getElementById(mapId).textContent)
58
- const map = new U.Map(`${mapId}_target`, previewSettings)
59
- CACHE[mapId] = map
60
- } else {
61
- CACHE[mapId].invalidateSize()
62
- }
63
- })
64
- }
65
- })()
48
+ <script type="module">
49
+ import Umap from '{% static "umap/js/modules/umap.js" %}'
50
+ const CACHE = {}
51
+ for (const mapOpener of document.querySelectorAll("button.map-opener")) {
52
+ mapOpener.addEventListener('click', (event) => {
53
+ const button = event.target.closest('button')
54
+ button.nextElementSibling.showModal()
55
+ const mapId = button.dataset.mapId
56
+ if (!document.querySelector(`#${mapId}_target`).children.length) {
57
+ const previewSettings = JSON.parse(document.getElementById(mapId).textContent)
58
+ const map = new Umap(`${mapId}_target`, previewSettings)
59
+ CACHE[mapId] = map
60
+ } else {
61
+ CACHE[mapId].invalidateSize()
62
+ }
63
+ })
64
+ }
66
65
  </script>
67
66
  {% endblock bottom_js %}
umap/tests/base.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import copy
2
2
  import json
3
+ import uuid
3
4
  from pathlib import Path
4
5
 
5
6
  import factory
@@ -41,7 +42,7 @@ class LicenceFactory(factory.django.DjangoModelFactory):
41
42
 
42
43
  class TileLayerFactory(factory.django.DjangoModelFactory):
43
44
  name = "Test zoom layer"
44
- url_template = "https://{s}.test.org/osmfr/{z}/{x}/{y}.png"
45
+ url_template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
45
46
  attribution = "Test layer attribution"
46
47
 
47
48
  class Meta:
@@ -101,6 +102,7 @@ class MapFactory(factory.django.DjangoModelFactory):
101
102
 
102
103
  licence = factory.SubFactory(LicenceFactory)
103
104
  owner = factory.SubFactory(UserFactory)
105
+ share_status = Map.PUBLIC
104
106
 
105
107
  @classmethod
106
108
  def _adjust_kwargs(cls, **kwargs):
@@ -114,6 +116,7 @@ class MapFactory(factory.django.DjangoModelFactory):
114
116
 
115
117
 
116
118
  class DataLayerFactory(factory.django.DjangoModelFactory):
119
+ uuid = factory.LazyFunction(lambda: uuid.uuid4())
117
120
  map = factory.SubFactory(MapFactory)
118
121
  name = "test datalayer"
119
122
  description = "test description"
@@ -154,5 +157,6 @@ def login_required(response):
154
157
 
155
158
 
156
159
  def mock_tiles(route):
160
+ print("Intercepted route", route.request.url)
157
161
  path = Path(__file__).parent / "fixtures/empty_tile.png"
158
162
  route.fulfill(path=path)
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import subprocess
3
4
  import time
4
5
  from pathlib import Path
@@ -25,7 +26,7 @@ def set_timeout(context):
25
26
  @pytest.fixture(autouse=True)
26
27
  def mock_osm_tiles(page):
27
28
  if not bool(os.environ.get("PWDEBUG", False)):
28
- page.route("*/**/osmfr/**", mock_tiles)
29
+ page.route(re.compile(r".*tile\..*"), mock_tiles)
29
30
 
30
31
 
31
32
  @pytest.fixture
@@ -76,8 +76,6 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
76
76
  edit_permissions = owner_session.get_by_title("Update permissions and editors")
77
77
  expect(edit_permissions).to_be_visible()
78
78
  edit_permissions.click()
79
- select = owner_session.locator(".umap-field-share_status select")
80
- expect(select).to_be_hidden()
81
79
  owner_field = owner_session.locator(".umap-field-owner")
82
80
  expect(owner_field).to_be_hidden()
83
81
  editors_field = owner_session.locator(".umap-field-editors input")
@@ -92,8 +90,15 @@ def test_owner_permissions_form(map, datalayer, live_server, owner_session):
92
90
  ".datalayer-permissions select[name='edit_status'] option:checked"
93
91
  )
94
92
  expect(option).to_have_text("Inherit")
95
- # Those fields should not be present in anonymous maps
96
- expect(owner_session.locator(".umap-field-share_status select")).to_be_hidden()
93
+ expect(owner_session.locator(".umap-field-share_status select")).to_be_visible()
94
+ options = [
95
+ int(option.get_attribute("value"))
96
+ for option in owner_session.locator(
97
+ ".umap-field-share_status select option"
98
+ ).all()
99
+ ]
100
+ assert options == [Map.DRAFT, Map.PUBLIC]
101
+ # This field should not be present in anonymous maps
97
102
  expect(owner_session.locator(".umap-field-owner")).to_be_hidden()
98
103
 
99
104
 
@@ -135,15 +140,15 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
135
140
  page.get_by_title("Manage layers").click()
136
141
  page.get_by_title("Add a layer").click()
137
142
  page.locator("input[name=name]").fill("Layer 1")
138
- save = page.get_by_role("button", name="Save")
139
- expect(save).to_be_visible()
143
+ expect(
144
+ page.get_by_role("button", name="Visibility: Draft (private)")
145
+ ).to_be_visible()
146
+ expect(page.get_by_role("button", name="Save", exact=True)).to_be_hidden()
140
147
  with page.expect_response(re.compile(r".*/datalayer/create/.*")):
141
- save.click()
148
+ page.get_by_role("button", name="Save draft", exact=True).click()
142
149
  edit_permissions = page.get_by_title("Update permissions and editors")
143
150
  expect(edit_permissions).to_be_visible()
144
151
  edit_permissions.click()
145
- select = page.locator(".umap-field-share_status select")
146
- expect(select).to_be_hidden()
147
152
  owner_field = page.locator(".umap-field-owner")
148
153
  expect(owner_field).to_be_hidden()
149
154
  editors_field = page.locator(".umap-field-editors input")
@@ -157,6 +162,9 @@ def test_can_change_perms_after_create(tilelayer, live_server, page):
157
162
  )
158
163
  expect(option).to_have_text("Inherit")
159
164
  expect(page.get_by_label("Secret edit link:")).to_be_visible()
165
+ page.locator('select[name="share_status"]').select_option("1")
166
+ expect(page.get_by_role("button", name="Save draft", exact=True)).to_be_hidden()
167
+ expect(page.get_by_role("button", name="Save", exact=True)).to_be_visible()
160
168
 
161
169
 
162
170
  def test_alert_message_after_create(
@@ -232,7 +240,7 @@ def test_anonymous_owner_can_delete_the_map(anonymap, live_server, owner_session
232
240
  owner_session.get_by_role("button", name="Delete").click()
233
241
  with owner_session.expect_response(re.compile(r".*/update/delete/.*")):
234
242
  owner_session.get_by_role("button", name="OK").click()
235
- assert not Map.objects.count()
243
+ assert Map.objects.get(pk=anonymap.pk).share_status == Map.DELETED
236
244
 
237
245
 
238
246
  def test_non_owner_cannot_see_delete_button(anonymap, live_server, page):
@@ -15,7 +15,12 @@ DATALAYER_DATA = {
15
15
  "features": [
16
16
  {
17
17
  "type": "Feature",
18
- "properties": {"name": "one point in france", "foo": "point", "bar": "one"},
18
+ "properties": {
19
+ "name": "one point in france",
20
+ "foo": "point",
21
+ "bar": "one",
22
+ "label": "this is label one",
23
+ },
19
24
  "geometry": {"type": "Point", "coordinates": [3.339844, 46.920255]},
20
25
  },
21
26
  {
@@ -24,6 +29,7 @@ DATALAYER_DATA = {
24
29
  "name": "one polygon in greenland",
25
30
  "foo": "polygon",
26
31
  "bar": "two",
32
+ "label": "this is label two",
27
33
  },
28
34
  "geometry": {
29
35
  "type": "Polygon",
@@ -44,6 +50,7 @@ DATALAYER_DATA = {
44
50
  "name": "one line in new zeland",
45
51
  "foo": "line",
46
52
  "bar": "three",
53
+ "label": "this is label three",
47
54
  },
48
55
  "geometry": {
49
56
  "type": "LineString",
@@ -476,3 +483,11 @@ def test_main_toolbox_toggle_all_layers(live_server, map, page):
476
483
  # Should hidden again all layers
477
484
  expect(page.locator(".datalayer.off")).to_have_count(3)
478
485
  expect(markers).to_have_count(0)
486
+
487
+
488
+ def test_honour_the_label_fields_settings(live_server, map, page, bootstrap, settings):
489
+ settings.UMAP_LABEL_KEYS = ["label", "name"]
490
+ page.goto(f"{live_server.url}{map.get_absolute_url()}")
491
+ expect(page.locator(".panel").get_by_text("this is label one")).to_be_visible()
492
+ expect(page.locator(".panel").get_by_text("this is label two")).to_be_visible()
493
+ expect(page.locator(".panel").get_by_text("this is label three")).to_be_visible()
@@ -22,7 +22,7 @@ def test_owner_can_delete_map_after_confirmation(map, live_server, login):
22
22
  with page.expect_navigation():
23
23
  delete_button.click()
24
24
  assert dialog_shown
25
- assert Map.objects.all().count() == 0
25
+ assert Map.objects.get(pk=map.pk).share_status == Map.DELETED
26
26
 
27
27
 
28
28
  def test_dashboard_map_preview(map, live_server, datalayer, login):
@@ -120,7 +120,7 @@ def test_can_change_name(live_server, openmap, page, datalayer):
120
120
  page.locator('input[name="name"]').press("Control+a")
121
121
  page.locator('input[name="name"]').fill("new name")
122
122
  expect(page.locator(".umap-browser .datalayer")).to_contain_text("new name")
123
- expect(page.locator(".umap-is-dirty")).to_be_visible()
123
+ expect(page.locator("body")).to_have_class(re.compile(".*umap-is-dirty.*"))
124
124
  with page.expect_response(re.compile(".*/datalayer/update/.*")):
125
125
  page.get_by_role("button", name="Save").click()
126
126
  saved = DataLayer.objects.last()
@@ -129,32 +129,43 @@ def test_can_change_name(live_server, openmap, page, datalayer):
129
129
 
130
130
 
131
131
  def test_can_create_new_datalayer(live_server, openmap, page, datalayer):
132
+ """
133
+ Test the creation and editing of a new datalayer.
134
+
135
+ This test verifies that:
136
+ 1. A new datalayer can be created and saved.
137
+ 2. The newly created datalayer appears in the UI and is saved in the database.
138
+ 3. Editing the same datalayer updates it instead of creating a new one.
139
+ 4. The UI reflects the changes and the database is updated correctly.
140
+ 5. The 'dirty' state of the map is managed correctly during these operations.
141
+ """
142
+
132
143
  page.goto(
133
144
  f"{live_server.url}{openmap.get_absolute_url()}?edit&onLoadPanel=databrowser"
134
145
  )
135
146
  page.get_by_role("link", name="Manage layers").click()
136
147
  page.get_by_role("button", name="Add a layer").click()
137
148
  page.locator('input[name="name"]').click()
138
- page.locator('input[name="name"]').fill("my new layer")
139
- expect(page.get_by_text("my new layer")).to_be_visible()
149
+ page.locator('input[name="name"]').fill("Layer A")
150
+ expect(page.get_by_text("Layer A")).to_be_visible()
140
151
  with page.expect_response(re.compile(".*/datalayer/create/.*")):
141
152
  page.get_by_role("button", name="Save").click()
142
153
  assert DataLayer.objects.count() == 2
143
154
  saved = DataLayer.objects.last()
144
- assert saved.name == "my new layer"
155
+ assert saved.name == "Layer A"
145
156
  expect(page.locator(".umap-is-dirty")).to_be_hidden()
146
157
  # Edit again, it should not create a new datalayer
147
158
  page.get_by_role("link", name="Manage layers").click()
148
159
  page.locator(".panel.right").get_by_title("Edit", exact=True).first.click()
149
160
  page.locator('input[name="name"]').click()
150
- page.locator('input[name="name"]').fill("my new layer with a new name")
151
- expect(page.get_by_text("my new layer with a new name")).to_be_visible()
161
+ page.locator('input[name="name"]').fill("Layer A with a new name")
162
+ expect(page.get_by_text("Layer A with a new name")).to_be_visible()
152
163
  page.get_by_role("button", name="Save").click()
153
164
  with page.expect_response(re.compile(".*/datalayer/update/.*")):
154
165
  page.get_by_role("button", name="Save").click()
155
166
  assert DataLayer.objects.count() == 2
156
167
  saved = DataLayer.objects.last()
157
- assert saved.name == "my new layer with a new name"
168
+ assert saved.name == "Layer A with a new name"
158
169
  expect(page.locator(".umap-is-dirty")).to_be_hidden()
159
170
 
160
171
 
@@ -71,9 +71,8 @@ def test_umap_import_from_file(live_server, tilelayer, page):
71
71
  expect(nonloaded).to_have_count(1)
72
72
 
73
73
 
74
+ @pytest.mark.skip
74
75
  def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
75
- page.route("https://tile.openstreetmap.fr/hot/**", mock_tiles)
76
-
77
76
  settings.UMAP_ALLOW_ANONYMOUS = True
78
77
  page.goto(f"{live_server.url}/map/new/")
79
78
  page.get_by_role("button", name="Open browser").click()
@@ -94,11 +93,11 @@ def test_umap_import_from_textarea(live_server, tilelayer, page, settings):
94
93
  expect(
95
94
  page.locator('img[src="https://tile.openstreetmap.fr/hot/6/32/21.png"]')
96
95
  ).to_be_visible()
97
- # Should not have imported umap_id, while in the file options
98
- assert not page.evaluate("U.MAP.options.umap_id")
96
+ # Should not have imported id, while in the file options
97
+ assert not page.evaluate("U.MAP.properties.id")
99
98
  with page.expect_response(re.compile(r".*/datalayer/create/.*")):
100
99
  page.get_by_role("button", name="Save").click()
101
- assert page.evaluate("U.MAP.options.umap_id")
100
+ assert page.evaluate("U.MAP.properties.id")
102
101
 
103
102
 
104
103
  def test_import_geojson_from_textarea(tilelayer, live_server, page):
@@ -607,6 +606,7 @@ def test_overpass_import_with_bbox(page, live_server, tilelayer, settings):
607
606
  }
608
607
  page.goto(f"{live_server.url}/map/new/")
609
608
  page.get_by_role("link", name="Import data").click()
609
+ page.get_by_role("button", name="Import helpers").click()
610
610
  page.get_by_role("button", name="Overpass").click()
611
611
  page.get_by_placeholder("amenity=drinking_water").fill("building")
612
612
  page.get_by_role("button", name="Choose this data").click()
@@ -657,6 +657,7 @@ def test_overpass_import_retains_boundary(page, live_server, tilelayer, settings
657
657
  page.route(re.compile("https://foobar.io/api.*"), handle)
658
658
  page.goto(f"{live_server.url}/map/new/")
659
659
  page.get_by_role("link", name="Import data").click()
660
+ page.get_by_role("button", name="Import helpers").click()
660
661
  page.get_by_role("button", name="Overpass").click()
661
662
  page.get_by_placeholder("amenity=drinking_water").fill("building")
662
663
  page.get_by_placeholder("Type area name, or let empty").click()
@@ -669,6 +670,7 @@ def test_overpass_import_retains_boundary(page, live_server, tilelayer, settings
669
670
  expect(page.get_by_placeholder("Provide an URL here")).to_have_value(
670
671
  "https://my.overpass.io/interpreter?data=[out:json];nwr[building](area:3601393025);out geom;"
671
672
  )
673
+ page.get_by_role("button", name="Import helpers").click()
672
674
  page.get_by_role("button", name="Overpass").click()
673
675
  expect(page.locator("#area")).to_contain_text(
674
676
  "Bray-sur-Seine, Seine-et-Marne, Île-de-France, France"
@@ -710,6 +712,7 @@ def test_import_from_datasets(page, live_server, tilelayer, settings):
710
712
  page.goto(f"{live_server.url}/map/new/")
711
713
  expect(page.locator(".leaflet-marker-icon")).to_be_hidden()
712
714
  page.get_by_role("link", name="Import data").click()
715
+ page.get_by_role("button", name="Import helpers").click()
713
716
  page.get_by_role("button", name="Datasets").click()
714
717
  page.get_by_role("dialog").get_by_role("combobox").select_option(
715
718
  "https://remote.org/data.json"