umap-project 3.0.5__py3-none-any.whl → 3.1.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (145) hide show
  1. umap/__init__.py +1 -1
  2. umap/forms.py +1 -1
  3. umap/locale/br/LC_MESSAGES/django.mo +0 -0
  4. umap/locale/br/LC_MESSAGES/django.po +219 -72
  5. umap/locale/ca/LC_MESSAGES/django.mo +0 -0
  6. umap/locale/ca/LC_MESSAGES/django.po +286 -95
  7. umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  8. umap/locale/cs_CZ/LC_MESSAGES/django.po +211 -65
  9. umap/locale/da/LC_MESSAGES/django.mo +0 -0
  10. umap/locale/da/LC_MESSAGES/django.po +394 -202
  11. umap/locale/de/LC_MESSAGES/django.mo +0 -0
  12. umap/locale/de/LC_MESSAGES/django.po +146 -75
  13. umap/locale/el/LC_MESSAGES/django.mo +0 -0
  14. umap/locale/el/LC_MESSAGES/django.po +125 -59
  15. umap/locale/en/LC_MESSAGES/django.mo +0 -0
  16. umap/locale/en/LC_MESSAGES/django.po +124 -58
  17. umap/locale/es/LC_MESSAGES/django.mo +0 -0
  18. umap/locale/es/LC_MESSAGES/django.po +125 -59
  19. umap/locale/et/LC_MESSAGES/django.mo +0 -0
  20. umap/locale/et/LC_MESSAGES/django.po +210 -64
  21. umap/locale/eu/LC_MESSAGES/django.mo +0 -0
  22. umap/locale/eu/LC_MESSAGES/django.po +212 -65
  23. umap/locale/fa_IR/LC_MESSAGES/django.mo +0 -0
  24. umap/locale/fa_IR/LC_MESSAGES/django.po +286 -95
  25. umap/locale/fr/LC_MESSAGES/django.mo +0 -0
  26. umap/locale/fr/LC_MESSAGES/django.po +125 -59
  27. umap/locale/gl/LC_MESSAGES/django.mo +0 -0
  28. umap/locale/gl/LC_MESSAGES/django.po +212 -66
  29. umap/locale/hu/LC_MESSAGES/django.mo +0 -0
  30. umap/locale/hu/LC_MESSAGES/django.po +148 -78
  31. umap/locale/is/LC_MESSAGES/django.mo +0 -0
  32. umap/locale/is/LC_MESSAGES/django.po +130 -60
  33. umap/locale/it/LC_MESSAGES/django.mo +0 -0
  34. umap/locale/it/LC_MESSAGES/django.po +219 -73
  35. umap/locale/ms/LC_MESSAGES/django.mo +0 -0
  36. umap/locale/ms/LC_MESSAGES/django.po +289 -98
  37. umap/locale/nl/LC_MESSAGES/django.mo +0 -0
  38. umap/locale/nl/LC_MESSAGES/django.po +128 -61
  39. umap/locale/pl/LC_MESSAGES/django.mo +0 -0
  40. umap/locale/pl/LC_MESSAGES/django.po +287 -96
  41. umap/locale/pt/LC_MESSAGES/django.mo +0 -0
  42. umap/locale/pt/LC_MESSAGES/django.po +211 -65
  43. umap/locale/zh_TW/LC_MESSAGES/django.mo +0 -0
  44. umap/locale/zh_TW/LC_MESSAGES/django.po +212 -66
  45. umap/management/commands/migrate_to_S3.py +42 -20
  46. umap/management/commands/purge_old_versions.py +63 -0
  47. umap/management/commands/switch_user.py +52 -0
  48. umap/managers.py +29 -2
  49. umap/middleware.py +1 -1
  50. umap/migrations/0028_map_is_template.py +21 -0
  51. umap/models.py +14 -4
  52. umap/settings/base.py +22 -0
  53. umap/static/umap/base.css +4 -2
  54. umap/static/umap/content.css +1 -1
  55. umap/static/umap/css/dialog.css +5 -2
  56. umap/static/umap/css/form.css +19 -12
  57. umap/static/umap/css/icon.css +6 -0
  58. umap/static/umap/css/importers.css +4 -0
  59. umap/static/umap/css/panel.css +2 -0
  60. umap/static/umap/img/16-white.svg +5 -1
  61. umap/static/umap/img/16.svg +1 -1
  62. umap/static/umap/img/24-white.svg +3 -2
  63. umap/static/umap/img/24.svg +3 -4
  64. umap/static/umap/img/importers/opendata.svg +1 -0
  65. umap/static/umap/img/source/16-white.svg +8 -4
  66. umap/static/umap/img/source/16.svg +1 -1
  67. umap/static/umap/img/source/24-white.svg +5 -4
  68. umap/static/umap/img/source/24.svg +5 -6
  69. umap/static/umap/js/components/modal.js +27 -0
  70. umap/static/umap/js/modules/caption.js +4 -4
  71. umap/static/umap/js/modules/data/features.js +40 -4
  72. umap/static/umap/js/modules/data/layer.js +208 -138
  73. umap/static/umap/js/modules/form/builder.js +6 -14
  74. umap/static/umap/js/modules/form/fields.js +2 -2
  75. umap/static/umap/js/modules/help.js +15 -3
  76. umap/static/umap/js/modules/importer.js +7 -4
  77. umap/static/umap/js/modules/importers/opendata.js +142 -0
  78. umap/static/umap/js/modules/permissions.js +3 -3
  79. umap/static/umap/js/modules/rendering/controls.js +34 -2
  80. umap/static/umap/js/modules/rendering/icon.js +2 -2
  81. umap/static/umap/js/modules/rendering/layers/base.js +1 -1
  82. umap/static/umap/js/modules/rendering/layers/classified.js +55 -49
  83. umap/static/umap/js/modules/rendering/layers/cluster.js +16 -9
  84. umap/static/umap/js/modules/rendering/layers/heat.js +13 -11
  85. umap/static/umap/js/modules/rendering/map.js +5 -0
  86. umap/static/umap/js/modules/rendering/ui.js +23 -0
  87. umap/static/umap/js/modules/rules.js +24 -23
  88. umap/static/umap/js/modules/schema.js +60 -4
  89. umap/static/umap/js/modules/sync/updaters.js +7 -3
  90. umap/static/umap/js/modules/tableeditor.js +7 -30
  91. umap/static/umap/js/modules/templates.js +122 -0
  92. umap/static/umap/js/modules/ui/bar.js +13 -3
  93. umap/static/umap/js/modules/ui/panel.js +1 -1
  94. umap/static/umap/js/modules/umap.js +28 -13
  95. umap/static/umap/js/umap.controls.js +11 -4
  96. umap/static/umap/locale/br.js +51 -18
  97. umap/static/umap/locale/br.json +51 -18
  98. umap/static/umap/locale/da.js +343 -310
  99. umap/static/umap/locale/da.json +343 -310
  100. umap/static/umap/locale/de.js +40 -7
  101. umap/static/umap/locale/de.json +40 -7
  102. umap/static/umap/locale/el.js +31 -4
  103. umap/static/umap/locale/el.json +31 -4
  104. umap/static/umap/locale/en.js +34 -1
  105. umap/static/umap/locale/en.json +34 -1
  106. umap/static/umap/locale/es.js +37 -4
  107. umap/static/umap/locale/es.json +37 -4
  108. umap/static/umap/locale/fr.js +34 -1
  109. umap/static/umap/locale/fr.json +34 -1
  110. umap/static/umap/locale/hu.js +44 -17
  111. umap/static/umap/locale/hu.json +44 -17
  112. umap/static/umap/locale/it.js +74 -41
  113. umap/static/umap/locale/it.json +74 -41
  114. umap/static/umap/locale/nl.js +42 -9
  115. umap/static/umap/locale/nl.json +42 -9
  116. umap/static/umap/map.css +3 -23
  117. umap/static/umap/vendors/textpath/leaflet.textpath.js +184 -0
  118. umap/storage/fs.py +19 -9
  119. umap/templates/umap/dashboard_menu.html +5 -0
  120. umap/templates/umap/design_system.html +9 -0
  121. umap/templates/umap/js.html +3 -0
  122. umap/templates/umap/map_init.html +3 -1
  123. umap/templates/umap/map_list.html +2 -2
  124. umap/templates/umap/map_table.html +18 -18
  125. umap/templates/umap/user_dashboard.html +9 -58
  126. umap/templates/umap/user_map_table.html +36 -0
  127. umap/templates/umap/user_templates.html +19 -0
  128. umap/templatetags/umap_tags.py +5 -0
  129. umap/tests/integration/test_basics.py +9 -1
  130. umap/tests/integration/test_conditional_rules.py +57 -0
  131. umap/tests/integration/test_datalayer.py +16 -9
  132. umap/tests/integration/test_edit_marker.py +11 -0
  133. umap/tests/integration/test_tableeditor.py +42 -7
  134. umap/tests/integration/test_templates.py +44 -0
  135. umap/tests/test_dashboard.py +19 -0
  136. umap/tests/test_purge_old_versions.py +91 -0
  137. umap/tests/test_switch_user.py +31 -0
  138. umap/tests/test_views.py +67 -0
  139. umap/urls.py +7 -1
  140. umap/views.py +65 -19
  141. {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/METADATA +15 -15
  142. {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/RECORD +145 -132
  143. {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/WHEEL +0 -0
  144. {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/entry_points.txt +0 -0
  145. {umap_project-3.0.5.dist-info → umap_project-3.1.0.dist-info}/licenses/LICENSE +0 -0
umap/static/umap/map.css CHANGED
@@ -151,29 +151,6 @@ html[dir="rtl"] .leaflet-tooltip-pane > * {
151
151
  min-height: 23px;
152
152
  height: 23px;
153
153
  }
154
- .edit-enable [type="button"]:before {
155
- content: ' ';
156
- width: 24px;
157
- height: 24px;
158
- display: inline-block;
159
- vertical-align: -20%;
160
- background-image: url('./img/16-white.svg');
161
- background-position: -48px -48px;
162
- }
163
- .edit-enable [type="button"] {
164
- width: initial;
165
- padding: 0 20px;
166
- background-color: #353c3e;
167
- color: #fff;
168
- background-image: none;
169
- border-radius: 20px;
170
- height: var(--control-size);
171
- line-height: var(--control-size);
172
- display: block;
173
- }
174
- .edit-enable [type="button"]:hover {
175
- background-color: #4d5759;
176
- }
177
154
  .umap-permanent-credits-container {
178
155
  max-width: 20rem;
179
156
  margin-inline-start: 5px!important;
@@ -468,6 +445,9 @@ ul.photon-autocomplete {
468
445
  width: 100%;
469
446
  margin-bottom: var(--text-margin);
470
447
  }
448
+ .umap-help-links li {
449
+ margin-bottom: var(--text-margin);
450
+ }
471
451
  .umap-help {
472
452
  font-style: italic;
473
453
  }
@@ -0,0 +1,184 @@
1
+ /*
2
+ * Leaflet.TextPath - Shows text along a polyline
3
+ * Inspired by Tom Mac Wright article :
4
+ * https://web.archive.org/web/20130312131812/http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/
5
+ */
6
+
7
+ (function () {
8
+
9
+ var __onAdd = L.Polyline.prototype.onAdd,
10
+ __onRemove = L.Polyline.prototype.onRemove,
11
+ __updatePath = L.Polyline.prototype._updatePath,
12
+ __bringToFront = L.Polyline.prototype.bringToFront;
13
+
14
+
15
+ var PolylineTextPath = {
16
+
17
+ onAdd: function (map) {
18
+ __onAdd.call(this, map);
19
+ this._textRedraw();
20
+ },
21
+
22
+ onRemove: function (map) {
23
+ map = map || this._map;
24
+ if (map && this._textNode && this._renderer._container)
25
+ this._renderer._container.removeChild(this._textNode);
26
+ __onRemove.call(this, map);
27
+ },
28
+
29
+ bringToFront: function () {
30
+ __bringToFront.call(this);
31
+ this._textRedraw();
32
+ },
33
+
34
+ _updatePath: function () {
35
+ __updatePath.call(this);
36
+ this._textRedraw();
37
+ },
38
+
39
+ _textRedraw: function () {
40
+ var text = this._text,
41
+ options = this._textOptions;
42
+ if (text) {
43
+ this.setText(null).setText(text, options);
44
+ }
45
+ },
46
+
47
+ setText: function (text, options) {
48
+ this._text = text;
49
+ this._textOptions = options;
50
+
51
+ /* If not in SVG mode or Polyline not added to map yet return */
52
+ /* setText will be called by onAdd, using value stored in this._text */
53
+ if (!L.Browser.svg || typeof this._map === 'undefined') {
54
+ return this;
55
+ }
56
+
57
+ var defaults = {
58
+ repeat: false,
59
+ fillColor: 'black',
60
+ attributes: {},
61
+ below: false,
62
+ };
63
+ options = L.Util.extend(defaults, options);
64
+
65
+ /* If empty text, hide */
66
+ if (!text) {
67
+ if (this._textNode && this._textNode.parentNode) {
68
+ this._renderer._container.removeChild(this._textNode);
69
+
70
+ /* delete the node, so it will not be removed a 2nd time if the layer is later removed from the map */
71
+ delete this._textNode;
72
+ }
73
+ return this;
74
+ }
75
+
76
+ text = text.replace(/ /g, '\u00A0'); // Non breakable spaces
77
+ var id = 'pathdef-' + L.Util.stamp(this);
78
+ var svg = this._renderer._container;
79
+ this._path.setAttribute('id', id);
80
+
81
+ if (options.repeat) {
82
+ /* Compute single pattern length */
83
+ var pattern = L.SVG.create('text');
84
+ for (var attr in options.attributes)
85
+ pattern.setAttribute(attr, options.attributes[attr]);
86
+ pattern.appendChild(document.createTextNode(text));
87
+ svg.appendChild(pattern);
88
+ var alength = pattern.getComputedTextLength();
89
+ svg.removeChild(pattern);
90
+
91
+ /* Create string as long as path */
92
+ text = new Array(Math.ceil(isNaN(this._path.getTotalLength() / alength) ? 0 : this._path.getTotalLength() / alength)).join(text);
93
+ }
94
+
95
+ /* Put it along the path using textPath */
96
+ var textNode = L.SVG.create('text'),
97
+ textPath = L.SVG.create('textPath');
98
+
99
+ var dy = options.offset || this._path.getAttribute('stroke-width');
100
+
101
+ textPath.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", '#'+id);
102
+ textNode.setAttribute('dy', dy);
103
+ for (var attr in options.attributes)
104
+ textNode.setAttribute(attr, options.attributes[attr]);
105
+ textPath.appendChild(document.createTextNode(text));
106
+ textNode.appendChild(textPath);
107
+ this._textNode = textNode;
108
+
109
+ if (options.below) {
110
+ svg.insertBefore(textNode, svg.firstChild);
111
+ }
112
+ else {
113
+ svg.appendChild(textNode);
114
+ }
115
+
116
+ /* Center text according to the path's bounding box */
117
+ if (options.position === 'center' || options.center) {
118
+ var textLength = textNode.getComputedTextLength();
119
+ var pathLength = this._path.getTotalLength();
120
+ /* Set the position for the left side of the textNode */
121
+ textNode.setAttribute('dx', ((pathLength / 2) - (textLength / 2)));
122
+ }
123
+ else if (options.position === 'end') {
124
+ var textLength = textNode.getComputedTextLength();
125
+ var pathLength = this._path.getTotalLength();
126
+ // We add pixels to try to counter-balance the space around the character.
127
+ const dx = pathLength - textLength + 6
128
+ textNode.setAttribute('dx', dx);
129
+ } else {
130
+ // "start", which is the default.
131
+ }
132
+
133
+ /* Change label rotation (if required) */
134
+ if (options.orientation) {
135
+ var rotateAngle = 0;
136
+ switch (options.orientation) {
137
+ case 'flip':
138
+ rotateAngle = 180;
139
+ break;
140
+ case 'perpendicular':
141
+ rotateAngle = 90;
142
+ break;
143
+ default:
144
+ rotateAngle = options.orientation;
145
+ }
146
+
147
+ var rotatecenterX = (textNode.getBBox().x + textNode.getBBox().width / 2);
148
+ var rotatecenterY = (textNode.getBBox().y + textNode.getBBox().height / 2);
149
+ textNode.setAttribute('transform','rotate(' + rotateAngle + ' ' + rotatecenterX + ' ' + rotatecenterY + ')');
150
+ }
151
+
152
+ /* Initialize mouse events for the additional nodes */
153
+ if (this.options.interactive) {
154
+ if (L.Browser.svg || !L.Browser.vml) {
155
+ textPath.setAttribute('class', 'leaflet-interactive');
156
+ }
157
+
158
+ var events = ['click', 'dblclick', 'mousedown', 'mouseover',
159
+ 'mouseout', 'mousemove', 'contextmenu'];
160
+ for (var i = 0; i < events.length; i++) {
161
+ L.DomEvent.on(textNode, events[i], this.fire, this);
162
+ }
163
+ }
164
+
165
+ return this;
166
+ }
167
+ };
168
+
169
+ L.Polyline.include(PolylineTextPath);
170
+
171
+ L.LayerGroup.include({
172
+ setText: function(text, options) {
173
+ for (var layer in this._layers) {
174
+ if (typeof this._layers[layer].setText === 'function') {
175
+ this._layers[layer].setText(text, options);
176
+ }
177
+ }
178
+ return this;
179
+ }
180
+ });
181
+
182
+
183
+
184
+ })();
umap/storage/fs.py CHANGED
@@ -16,9 +16,15 @@ class FSDataStorage(FileSystemStorage):
16
16
  name = "%s_%s.geojson" % (instance.pk, int(time.time() * 1000))
17
17
  return root / name
18
18
 
19
- def list_versions(self, instance):
19
+ def _get_names(self, instance):
20
20
  root = self._base_path(instance)
21
- names = self.listdir(root)[1]
21
+ try:
22
+ return self.listdir(root)[1]
23
+ except FileNotFoundError:
24
+ return []
25
+
26
+ def list_versions(self, instance):
27
+ names = self._get_names(instance)
22
28
  names = [name for name in names if self._is_valid_version(name, instance)]
23
29
  versions = [self._version_metadata(name, instance) for name in names]
24
30
  versions.sort(reverse=True, key=operator.itemgetter("at"))
@@ -38,12 +44,12 @@ class FSDataStorage(FileSystemStorage):
38
44
  return fullpath
39
45
 
40
46
  def onDatalayerSave(self, instance):
41
- self._purge_gzip(instance)
42
- self._purge_old_versions(instance, keep=settings.UMAP_KEEP_VERSIONS)
47
+ self.purge_gzip(instance)
48
+ self.purge_old_versions(instance, keep=settings.UMAP_KEEP_VERSIONS)
43
49
 
44
50
  def onDatalayerDelete(self, instance):
45
- self._purge_gzip(instance)
46
- self._purge_old_versions(instance, keep=None)
51
+ self.purge_gzip(instance)
52
+ self.purge_old_versions(instance, keep=None)
47
53
 
48
54
  def _extract_version_ref(self, path):
49
55
  version = path.split(".")[0]
@@ -73,11 +79,12 @@ class FSDataStorage(FileSystemStorage):
73
79
  "size": self.size(self._base_path(instance) / name),
74
80
  }
75
81
 
76
- def _purge_old_versions(self, instance, keep=None):
82
+ def purge_old_versions(self, instance, keep=None):
77
83
  root = self._base_path(instance)
78
84
  versions = self.list_versions(instance)
79
85
  if keep is not None:
80
86
  versions = versions[keep:]
87
+ deleted = 0
81
88
  for version in versions:
82
89
  name = version["name"]
83
90
  # Should not be in the list, but ensure to not delete the file
@@ -88,10 +95,13 @@ class FSDataStorage(FileSystemStorage):
88
95
  self.delete(root / name)
89
96
  except FileNotFoundError:
90
97
  pass
98
+ else:
99
+ deleted += 1
100
+ return deleted
91
101
 
92
- def _purge_gzip(self, instance):
102
+ def purge_gzip(self, instance):
93
103
  root = self._base_path(instance)
94
- names = self.listdir(root)[1]
104
+ names = self._get_names(instance)
95
105
  prefixes = [f"{instance.pk}_"]
96
106
  if instance.old_id:
97
107
  prefixes.append(f"{instance.old_id}_")
@@ -7,6 +7,11 @@
7
7
  {% else %}
8
8
  <a href="{% url 'user_dashboard' %}">{% trans "My Maps" %}</a>
9
9
  {% endif %}
10
+ {% if selected == "templates" %}
11
+ <a class="selected" href="{% url 'user_templates' %}">{% blocktranslate with count=maps.paginator.count %}My Templates ({{ count }}){% endblocktranslate %}</a>
12
+ {% else %}
13
+ <a href="{% url 'user_templates' %}">{% trans "My Templates" %}</a>
14
+ {% endif %}
10
15
  {% if UMAP_ALLOW_EDIT_PROFILE %}
11
16
  <a {% if selected == "profile" %}class="selected"{% endif %}
12
17
  href="{% url 'user_profile' %}">{% trans "My profile" %}</a>
@@ -301,6 +301,15 @@
301
301
  </form>
302
302
  </fieldset>
303
303
  </details>
304
+ <details open>
305
+ <summary>With tabs</summary>
306
+ <div class="flat-tabs" data-ref="tabs">
307
+ <button class="flat on" data-ref="recent">Récents</button>
308
+ <button class="flat" data-ref="symbols">Symbole</button>
309
+ <button class="flat" data-ref="chars">Emoji &amp; texte</button>
310
+ <button class="flat" data-ref="url">URL</button>
311
+ </div>
312
+ </details>
304
313
  </div>
305
314
  <h4>Importers</h4>
306
315
  <div class="umap-dialog window importers dark">
@@ -34,9 +34,12 @@
34
34
  <script src="{% static 'umap/vendors/iconlayers/iconLayers.js' %}" defer></script>
35
35
  <script src="{% static 'umap/vendors/locatecontrol/L.Control.Locate.min.js' %}"
36
36
  defer></script>
37
+ <script src="{% static 'umap/vendors/textpath/leaflet.textpath.js' %}"
38
+ defer></script>
37
39
  <script src="{% static 'umap/vendors/simple-statistics/simple-statistics.min.js' %}"
38
40
  defer></script>
39
41
  <script src="{% static 'umap/js/umap.core.js' %}" defer></script>
40
42
  <script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
41
43
  <script type="module" src="{% static 'umap/js/components/fragment.js' %}" defer></script>
44
+ <script type="module" src="{% static 'umap/js/components/modal.js' %}" defer></script>
42
45
  {% endautoescape %}
@@ -4,10 +4,12 @@
4
4
  <div id="map">
5
5
  </div>
6
6
  <!-- djlint:off -->
7
+ <script id="map-settings" data-settings="{{ map_settings|escape }}"></script>
7
8
  <script defer type="module">
8
9
  {% autoescape off %}
9
10
  import Umap from '{% static "umap/js/modules/umap.js" %}'
10
11
  {% endautoescape %}
11
- U.MAP = new Umap("map", {{ map_settings|notag|safe }})
12
+ U.SETTINGS = JSON.parse(document.getElementById('map-settings').dataset.settings)
13
+ U.MAP = new Umap("map", U.SETTINGS)
12
14
  </script>
13
15
  <!-- djlint:on -->
@@ -12,14 +12,14 @@
12
12
  {% endfor %}
13
13
  </ul>
14
14
  {% endif %}
15
- <h3>{{ map_inst.name }}</h3>
15
+ <h3>{% if map_inst.is_template %}<mark class="template-map">[{% trans "template" %}]</mark>{% endif %} {{ map_inst.name }}</h3>
16
16
  {% with author=map_inst.get_author %}
17
17
  {% if author %}
18
18
  <p>{% trans "by" %} <a href="{{ author.get_url }}">{{ author }}</a></p>
19
19
  {% endif %}
20
20
  {% endwith %}
21
21
  </div>
22
- <a class="main" href="{{ map_inst.get_absolute_url }}">{% translate "See the map" %}</a>
22
+ <a class="main" href="{{ map_inst.get_absolute_url }}">{% if map_inst.is_template %}{% translate "See the template" %}{% else %}{% translate "See the map" %}{% endif %}</a>
23
23
  </hgroup>
24
24
  </div>
25
25
  {% endfor %}
@@ -35,24 +35,24 @@
35
35
  <a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>
36
36
  </th>
37
37
  <td>
38
- {{ map_inst.preview_settings|json_script:unique_id }}
39
- <button class="map-icon map-opener"
40
- data-map-id="{{ unique_id }}"
41
- title="{% translate "Open preview" %}">
42
- <span class="icon-dashboard icon-view"></span>
43
- <span class="sr-only">{% translate "Open preview" %}</span>
44
- </button>
45
- <dialog>
46
- <form method="dialog">
47
- <div id="{{ unique_id }}_target" class="map_fragment">
48
- </div>
49
- <p class="close-dialog">
50
- <button class="button" type="submit">
51
- Close
52
- </button>
53
- </p>
54
- </form>
55
- </dialog>
38
+ <umap-modal data-settings='{{ map_inst.preview_settings|dumps|escape }}' data-map-id="{{ unique_id }}">
39
+ <button class="map-icon map-opener"
40
+ title="{% translate "Open preview" %}">
41
+ <span class="icon-dashboard icon-view"></span>
42
+ <span class="sr-only">{% translate "Open preview" %}</span>
43
+ </button>
44
+ <dialog>
45
+ <form method="dialog">
46
+ <div id="{{ unique_id }}_target" class="map_fragment">
47
+ </div>
48
+ <p class="close-dialog">
49
+ <button class="button" type="submit">
50
+ Close
51
+ </button>
52
+ </p>
53
+ </form>
54
+ </dialog>
55
+ </umap-modal>
56
56
  </td>
57
57
  <td>
58
58
  {{ map_inst.get_share_status_display }}
@@ -6,63 +6,14 @@
6
6
  {% translate "My Dashboard" %} - {{ SITE_DESCRIPTION }}
7
7
  {% endblock head_title %}
8
8
  {% block maincontent %}
9
- {% trans "Search my maps" as placeholder %}
10
9
  {% include "umap/dashboard_menu.html" with selected="maps" %}
11
- <div class="wrapper">
12
- <div class="row">
13
- <div class="table-header">
14
- <form action="{{ request.get_full_path }}" method="get">
15
- <span>
16
- <label class="sr-only" for="q">
17
- {% translate "Map’s title" %}
18
- </label>
19
- <input id="q"
20
- name="q"
21
- type="search"
22
- placeholder="{% translate "Map’s title" %}"
23
- value="{{ request.GET.q|default:"" }}" />
24
- </span>
25
- <input type="submit" value="{% trans "Search my maps" %}" />
26
- </form>
27
- {% if maps.object_list|length > 1 %}
28
- <a href="{% url 'user_download' %}?{% spaceless %} {% for map_inst in maps %}map_id={{ map_inst.pk }}{% if not forloop.last %}&{% endif %}{% endfor %} {% endspaceless %}"
29
- class="button button-download">
30
- {% blocktranslate with count=maps.object_list|length trimmed %}
31
- Download {{ count }} maps
32
- {% endblocktranslate %}
33
- </a>
34
- {% endif %}
35
- </div>
36
- {% if maps or request.GET.q %}
37
- {% include "umap/map_table.html" %}
38
- {% else %}
39
- <div>
40
- {% blocktrans %}You have no map yet.{% endblocktrans %} <a href="{% url 'map_new' %}">{% translate "Create a map" %}</a>
41
- </div>
42
- {% endif %}
43
- </div>
44
- </div>
10
+ {% trans "Search my maps" as submit_label %}
11
+ {% trans "Map’s name" as label_title %}
12
+ {% blocktranslate asvar download_label with count=maps.object_list|length trimmed %}
13
+ Download {{ count }} maps
14
+ {% endblocktranslate %}
15
+ {% blocktranslate asvar empty_label %}You have no map yet.{% endblocktranslate %}
16
+ {% translate "Create a map" as create_label %}
17
+ {% translate "No map found." as empty_search_label %}
18
+ {% include "umap/user_map_table.html" with submit_label=submit_label label_title=label_title download_label=download_label empty_label=empty_label create_label=create_label empty_search_label=empty_search_label %}
45
19
  {% endblock maincontent %}
46
- {% block bottom_js %}
47
- {{ block.super }}
48
- <script type="module">
49
- {% autoescape off %}
50
- import Umap from '{% static "umap/js/modules/umap.js" %}'
51
- {% endautoescape %}
52
- const CACHE = {}
53
- for (const mapOpener of document.querySelectorAll("button.map-opener")) {
54
- mapOpener.addEventListener('click', (event) => {
55
- const button = event.target.closest('button')
56
- button.nextElementSibling.showModal()
57
- const mapId = button.dataset.mapId
58
- if (!document.querySelector(`#${mapId}_target`).children.length) {
59
- const previewSettings = JSON.parse(document.getElementById(mapId).textContent)
60
- const map = new Umap(`${mapId}_target`, previewSettings)
61
- CACHE[mapId] = map
62
- } else {
63
- CACHE[mapId].invalidateSize()
64
- }
65
- })
66
- }
67
- </script>
68
- {% endblock bottom_js %}
@@ -0,0 +1,36 @@
1
+ <div class="wrapper">
2
+ <div class="row">
3
+ <div class="table-header">
4
+ <form action="{{ request.get_full_path }}" method="get">
5
+ <span>
6
+ <label class="sr-only" for="q">
7
+ {{ label_title }}
8
+ </label>
9
+ <input id="q"
10
+ name="q"
11
+ type="search"
12
+ placeholder="{{ label_title }}"
13
+ value="{{ request.GET.q|default:"" }}" />
14
+ </span>
15
+ <input type="submit" value="{{ submit_label }}" />
16
+ </form>
17
+ {% if maps.object_list|length > 1 %}
18
+ <a href="{% url 'user_download' %}?{% spaceless %} {% for map_inst in maps %}map_id={{ map_inst.pk }}{% if not forloop.last %}&{% endif %}{% endfor %} {% endspaceless %}"
19
+ class="button button-download">
20
+ {{ download_label }}
21
+ </a>
22
+ {% endif %}
23
+ </div>
24
+ {% if maps %}
25
+ {% include "umap/map_table.html" %}
26
+ {% else %}
27
+ <div>
28
+ {% if request.GET.q or request.GET.tags %}
29
+ {{ empty_search_label }}
30
+ {% else %}
31
+ {{ empty_label }} <a href="{% url 'map_new' %}">{{ create_label }}</a>
32
+ {% endif %}
33
+ </div>
34
+ {% endif %}
35
+ </div>
36
+ </div>
@@ -0,0 +1,19 @@
1
+ {% extends "umap/content.html" %}
2
+
3
+ {% load i18n static %}
4
+
5
+ {% block head_title %}
6
+ {% translate "My Templates" %} - {{ SITE_DESCRIPTION }}
7
+ {% endblock head_title %}
8
+ {% block maincontent %}
9
+ {% include "umap/dashboard_menu.html" with selected="templates" %}
10
+ {% trans "Search my templates" as submit_label %}
11
+ {% trans "Template’s name" as label_title %}
12
+ {% blocktranslate asvar download_label with count=maps.object_list|length trimmed %}
13
+ Download {{ count }} templates
14
+ {% endblocktranslate %}
15
+ {% blocktranslate asvar empty_label %}You have no template yet.{% endblocktranslate %}
16
+ {% translate "Create a template" as create_label %}
17
+ {% translate "No template found." as empty_search_label %}
18
+ {% include "umap/user_map_table.html" with submit_label=submit_label label_title=label_title download_label=download_label empty_label=empty_label create_label=create_label empty_search_label=empty_search_label %}
19
+ {% endblock maincontent %}
@@ -43,6 +43,11 @@ def tilelayer_preview(tilelayer):
43
43
  return output
44
44
 
45
45
 
46
+ @register.filter
47
+ def dumps(dict_):
48
+ return json_dumps(dict_)
49
+
50
+
46
51
  @register.filter
47
52
  def can_delete_map(map, request):
48
53
  return map.can_delete(request)
@@ -61,13 +61,21 @@ def test_cannot_put_script_tag_in_datalayer_name_or_description(
61
61
  page.locator(".umap-field-description textarea").fill(
62
62
  '<p>before <script>alert("attack")</script> after</p>'
63
63
  )
64
- page.get_by_role("button", name="Save").click()
64
+ page.wait_for_timeout(300) # Wait for debounce
65
+ with page.expect_response(re.compile(".*/datalayer/create/.*")):
66
+ page.get_by_role("button", name="Save").click()
65
67
  page.get_by_role("button", name="About").click()
66
68
  # Title should contain raw HTML (we are using textContent)
67
69
  expect(page.get_by_text('<script>alert("attack")</script>')).to_be_visible()
68
70
  # Description should contain escaped HTML
69
71
  expect(page.get_by_text("before after")).to_be_visible()
70
72
 
73
+ # Reload the map
74
+ page.goto(f"{live_server.url}{openmap.get_absolute_url()}")
75
+ page.get_by_role("button", name="About").click()
76
+ expect(page.get_by_text('<script>alert("attack")</script>')).to_be_visible()
77
+ expect(page.get_by_text("before after")).to_be_visible()
78
+
71
79
 
72
80
  def test_login_from_map_page(live_server, page, tilelayer, settings, user, context):
73
81
  settings.ENABLE_ACCOUNT_LOGIN = True
@@ -1,3 +1,5 @@
1
+ from copy import deepcopy
2
+
1
3
  import pytest
2
4
  from playwright.sync_api import expect
3
5
 
@@ -308,3 +310,58 @@ def test_autocomplete_datalist(live_server, page, openmap):
308
310
  expect(datalist).to_have_count(2)
309
311
  values = {option.inner_text() for option in datalist.all()}
310
312
  assert values == {"mytype=even", "mytype=odd"}
313
+
314
+
315
+ def test_can_combine_rules(live_server, page, map):
316
+ map.settings["properties"]["rules"] = [
317
+ {"condition": "mytype=odd", "options": {"color": "aliceblue"}},
318
+ {"condition": "mynumber>10", "options": {"iconClass": "Drop"}},
319
+ ]
320
+ map.save()
321
+ DataLayerFactory(map=map, data=DATALAYER_DATA1)
322
+ DataLayerFactory(map=map, data=DATALAYER_DATA2)
323
+ page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
324
+ markers = page.locator(".leaflet-marker-icon .icon_container")
325
+ drops = page.locator(".umap-drop-icon .icon_container")
326
+ expect(markers).to_have_count(5)
327
+ expect(drops).to_have_count(2)
328
+ colors = getColors(markers)
329
+ assert colors.count("rgb(240, 248, 255)") == 3
330
+ colors = getColors(drops)
331
+ assert colors.count("rgb(240, 248, 255)") == 2
332
+
333
+
334
+ def test_first_matching_rule_wins_on_given_property(live_server, page, map):
335
+ map.settings["properties"]["rules"] = [
336
+ {"condition": "mytype=odd", "options": {"color": "aliceblue"}},
337
+ {"condition": "mytype!=even", "options": {"color": "darkred"}},
338
+ ]
339
+ map.save()
340
+ DataLayerFactory(map=map, data=DATALAYER_DATA1)
341
+ DataLayerFactory(map=map, data=DATALAYER_DATA2)
342
+ page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
343
+ markers = page.locator(".leaflet-marker-icon .icon_container")
344
+ expect(markers).to_have_count(5)
345
+ colors = getColors(markers)
346
+ assert colors.count("rgb(240, 248, 255)") == 3
347
+
348
+
349
+ def test_rules_from_datalayer(live_server, page, map):
350
+ map.settings["properties"]["rules"] = [
351
+ {"condition": "mytype=odd", "options": {"color": "darkred"}}
352
+ ]
353
+ map.save()
354
+ data = deepcopy(DATALAYER_DATA1)
355
+ data["_umap_options"]["rules"] = [
356
+ {"condition": "mytype=odd", "options": {"color": "aliceblue"}}
357
+ ]
358
+ DataLayerFactory(map=map, data=data)
359
+ DataLayerFactory(map=map, data=DATALAYER_DATA2)
360
+ page.goto(f"{live_server.url}{map.get_absolute_url()}#6/48.948/1.670")
361
+ markers = page.locator(".leaflet-marker-icon .icon_container")
362
+ expect(markers).to_have_count(5)
363
+ colors = getColors(markers)
364
+ # Alice Blue should only affect layer 1
365
+ assert colors.count("rgb(240, 248, 255)") == 1
366
+ # Dark Red as for map global rules
367
+ assert colors.count("rgb(139, 0, 0)") == 2