umap-project 2.5.0__py3-none-any.whl → 2.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of umap-project might be problematic. Click here for more details.

Files changed (276) hide show
  1. umap/__init__.py +1 -1
  2. umap/admin.py +6 -1
  3. umap/context_processors.py +2 -1
  4. umap/decorators.py +13 -2
  5. umap/forms.py +26 -2
  6. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  7. umap/locale/br/LC_MESSAGES/django.po +252 -146
  8. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  9. umap/locale/ca/LC_MESSAGES/django.po +274 -162
  10. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  11. umap/locale/cs_CZ/LC_MESSAGES/django.po +261 -150
  12. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  13. umap/locale/de/LC_MESSAGES/django.po +299 -187
  14. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  15. umap/locale/el/LC_MESSAGES/django.po +215 -159
  16. umap/locale/en/LC_MESSAGES/django.po +211 -155
  17. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/es/LC_MESSAGES/django.po +255 -144
  19. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/eu/LC_MESSAGES/django.po +254 -198
  21. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  22. umap/locale/fa_IR/LC_MESSAGES/django.po +347 -235
  23. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  24. umap/locale/fr/LC_MESSAGES/django.po +216 -160
  25. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  26. umap/locale/hu/LC_MESSAGES/django.po +215 -159
  27. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  28. umap/locale/it/LC_MESSAGES/django.po +252 -146
  29. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  30. umap/locale/ms/LC_MESSAGES/django.po +252 -146
  31. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  32. umap/locale/pl/LC_MESSAGES/django.po +254 -148
  33. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  34. umap/locale/pt/LC_MESSAGES/django.po +215 -159
  35. umap/locale/sv/LC_MESSAGES/django.mo +0 -0
  36. umap/locale/sv/LC_MESSAGES/django.po +254 -143
  37. umap/locale/th_TH/LC_MESSAGES/django.mo +0 -0
  38. umap/locale/th_TH/LC_MESSAGES/django.po +125 -70
  39. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  40. umap/locale/zh_TW/LC_MESSAGES/django.po +256 -145
  41. umap/migrations/0022_add_team.py +94 -0
  42. umap/models.py +45 -10
  43. umap/settings/__init__.py +2 -0
  44. umap/settings/base.py +9 -2
  45. umap/static/umap/base.css +32 -41
  46. umap/static/umap/content.css +19 -25
  47. umap/static/umap/css/icon.css +63 -37
  48. umap/static/umap/css/importers.css +1 -1
  49. umap/static/umap/css/slideshow.css +7 -5
  50. umap/static/umap/css/tableeditor.css +4 -3
  51. umap/static/umap/img/16-white.svg +1 -4
  52. umap/static/umap/img/16.svg +2 -6
  53. umap/static/umap/img/24-white.svg +4 -4
  54. umap/static/umap/img/24.svg +6 -6
  55. umap/static/umap/img/source/16-white.svg +2 -5
  56. umap/static/umap/img/source/16.svg +3 -7
  57. umap/static/umap/img/source/24-white.svg +7 -14
  58. umap/static/umap/img/source/24.svg +10 -17
  59. umap/static/umap/js/components/alerts/alert.css +20 -8
  60. umap/static/umap/js/modules/autocomplete.js +8 -12
  61. umap/static/umap/js/modules/browser.js +4 -3
  62. umap/static/umap/js/modules/caption.js +9 -11
  63. umap/static/umap/js/modules/data/features.js +993 -0
  64. umap/static/umap/js/modules/data/layer.js +1210 -0
  65. umap/static/umap/js/modules/formatter.js +12 -3
  66. umap/static/umap/js/modules/global.js +21 -5
  67. umap/static/umap/js/modules/importers/overpass.js +22 -8
  68. umap/static/umap/js/modules/permissions.js +280 -0
  69. umap/static/umap/js/{umap.icon.js → modules/rendering/icon.js} +77 -56
  70. umap/static/umap/js/modules/rendering/layers/base.js +105 -0
  71. umap/static/umap/js/modules/rendering/layers/classified.js +484 -0
  72. umap/static/umap/js/modules/rendering/layers/cluster.js +103 -0
  73. umap/static/umap/js/modules/rendering/layers/heat.js +182 -0
  74. umap/static/umap/js/modules/rendering/popup.js +99 -0
  75. umap/static/umap/js/modules/rendering/template.js +217 -0
  76. umap/static/umap/js/modules/rendering/ui.js +610 -0
  77. umap/static/umap/js/modules/rules.js +16 -3
  78. umap/static/umap/js/modules/schema.js +25 -1
  79. umap/static/umap/js/modules/share.js +66 -45
  80. umap/static/umap/js/modules/sync/updaters.js +9 -10
  81. umap/static/umap/js/modules/tableeditor.js +7 -7
  82. umap/static/umap/js/modules/ui/dialog.js +8 -4
  83. umap/static/umap/js/modules/utils.js +22 -13
  84. umap/static/umap/js/umap.controls.js +80 -146
  85. umap/static/umap/js/umap.core.js +9 -9
  86. umap/static/umap/js/umap.forms.js +41 -17
  87. umap/static/umap/js/umap.js +72 -65
  88. umap/static/umap/locale/am_ET.js +8 -2
  89. umap/static/umap/locale/am_ET.json +8 -2
  90. umap/static/umap/locale/ar.js +8 -2
  91. umap/static/umap/locale/ar.json +8 -2
  92. umap/static/umap/locale/ast.js +8 -2
  93. umap/static/umap/locale/ast.json +8 -2
  94. umap/static/umap/locale/bg.js +8 -2
  95. umap/static/umap/locale/bg.json +8 -2
  96. umap/static/umap/locale/br.js +42 -36
  97. umap/static/umap/locale/br.json +42 -36
  98. umap/static/umap/locale/ca.js +67 -61
  99. umap/static/umap/locale/ca.json +67 -61
  100. umap/static/umap/locale/cs_CZ.js +8 -2
  101. umap/static/umap/locale/cs_CZ.json +8 -2
  102. umap/static/umap/locale/da.js +8 -2
  103. umap/static/umap/locale/da.json +8 -2
  104. umap/static/umap/locale/de.js +143 -137
  105. umap/static/umap/locale/de.json +143 -137
  106. umap/static/umap/locale/el.js +54 -48
  107. umap/static/umap/locale/el.json +54 -48
  108. umap/static/umap/locale/en.js +10 -2
  109. umap/static/umap/locale/en.json +10 -2
  110. umap/static/umap/locale/en_US.json +8 -2
  111. umap/static/umap/locale/es.js +8 -2
  112. umap/static/umap/locale/es.json +8 -2
  113. umap/static/umap/locale/et.js +8 -2
  114. umap/static/umap/locale/et.json +8 -2
  115. umap/static/umap/locale/eu.js +346 -338
  116. umap/static/umap/locale/eu.json +346 -338
  117. umap/static/umap/locale/fa_IR.js +415 -407
  118. umap/static/umap/locale/fa_IR.json +415 -407
  119. umap/static/umap/locale/fi.js +8 -2
  120. umap/static/umap/locale/fi.json +8 -2
  121. umap/static/umap/locale/fr.js +11 -3
  122. umap/static/umap/locale/fr.json +11 -3
  123. umap/static/umap/locale/gl.js +8 -2
  124. umap/static/umap/locale/gl.json +8 -2
  125. umap/static/umap/locale/he.js +8 -2
  126. umap/static/umap/locale/he.json +8 -2
  127. umap/static/umap/locale/hr.js +8 -2
  128. umap/static/umap/locale/hr.json +8 -2
  129. umap/static/umap/locale/hu.js +31 -23
  130. umap/static/umap/locale/hu.json +31 -23
  131. umap/static/umap/locale/id.js +8 -2
  132. umap/static/umap/locale/id.json +8 -2
  133. umap/static/umap/locale/is.js +8 -2
  134. umap/static/umap/locale/is.json +8 -2
  135. umap/static/umap/locale/it.js +8 -2
  136. umap/static/umap/locale/it.json +8 -2
  137. umap/static/umap/locale/ja.js +8 -2
  138. umap/static/umap/locale/ja.json +8 -2
  139. umap/static/umap/locale/ko.js +8 -2
  140. umap/static/umap/locale/ko.json +8 -2
  141. umap/static/umap/locale/lt.js +8 -2
  142. umap/static/umap/locale/lt.json +8 -2
  143. umap/static/umap/locale/ms.js +8 -2
  144. umap/static/umap/locale/ms.json +8 -2
  145. umap/static/umap/locale/nl.js +8 -2
  146. umap/static/umap/locale/nl.json +8 -2
  147. umap/static/umap/locale/no.js +8 -2
  148. umap/static/umap/locale/no.json +8 -2
  149. umap/static/umap/locale/pl.js +54 -48
  150. umap/static/umap/locale/pl.json +54 -48
  151. umap/static/umap/locale/pl_PL.json +8 -2
  152. umap/static/umap/locale/pt.js +24 -18
  153. umap/static/umap/locale/pt.json +24 -18
  154. umap/static/umap/locale/pt_BR.js +8 -2
  155. umap/static/umap/locale/pt_BR.json +8 -2
  156. umap/static/umap/locale/pt_PT.js +214 -208
  157. umap/static/umap/locale/pt_PT.json +214 -208
  158. umap/static/umap/locale/ro.js +8 -2
  159. umap/static/umap/locale/ro.json +8 -2
  160. umap/static/umap/locale/ru.js +8 -2
  161. umap/static/umap/locale/ru.json +8 -2
  162. umap/static/umap/locale/sk_SK.js +8 -2
  163. umap/static/umap/locale/sk_SK.json +8 -2
  164. umap/static/umap/locale/sl.js +8 -2
  165. umap/static/umap/locale/sl.json +8 -2
  166. umap/static/umap/locale/sr.js +8 -2
  167. umap/static/umap/locale/sr.json +8 -2
  168. umap/static/umap/locale/sv.js +8 -2
  169. umap/static/umap/locale/sv.json +8 -2
  170. umap/static/umap/locale/th_TH.js +33 -27
  171. umap/static/umap/locale/th_TH.json +33 -27
  172. umap/static/umap/locale/tr.js +8 -2
  173. umap/static/umap/locale/tr.json +8 -2
  174. umap/static/umap/locale/uk_UA.js +8 -2
  175. umap/static/umap/locale/uk_UA.json +8 -2
  176. umap/static/umap/locale/vi.js +8 -2
  177. umap/static/umap/locale/vi.json +8 -2
  178. umap/static/umap/locale/vi_VN.json +8 -2
  179. umap/static/umap/locale/zh.js +8 -2
  180. umap/static/umap/locale/zh.json +8 -2
  181. umap/static/umap/locale/zh_CN.json +8 -2
  182. umap/static/umap/locale/zh_TW.Big5.json +8 -2
  183. umap/static/umap/locale/zh_TW.js +102 -96
  184. umap/static/umap/locale/zh_TW.json +102 -96
  185. umap/static/umap/map.css +111 -108
  186. umap/static/umap/nav.css +19 -10
  187. umap/static/umap/unittests/utils.js +230 -107
  188. umap/static/umap/vars.css +1 -0
  189. umap/static/umap/vendors/csv2geojson/csv2geojson.js +62 -40
  190. umap/static/umap/vendors/editable/Leaflet.Editable.js +2079 -1937
  191. umap/storage.py +4 -3
  192. umap/templates/404.html +5 -1
  193. umap/templates/500.html +3 -1
  194. umap/templates/auth/user_detail.html +8 -2
  195. umap/templates/auth/user_form.html +19 -10
  196. umap/templates/auth/user_stars.html +8 -2
  197. umap/templates/base.html +1 -0
  198. umap/templates/registration/login.html +18 -3
  199. umap/templates/umap/about.html +1 -0
  200. umap/templates/umap/about_summary.html +22 -7
  201. umap/templates/umap/components/alerts/alert.html +42 -21
  202. umap/templates/umap/content.html +2 -0
  203. umap/templates/umap/content_footer.html +7 -3
  204. umap/templates/umap/css.html +1 -0
  205. umap/templates/umap/dashboard_menu.html +15 -0
  206. umap/templates/umap/home.html +14 -4
  207. umap/templates/umap/js.html +4 -9
  208. umap/templates/umap/login_popup_end.html +10 -4
  209. umap/templates/umap/map_detail.html +8 -2
  210. umap/templates/umap/map_fragment.html +3 -1
  211. umap/templates/umap/map_init.html +2 -1
  212. umap/templates/umap/map_list.html +6 -3
  213. umap/templates/umap/map_table.html +36 -12
  214. umap/templates/umap/messages.html +0 -1
  215. umap/templates/umap/navigation.html +2 -1
  216. umap/templates/umap/password_change.html +5 -1
  217. umap/templates/umap/password_change_done.html +8 -2
  218. umap/templates/umap/search.html +8 -2
  219. umap/templates/umap/search_bar.html +1 -0
  220. umap/templates/umap/team_confirm_delete.html +19 -0
  221. umap/templates/umap/team_detail.html +27 -0
  222. umap/templates/umap/team_form.html +60 -0
  223. umap/templates/umap/user_dashboard.html +7 -9
  224. umap/templates/umap/user_teams.html +51 -0
  225. umap/tests/base.py +8 -1
  226. umap/tests/conftest.py +6 -0
  227. umap/tests/fixtures/test_circles_layer.geojson +219 -0
  228. umap/tests/fixtures/test_upload_georss.xml +20 -0
  229. umap/tests/integration/conftest.py +18 -4
  230. umap/tests/integration/helpers.py +12 -0
  231. umap/tests/integration/test_anonymous_owned_map.py +23 -0
  232. umap/tests/integration/test_basics.py +29 -0
  233. umap/tests/integration/test_browser.py +20 -0
  234. umap/tests/integration/test_caption.py +20 -0
  235. umap/tests/integration/test_circles_layer.py +69 -0
  236. umap/tests/integration/test_conditional_rules.py +102 -17
  237. umap/tests/integration/test_draw_polygon.py +138 -13
  238. umap/tests/integration/test_draw_polyline.py +8 -18
  239. umap/tests/integration/test_edit_datalayer.py +3 -3
  240. umap/tests/integration/test_import.py +124 -5
  241. umap/tests/integration/test_owned_map.py +21 -13
  242. umap/tests/integration/test_querystring.py +7 -0
  243. umap/tests/integration/test_team.py +47 -0
  244. umap/tests/integration/test_tilelayer.py +19 -2
  245. umap/tests/integration/test_view_marker.py +28 -1
  246. umap/tests/integration/test_websocket_sync.py +5 -5
  247. umap/tests/test_datalayer.py +32 -7
  248. umap/tests/test_datalayer_views.py +1 -1
  249. umap/tests/test_map.py +30 -4
  250. umap/tests/test_map_views.py +2 -2
  251. umap/tests/test_statics.py +40 -0
  252. umap/tests/test_team_views.py +131 -0
  253. umap/tests/test_views.py +15 -1
  254. umap/urls.py +23 -13
  255. umap/views.py +116 -10
  256. {umap_project-2.5.0.dist-info → umap_project-2.6.0.dist-info}/METADATA +14 -14
  257. {umap_project-2.5.0.dist-info → umap_project-2.6.0.dist-info}/RECORD +260 -253
  258. umap/static/umap/js/umap.datalayer.permissions.js +0 -70
  259. umap/static/umap/js/umap.features.js +0 -1290
  260. umap/static/umap/js/umap.layer.js +0 -1837
  261. umap/static/umap/js/umap.permissions.js +0 -208
  262. umap/static/umap/js/umap.popup.js +0 -341
  263. umap/static/umap/test/TableEditor.js +0 -104
  264. umap/static/umap/vendors/leaflet/leaflet-src.js +0 -14512
  265. umap/static/umap/vendors/leaflet/leaflet-src.js.map +0 -1
  266. umap/static/umap/vendors/leaflet/leaflet.js +0 -6
  267. umap/static/umap/vendors/leaflet/leaflet.js.map +0 -1
  268. umap/static/umap/vendors/markercluster/WhereAreTheJavascriptFiles.txt +0 -5
  269. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js +0 -2718
  270. umap/static/umap/vendors/markercluster/leaflet.markercluster-src.js.map +0 -1
  271. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.css +0 -117
  272. umap/static/umap/vendors/toolbar/leaflet.toolbar-src.js +0 -365
  273. umap/tests/integration/test_statics.py +0 -47
  274. {umap_project-2.5.0.dist-info → umap_project-2.6.0.dist-info}/WHEEL +0 -0
  275. {umap_project-2.5.0.dist-info → umap_project-2.6.0.dist-info}/entry_points.txt +0 -0
  276. {umap_project-2.5.0.dist-info → umap_project-2.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -210,7 +210,7 @@ def test_user_not_allowed_should_not_clone_map(client, map, user, settings):
210
210
 
211
211
  def test_clone_should_set_cloner_as_owner(client, map, user):
212
212
  url = reverse("map_clone", kwargs={"map_id": map.pk})
213
- map.edit_status = map.EDITORS
213
+ map.edit_status = map.COLLABORATORS
214
214
  map.editors.add(user)
215
215
  map.save()
216
216
  client.login(username=user.username, password="123123")
@@ -330,7 +330,7 @@ def test_only_owner_can_delete(client, map, user):
330
330
 
331
331
  def test_map_editors_do_not_see_owner_change_input(client, map, user):
332
332
  map.editors.add(user)
333
- map.edit_status = map.EDITORS
333
+ map.edit_status = map.COLLABORATORS
334
334
  map.save()
335
335
  url = reverse("map_update_permissions", kwargs={"map_id": map.pk})
336
336
  client.login(username=user.username, password="123123")
@@ -0,0 +1,40 @@
1
+ import json
2
+ import shutil
3
+ import tempfile
4
+ from copy import deepcopy
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from django.core.management import call_command
9
+
10
+
11
+ @pytest.fixture
12
+ def staticfiles(settings):
13
+ static_root = tempfile.mkdtemp(prefix="test_static")
14
+ settings.STATIC_ROOT = static_root
15
+ # Make sure settings are properly reset after the test
16
+ settings.STORAGES = deepcopy(settings.STORAGES)
17
+ settings.STORAGES["staticfiles"]["BACKEND"] = (
18
+ "umap.storage.UmapManifestStaticFilesStorage"
19
+ )
20
+ try:
21
+ call_command("collectstatic", "--noinput")
22
+ yield
23
+ finally:
24
+ shutil.rmtree(static_root)
25
+
26
+
27
+ def test_collectstatic_ran_successfully_with_hashes(settings, staticfiles):
28
+ static_root = settings.STATIC_ROOT
29
+ manifest = Path(static_root) / "staticfiles.json"
30
+ assert manifest.exists()
31
+ json_manifest = json.loads(manifest.read_text())
32
+ assert "hash" in json_manifest.keys()
33
+ assert "umap/base.css" in json_manifest["paths"]
34
+ # Hash + the dot ("umap/base.<hash>.css").
35
+ md5_hash_lenght = 12 + 1
36
+ # The value of the manifest must contain the hash (length).
37
+ assert (
38
+ len(json_manifest["paths"]["umap/base.css"])
39
+ == len("umap/base.css") + md5_hash_lenght
40
+ )
@@ -0,0 +1,131 @@
1
+ import pytest
2
+ from django.urls import reverse
3
+
4
+ from umap.models import Team
5
+
6
+ pytestmark = pytest.mark.django_db
7
+
8
+
9
+ def test_can_see_team_maps(client, map, team):
10
+ map.team = team
11
+ map.save()
12
+ url = reverse("team_maps", args=(team.pk,))
13
+ response = client.get(url)
14
+ assert response.status_code == 200
15
+ assert map.name in response.content.decode()
16
+
17
+
18
+ def test_user_can_see_their_teams(client, team, user):
19
+ user.teams.add(team)
20
+ user.save()
21
+ url = reverse("user_teams")
22
+ client.login(username=user.username, password="123123")
23
+ response = client.get(url)
24
+ assert response.status_code == 200
25
+ assert team.name in response.content.decode()
26
+
27
+
28
+ def test_can_create_a_team(client, user):
29
+ assert not Team.objects.count()
30
+ url = reverse("team_new")
31
+ client.login(username=user.username, password="123123")
32
+ response = client.post(url, {"name": "my new team", "members": [user.pk]})
33
+ assert response.status_code == 302
34
+ assert response["Location"] == "/en/me/teams"
35
+ assert Team.objects.count() == 1
36
+ team = Team.objects.first()
37
+ assert team.name == "my new team"
38
+ assert team in user.teams.all()
39
+
40
+
41
+ def test_can_edit_a_team_name(client, user, team):
42
+ user.teams.add(team)
43
+ user.save()
44
+ assert Team.objects.count() == 1
45
+ url = reverse("team_update", args=(team.pk,))
46
+ client.login(username=user.username, password="123123")
47
+ response = client.post(url, {"name": "my new team", "members": [user.pk]})
48
+ assert response.status_code == 302
49
+ assert response["Location"] == "/en/me/teams"
50
+ assert Team.objects.count() == 1
51
+ modified = Team.objects.first()
52
+ assert modified.name == "my new team"
53
+ assert modified in user.teams.all()
54
+
55
+
56
+ def test_can_add_user_to_team(client, user, user2, team):
57
+ user.teams.add(team)
58
+ user.save()
59
+ assert Team.objects.count() == 1
60
+ url = reverse("team_update", args=(team.pk,))
61
+ client.login(username=user.username, password="123123")
62
+ response = client.post(url, {"name": team.name, "members": [user.pk, user2.pk]})
63
+ assert response.status_code == 302
64
+ assert response["Location"] == "/en/me/teams"
65
+ assert Team.objects.count() == 1
66
+ modified = Team.objects.first()
67
+ assert user in modified.users.all()
68
+ assert user2 in modified.users.all()
69
+
70
+
71
+ def test_can_remove_user_from_team(client, user, user2, team):
72
+ user.teams.add(team)
73
+ user.save()
74
+ user2.teams.add(team)
75
+ user2.save()
76
+ assert Team.objects.count() == 1
77
+ url = reverse("team_update", args=(team.pk,))
78
+ client.login(username=user.username, password="123123")
79
+ response = client.post(url, {"name": team.name, "members": [user.pk]})
80
+ assert response.status_code == 302
81
+ assert response["Location"] == "/en/me/teams"
82
+ assert Team.objects.count() == 1
83
+ modified = Team.objects.first()
84
+ assert user in modified.users.all()
85
+ assert user2 not in modified.users.all()
86
+
87
+
88
+ def test_cannot_edit_a_team_if_not_member(client, user, user2, team):
89
+ user.teams.add(team)
90
+ user.save()
91
+ assert Team.objects.count() == 1
92
+ url = reverse("team_update", args=(team.pk,))
93
+ client.login(username=user2.username, password="456456")
94
+ response = client.post(url, {"name": "my new team", "members": [user.pk]})
95
+ assert response.status_code == 403
96
+
97
+
98
+ def test_can_delete_a_team(client, user, team):
99
+ user.teams.add(team)
100
+ user.save()
101
+ assert Team.objects.count() == 1
102
+ url = reverse("team_delete", args=(team.pk,))
103
+ client.login(username=user.username, password="123123")
104
+ response = client.post(url)
105
+ assert response.status_code == 302
106
+ assert response["Location"] == "/en/me/teams"
107
+ assert Team.objects.count() == 0
108
+
109
+
110
+ def test_cannot_delete_a_team_if_not_member(client, user, user2, team):
111
+ user.teams.add(team)
112
+ user.save()
113
+ assert Team.objects.count() == 1
114
+ url = reverse("team_delete", args=(team.pk,))
115
+ client.login(username=user2.username, password="456456")
116
+ response = client.post(url)
117
+ assert response.status_code == 403
118
+ assert Team.objects.count() == 1
119
+
120
+
121
+ def test_cannot_delete_a_team_if_more_than_one_member(client, user, user2, team):
122
+ user.teams.add(team)
123
+ user.save()
124
+ user2.teams.add(team)
125
+ user2.save()
126
+ assert Team.objects.count() == 1
127
+ url = reverse("team_delete", args=(team.pk,))
128
+ client.login(username=user.username, password="123123")
129
+ response = client.post(url)
130
+ assert response.status_code == 400
131
+ assert Team.objects.count() == 1
umap/tests/test_views.py CHANGED
@@ -288,6 +288,20 @@ def test_user_dashboard_display_user_maps(client, map):
288
288
  assert "Owner only" in body
289
289
 
290
290
 
291
+ @pytest.mark.django_db
292
+ def test_user_dashboard_display_user_team_maps(client, map, team, user):
293
+ user.teams.add(team)
294
+ user.save()
295
+ map.team = team
296
+ map.save()
297
+ client.login(username=user.username, password="123123")
298
+ response = client.get(reverse("user_dashboard"))
299
+ assert response.status_code == 200
300
+ body = response.content.decode()
301
+ assert map.name in body
302
+ assert map.get_absolute_url() in body
303
+
304
+
291
305
  @pytest.mark.django_db
292
306
  def test_user_dashboard_display_user_maps_distinct(client, map):
293
307
  # cf https://github.com/umap-project/umap/issues/1325
@@ -474,7 +488,7 @@ def test_websocket_token_returns_a_valid_token_when_authorized(client, user, map
474
488
 
475
489
  @pytest.mark.django_db
476
490
  def test_websocket_token_is_generated_for_editors(client, user, user2, map):
477
- map.edit_status = Map.EDITORS
491
+ map.edit_status = Map.COLLABORATORS
478
492
  map.editors.add(user2)
479
493
  map.save()
480
494
 
umap/urls.py CHANGED
@@ -16,6 +16,7 @@ from .decorators import (
16
16
  can_edit_map,
17
17
  can_view_map,
18
18
  login_required_if_not_anonymous_allowed,
19
+ team_members_only,
19
20
  )
20
21
  from .utils import decorated_patterns
21
22
 
@@ -24,6 +25,10 @@ admin.autodiscover()
24
25
  urlpatterns = [
25
26
  re_path(r"^admin/", admin.site.urls),
26
27
  re_path("", include("social_django.urls", namespace="social")),
28
+ re_path(
29
+ r"^agnocomplete/",
30
+ include(("agnocomplete.urls", "agnocomplete"), namespace="agnocomplete"),
31
+ ),
27
32
  re_path(r"^m/(?P<pk>\d+)/$", views.MapShortUrl.as_view(), name="map_short_url"),
28
33
  re_path(r"^ajax-proxy/$", cache_page(180)(views.ajax_proxy), name="ajax-proxy"),
29
34
  re_path(
@@ -39,7 +44,6 @@ urlpatterns = [
39
44
  name="password_change_done",
40
45
  ),
41
46
  re_path(r"^i18n/", include("django.conf.urls.i18n")),
42
- re_path(r"^agnocomplete/", include("agnocomplete.urls")),
43
47
  re_path(r"^map/oembed/", views.MapOEmbed.as_view(), name="map_oembed"),
44
48
  re_path(
45
49
  r"^map/(?P<map_id>\d+)/download/",
@@ -96,12 +100,12 @@ i18n_urls += decorated_patterns(
96
100
  )
97
101
  i18n_urls += decorated_patterns(
98
102
  [ensure_csrf_cookie],
99
- re_path(r"^map/$", views.MapPreview.as_view(), name="map_preview"),
100
- re_path(r"^map/new/$", views.MapNew.as_view(), name="map_new"),
103
+ path("map/", views.MapPreview.as_view(), name="map_preview"),
104
+ path("map/new/", views.MapNew.as_view(), name="map_new"),
101
105
  )
102
106
  i18n_urls += decorated_patterns(
103
107
  [login_required_if_not_anonymous_allowed, never_cache],
104
- re_path(r"^map/create/$", views.MapCreate.as_view(), name="map_create"),
108
+ path("map/create/", views.MapCreate.as_view(), name="map_create"),
105
109
  )
106
110
  i18n_urls += decorated_patterns(
107
111
  [login_required],
@@ -110,9 +114,16 @@ i18n_urls += decorated_patterns(
110
114
  views.ToggleMapStarStatus.as_view(),
111
115
  name="map_star",
112
116
  ),
113
- re_path(r"^me$", views.user_dashboard, name="user_dashboard"),
114
- re_path(r"^me/profile$", views.user_profile, name="user_profile"),
115
- re_path(r"^me/download$", views.user_download, name="user_download"),
117
+ path("me", views.user_dashboard, name="user_dashboard"),
118
+ path("me/profile", views.user_profile, name="user_profile"),
119
+ path("me/download", views.user_download, name="user_download"),
120
+ path("me/teams", views.UserTeams.as_view(), name="user_teams"),
121
+ path("team/create/", views.TeamNew.as_view(), name="team_new"),
122
+ )
123
+ i18n_urls += decorated_patterns(
124
+ [login_required, team_members_only],
125
+ path("team/<int:pk>/edit/", views.TeamUpdate.as_view(), name="team_update"),
126
+ path("team/<int:pk>/delete/", views.TeamDelete.as_view(), name="team_delete"),
116
127
  )
117
128
  map_urls = [
118
129
  re_path(
@@ -179,14 +190,13 @@ datalayer_urls = [
179
190
  i18n_urls += decorated_patterns([can_edit_map, never_cache], *map_urls)
180
191
  i18n_urls += decorated_patterns([never_cache], *datalayer_urls)
181
192
  urlpatterns += i18n_patterns(
182
- re_path(r"^$", views.home, name="home"),
183
- re_path(
184
- r"^showcase/$", cache_page(24 * 60 * 60)(views.showcase), name="maps_showcase"
185
- ),
186
- re_path(r"^search/$", views.search, name="search"),
187
- re_path(r"^about/$", views.about, name="about"),
193
+ path("", views.home, name="home"),
194
+ path("showcase/", cache_page(24 * 60 * 60)(views.showcase), name="maps_showcase"),
195
+ path("search/", views.search, name="search"),
196
+ path("about/", views.about, name="about"),
188
197
  re_path(r"^user/(?P<identifier>.+)/stars/$", views.user_stars, name="user_stars"),
189
198
  re_path(r"^user/(?P<identifier>.+)/$", views.user_maps, name="user_maps"),
199
+ path("team/<int:pk>/", views.TeamMaps.as_view(), name="team_maps"),
190
200
  re_path(r"", include(i18n_urls)),
191
201
  )
192
202
  urlpatterns += (
umap/views.py CHANGED
@@ -62,10 +62,11 @@ from .forms import (
62
62
  FlatErrorList,
63
63
  MapSettingsForm,
64
64
  SendLinkForm,
65
+ TeamForm,
65
66
  UpdateMapPermissionsForm,
66
67
  UserProfileForm,
67
68
  )
68
- from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer
69
+ from .models import DataLayer, Licence, Map, Pictogram, Star, Team, TileLayer
69
70
  from .utils import (
70
71
  ConflictError,
71
72
  _urls_for_js,
@@ -188,6 +189,70 @@ class About(Home):
188
189
  about = About.as_view()
189
190
 
190
191
 
192
+ class TeamNew(CreateView):
193
+ model = Team
194
+ fields = ["name", "description"]
195
+ success_url = reverse_lazy("user_teams")
196
+
197
+ def form_valid(self, form):
198
+ response = super().form_valid(form)
199
+ self.request.user.teams.add(self.object)
200
+ self.request.user.save()
201
+ return response
202
+
203
+
204
+ class TeamUpdate(UpdateView):
205
+ model = Team
206
+ form_class = TeamForm
207
+ success_url = reverse_lazy("user_teams")
208
+
209
+ def get_initial(self):
210
+ initial = super().get_initial()
211
+ initial["members"] = self.object.users.all()
212
+ return initial
213
+
214
+ def form_valid(self, form):
215
+ actual = self.object.users.all()
216
+ wanted = form.cleaned_data["members"]
217
+ for user in wanted:
218
+ if user not in actual:
219
+ user.teams.add(self.object)
220
+ user.save()
221
+ for user in actual:
222
+ if user not in wanted:
223
+ user.teams.remove(self.object)
224
+ user.save()
225
+ return super().form_valid(form)
226
+
227
+
228
+ class TeamDelete(DeleteView):
229
+ model = Team
230
+ success_url = reverse_lazy("user_teams")
231
+
232
+ def form_valid(self, form):
233
+ if self.object.users.count() > 1:
234
+ return HttpResponseBadRequest(
235
+ _("Cannot delete a team with more than one member")
236
+ )
237
+ messages.info(
238
+ self.request,
239
+ _("Team “%(name)s” has been deleted") % {"name": self.object.name},
240
+ )
241
+ return super().form_valid(form)
242
+
243
+
244
+ class UserTeams(DetailView):
245
+ model = User
246
+ template_name = "umap/user_teams.html"
247
+
248
+ def get_object(self):
249
+ return self.get_queryset().get(pk=self.request.user.pk)
250
+
251
+ def get_context_data(self, **kwargs):
252
+ kwargs.update({"teams": self.object.teams.all()})
253
+ return super().get_context_data(**kwargs)
254
+
255
+
191
256
  class UserProfile(UpdateView):
192
257
  model = User
193
258
  form_class = UserProfileForm
@@ -247,6 +312,21 @@ class UserStars(UserMaps):
247
312
  user_stars = UserStars.as_view()
248
313
 
249
314
 
315
+ class TeamMaps(PaginatorMixin, DetailView):
316
+ model = Team
317
+ list_template_name = "umap/map_list.html"
318
+ context_object_name = "current_team"
319
+
320
+ def get_maps(self):
321
+ return Map.public.filter(team=self.object).order_by("-modified_at")
322
+
323
+ def get_context_data(self, **kwargs):
324
+ kwargs.update(
325
+ {"maps": self.paginate(self.get_maps(), settings.UMAP_MAPS_PER_PAGE)}
326
+ )
327
+ return super().get_context_data(**kwargs)
328
+
329
+
250
330
  class SearchMixin:
251
331
  def get_search_queryset(self, **kwargs):
252
332
  q = self.request.GET.get("q")
@@ -293,7 +373,12 @@ class UserDashboard(PaginatorMixin, DetailView, SearchMixin):
293
373
 
294
374
  def get_maps(self):
295
375
  qs = self.get_search_queryset() or Map.objects.all()
296
- qs = qs.filter(owner=self.object).union(qs.filter(editors=self.object))
376
+ teams = self.object.teams.all()
377
+ qs = (
378
+ qs.filter(owner=self.object)
379
+ .union(qs.filter(editors=self.object))
380
+ .union(qs.filter(team__in=teams))
381
+ )
297
382
  return qs.order_by("-modified_at")
298
383
 
299
384
  def get_context_data(self, **kwargs):
@@ -456,6 +541,23 @@ def simple_json_response(**kwargs):
456
541
  # ############## #
457
542
 
458
543
 
544
+ class SessionMixin:
545
+ def get_user_data(self):
546
+ data = {}
547
+ user = self.request.user
548
+ if hasattr(self, "object"):
549
+ data["is_owner"] = self.object.is_owner(user, self.request)
550
+ if user.is_anonymous:
551
+ return data
552
+ return {
553
+ "id": user.pk,
554
+ "name": str(self.request.user),
555
+ "url": reverse("user_dashboard"),
556
+ "teams": [team.get_metadata() for team in user.teams.all()],
557
+ **data,
558
+ }
559
+
560
+
459
561
  class FormLessEditMixin:
460
562
  http_method_names = [
461
563
  "post",
@@ -470,7 +572,7 @@ class FormLessEditMixin:
470
572
  return self.get_form_class()(**kwargs)
471
573
 
472
574
 
473
- class MapDetailMixin:
575
+ class MapDetailMixin(SessionMixin):
474
576
  model = Map
475
577
  pk_url_kwarg = "map_id"
476
578
 
@@ -522,12 +624,7 @@ class MapDetailMixin:
522
624
  if self.get_short_url():
523
625
  properties["shortUrl"] = self.get_short_url()
524
626
 
525
- if not user.is_anonymous:
526
- properties["user"] = {
527
- "id": user.pk,
528
- "name": str(user),
529
- "url": reverse("user_dashboard"),
530
- }
627
+ properties["user"] = self.get_user_data()
531
628
  return properties
532
629
 
533
630
  def get_context_data(self, **kwargs):
@@ -595,6 +692,8 @@ class PermissionsMixin:
595
692
  {"id": editor.pk, "name": str(editor)}
596
693
  for editor in self.object.editors.all()
597
694
  ]
695
+ if self.object.team:
696
+ permissions["team"] = self.object.team.get_metadata()
598
697
  if not self.object.owner and self.object.is_anonymous_owner(self.request):
599
698
  permissions["anonymous_edit_url"] = self.object.get_anonymous_edit_url()
600
699
  return permissions
@@ -659,6 +758,12 @@ class MapView(MapDetailMixin, PermissionsMixin, DetailView):
659
758
  map_settings["properties"] = {}
660
759
  map_settings["properties"]["name"] = self.object.name
661
760
  map_settings["properties"]["permissions"] = self.get_permissions()
761
+ author = self.object.get_author()
762
+ if author:
763
+ map_settings["properties"]["author"] = {
764
+ "name": str(author),
765
+ "url": author.get_url(),
766
+ }
662
767
  return map_settings
663
768
 
664
769
  def is_starred(self):
@@ -755,7 +860,7 @@ class MapPreview(MapDetailMixin, TemplateView):
755
860
  return properties
756
861
 
757
862
 
758
- class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
863
+ class MapCreate(FormLessEditMixin, PermissionsMixin, SessionMixin, CreateView):
759
864
  model = Map
760
865
  form_class = MapSettingsForm
761
866
 
@@ -772,6 +877,7 @@ class MapCreate(FormLessEditMixin, PermissionsMixin, CreateView):
772
877
  id=self.object.pk,
773
878
  url=self.object.get_absolute_url(),
774
879
  permissions=permissions,
880
+ user=self.get_user_data(),
775
881
  )
776
882
  if not self.request.user.is_authenticated:
777
883
  key, value = self.object.signed_cookie_elements
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: umap-project
3
- Version: 2.5.0
3
+ Version: 2.6.0
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,35 +19,35 @@ 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.7
22
+ Requires-Dist: django==5.1.1
23
23
  Requires-Dist: pillow==10.4.0
24
24
  Requires-Dist: psycopg==3.2.1
25
- Requires-Dist: pydantic==2.8.2
25
+ Requires-Dist: pydantic==2.9.1
26
26
  Requires-Dist: rcssmin==1.1.2
27
27
  Requires-Dist: requests==2.32.3
28
28
  Requires-Dist: rjsmin==1.2.2
29
- Requires-Dist: social-auth-app-django==5.4.1
29
+ Requires-Dist: social-auth-app-django==5.4.2
30
30
  Requires-Dist: social-auth-core==4.5.4
31
- Requires-Dist: websockets==12.0
31
+ Requires-Dist: websockets==13.0.1
32
32
  Provides-Extra: dev
33
- Requires-Dist: djlint==1.34.1; extra == 'dev'
33
+ Requires-Dist: djlint==1.35.2; extra == 'dev'
34
34
  Requires-Dist: hatch==1.12.0; extra == 'dev'
35
35
  Requires-Dist: isort==5.13.2; extra == 'dev'
36
- Requires-Dist: mkdocs-material==9.5.28; extra == 'dev'
36
+ Requires-Dist: mkdocs-material==9.5.34; extra == 'dev'
37
37
  Requires-Dist: mkdocs-static-i18n==1.2.3; extra == 'dev'
38
- Requires-Dist: mkdocs==1.6.0; extra == 'dev'
39
- Requires-Dist: pymdown-extensions==10.8.1; extra == 'dev'
40
- Requires-Dist: ruff==0.5.1; extra == 'dev'
38
+ Requires-Dist: mkdocs==1.6.1; extra == 'dev'
39
+ Requires-Dist: pymdown-extensions==10.9; extra == 'dev'
40
+ Requires-Dist: ruff==0.6.4; extra == 'dev'
41
41
  Requires-Dist: vermin==1.6.0; extra == 'dev'
42
42
  Provides-Extra: docker
43
43
  Requires-Dist: uwsgi==2.0.26; extra == 'docker'
44
44
  Provides-Extra: test
45
- Requires-Dist: factory-boy==3.2.1; extra == 'test'
45
+ Requires-Dist: factory-boy==3.3.1; extra == 'test'
46
46
  Requires-Dist: playwright>=1.39; extra == 'test'
47
- Requires-Dist: pytest-django==4.8.0; extra == 'test'
48
- Requires-Dist: pytest-playwright==0.5.1; extra == 'test'
47
+ Requires-Dist: pytest-django==4.9.0; extra == 'test'
48
+ Requires-Dist: pytest-playwright==0.5.2; extra == 'test'
49
49
  Requires-Dist: pytest-xdist<4,>=3.5.0; extra == 'test'
50
- Requires-Dist: pytest==8.2.2; extra == 'test'
50
+ Requires-Dist: pytest==8.3.2; extra == 'test'
51
51
  Description-Content-Type: text/markdown
52
52
 
53
53
  # uMap project