Projet

Général

Profil

0003-fields-use-new-map-marker-selection-widget-in-front-.patch

Frédéric Péters, 24 novembre 2020 16:19

Télécharger (16,6 ko)

Voir les différences:

Subject: [PATCH 3/3] fields: use new map marker selection widget in front
 (#47066)

 tests/form_pages/test_all.py                  |  55 +++++++++++++++++-
 tests/utilities.py                            |  12 ++++
 wcs/fields.py                                 |  27 ++++++---
 wcs/qommon/form.py                            |  27 +++++++++
 .../static/images/blank-marker-icon.png       | Bin 0 -> 1783 bytes
 wcs/qommon/static/js/qommon.map.js            |  55 +++++++++++++++++-
 .../forms/widgets/map-marker-selection.html   |  31 ++++++++++
 .../templates/qommon/forms/widgets/map.html   |   1 +
 8 files changed, 195 insertions(+), 13 deletions(-)
 create mode 100644 wcs/qommon/static/images/blank-marker-icon.png
 create mode 100644 wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html
tests/form_pages/test_all.py
9 9
import time
10 10
import zipfile
11 11
import base64
12
from webtest import Upload, Hidden
12
from webtest import Upload, Hidden, Radio
13 13
import mock
14 14
import xml.etree.ElementTree as ET
15 15

  
......
8957 8957
        '1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
8958 8958
    }
8959 8959
    assert '2020-04-18' in formdata.evolution[0].parts[0].content
8960

  
8961

  
8962
def test_form_item_map_data_source(pub, http_requests):
8963
    NamedDataSource.wipe()
8964
    data_source = NamedDataSource(name='foobar')
8965
    data_source.data_source = {
8966
        'type': 'geojson',
8967
        'value': 'http://remote.example.net/geojson',
8968
    }
8969
    data_source.id_property = 'id'
8970
    data_source.label_template_property = '{{ text }}'
8971
    data_source.cache_duration = '5'
8972
    data_source.store()
8973

  
8974
    formdef = create_formdef()
8975
    formdef.fields = [
8976
        fields.ItemField(id='1', label='map', display_mode='map', data_source={'type': 'foobar'}),
8977
    ]
8978
    formdef.store()
8979
    formdef.data_class().wipe()
8980
    app = get_app(pub)
8981
    resp = app.get('/test/')
8982
    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-url'] == '/api/geojson/foobar'
8983
    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-radio-name'] == 'f1$marker_id'
8984
    app.get('/api/geojson/wrong-foobar', status=404)
8985
    resp_geojson = app.get('/api/geojson/foobar')
8986
    assert len(resp_geojson.json['features']) == 2
8987
    assert http_requests.count() == 1
8988
    assert http_requests.get_last('url') == 'http://remote.example.net/geojson'
8989
    resp_geojson = app.get('/api/geojson/foobar')
8990
    assert http_requests.count() == 1  # cache was used
8991
    assert len(resp_geojson.json['features']) == 2
8992
    # simulate qommon.map.js that will create radio inputs
8993
    resp.form.fields['f1$marker_id'] = [Radio(form=resp.form, tag='input', name='f1$marker_id', pos=5)]
8994
    resp.form.fields['f1$marker_id'][0].options.append(('1', False, None))
8995
    resp.form.fields['f1$marker_id'][0].options.append(('2', False, None))
8996
    resp.form.fields['f1$marker_id'][0].optionPositions.append(5)
8997
    resp.form.fields['f1$marker_id'][0].optionPositions.append(6)
8998
    resp.form.field_order.append(('f1$marker_id', resp.form.fields['f1$marker_id'][0]))
8999
    resp.form['f1$marker_id'].value = '1'  # click on marker
9000
    resp.form['f1$latlng'] = '1;2'  # set via js
9001
    resp = resp.form.submit('submit')
9002
    assert 'Check values then click submit.' in resp
9003
    # selected option is displayed as readonly:
9004
    assert resp.pyquery('input[type=text][value=foo][readonly]')
9005
    resp = resp.form.submit('submit')
9006
    resp = resp.follow()
9007
    assert 'The form has been recorded' in resp
9008
    assert '<div class="value">foo</div>' in resp
9009
    assert formdef.data_class().count() == 1
9010
    data_id = formdef.data_class().select()[0].id
9011
    formdata = formdef.data_class().get(data_id)
9012
    assert formdata.data['1_structured']['geometry']['coordinates'] == [1, 2]
tests/utilities.py
1 1
import email.header
2 2
import email.parser
3
import json
3 4
import os
4 5
import tempfile
5 6
import random
......
326 327

  
327 328
        with open(os.path.join(os.path.dirname(__file__), 'idp_metadata.xml')) as fd:
328 329
            metadata = fd.read()
330
        geojson = {
331
            'features': [
332
                {'properties': {'id': '1', 'text': 'foo'},
333
                 'geometry': {'type': 'Point', 'coordinates': [1, 2]}
334
                },
335
                {'properties': {'id': '2', 'text': 'bar'},
336
                 'geometry': {'type': 'Point', 'coordinates': [3, 4]}
337
                }
338
            ]
339
        }
329 340
        status, data, headers = {
330 341
            'http://remote.example.net/204': (204, None, None),
331 342
            'http://remote.example.net/400': (400, 'bad request', None),
......
336 347
            'http://remote.example.net/json': (200, '{"foo": "bar"}', None),
337 348
            'http://remote.example.net/json-list': (200, '{"data": [{"id": "a", "text": "b"}]}', None),
338 349
            'http://remote.example.net/json-list-extra': (200, '{"data": [{"id": "a", "text": "b", "foo": "bar"}]}', None),
350
            'http://remote.example.net/geojson': (200, json.dumps(geojson), None),
339 351
            'http://remote.example.net/json-err0': (200, '{"data": "foo", "err": 0}', None),
340 352
            'http://remote.example.net/json-err1': (200, '{"data": "", "err": 1}', None),
341 353
            'http://remote.example.net/json-list-err1': (200, '{"data": [{"id": "a", "text": "b"}], "err": 1}', None),
wcs/fields.py
1498 1498
        if getattr(element.find('show_as_radio'), 'text', None) == 'True':
1499 1499
            self.display_mode = 'radio'
1500 1500

  
1501
    @property
1502
    def extra_attributes(self):
1503
        if self.display_mode == 'map':
1504
            return ['initial_zoom', 'min_zoom', 'max_zoom', 'data_source']
1505
        return []
1506

  
1501 1507
    def get_options(self, mode=None):
1502 1508
        if self.data_source:
1503 1509
            return [x[:3] for x in data_sources.get_items(self.data_source, mode=mode)]
......
1531 1537
            self.widget_class = JsonpSingleSelectWidget
1532 1538
            return
1533 1539

  
1534
        if self.data_source:
1535
            items = data_sources.get_items(self.data_source,
1536
                    include_disabled=self.display_disabled_items)
1537
            kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
1538
            kwargs['options_with_attributes'] = items[:]
1539
        else:
1540
            kwargs['options'] = self.get_options()
1541
        if not kwargs.get('options'):
1542
            kwargs['options'] = [(None, '---')]
1540
        if self.display_mode != 'map':
1541
            if self.data_source:
1542
                items = data_sources.get_items(self.data_source,
1543
                        include_disabled=self.display_disabled_items)
1544
                kwargs['options'] = [x[:3] for x in items if not x[-1].get('disabled')]
1545
                kwargs['options_with_attributes'] = items[:]
1546
            else:
1547
                kwargs['options'] = self.get_options()
1548
            if not kwargs.get('options'):
1549
                kwargs['options'] = [(None, '---')]
1543 1550
        if display_mode == 'radio':
1544 1551
            self.widget_class = RadiobuttonsWidget
1545 1552
            if type(kwargs['options'][0]) is str:
......
1552 1559
                kwargs['delim'] = htmltext('<br />')
1553 1560
        elif display_mode == 'autocomplete':
1554 1561
            kwargs['select2'] = True
1562
        elif display_mode == 'map':
1563
            self.widget_class = MapMarkerSelectionWidget
1555 1564

  
1556 1565
    def get_display_value(self, value):
1557 1566
        data_source = data_sources.get_object(self.data_source)
wcs/qommon/form.py
2470 2470
            self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon'])
2471 2471

  
2472 2472

  
2473
class MapMarkerSelectionWidget(MapWidget):
2474
    template_name = 'qommon/forms/widgets/map-marker-selection.html'
2475

  
2476
    def __init__(self, name, value=None, **kwargs):
2477
        CompositeWidget.__init__(self, name, value, **kwargs)
2478
        self.add(HiddenWidget, 'marker_id', value=value)
2479
        self.readonly = kwargs.pop('readonly', False)
2480

  
2481
        self.map_attributes = {}
2482
        self.map_attributes.update(get_publisher().get_map_attributes())
2483
        self.sync_map_and_address_fields = get_publisher().has_site_option('sync-map-and-address-fields')
2484
        for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
2485
            if attribute in kwargs:
2486
                self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
2487

  
2488
        from wcs import data_sources
2489
        data_source = data_sources.get_object(kwargs['data_source'])
2490
        self.geojson_markers_url = data_source.get_geojson_url()
2491

  
2492
    def initial_position(self):
2493
        return None
2494

  
2495
    def _parse(self, request):
2496
        CompositeWidget._parse(self, request)
2497
        self.value = self.get('marker_id')
2498

  
2499

  
2473 2500
class HiddenErrorWidget(HiddenWidget):
2474 2501
    def set_error(self, error):
2475 2502
        Widget.set_error(self, error)
wcs/qommon/static/js/qommon.map.js
46 46
     var hidden = $(this).prev();
47 47
     map.marker = null;
48 48
     var latlng;
49
     var initial_marker_id = $map_widget.data('markers-initial-id');
49 50
     if ($map_widget.data('init-lat')) {
50 51
       latlng = [$map_widget.data('init-lat'), $map_widget.data('init-lng')]
51
       map.marker = L.marker(latlng);
52
       map.marker.addTo(map);
52
       if (typeof initial_marker_id == 'undefined') {
53
         // if markers are used they will appear via their input widget
54
         map.marker = L.marker(latlng);
55
         map.marker.addTo(map);
56
       }
53 57
     } else if ($map_widget.data('def-lat')) {
54 58
       latlng = [$map_widget.data('def-lat'), $map_widget.data('def-lng')]
55 59
     } else {
......
67 71
     } else {
68 72
       map.addControl(gps_control);
69 73
     }
70
     if (! $map_widget.data('readonly')) {
74
     if ($map_widget.data('markers-url')) {
75
       var radio_name = $map_widget.data('markers-radio-name');
76
       $map_widget.on('change', 'input[name="' + radio_name + '"]', function() {
77
         var $radio = $(this);
78
         if ($radio.is(':checked')) {
79
           hidden.val($radio.data('lat') + ';' + $radio.data('lng'));
80
           hidden.trigger('change');
81
         }
82
       });
83
       $.getJSON($map_widget.data('markers-url')).done(
84
         function(data) {
85
           var geo_json = L.geoJson(data, {
86
             pointToLayer: function (feature, latlng) {
87
               var $label = $('<label>', {
88
                 title: feature.properties._text
89
               });
90
               var $radio = $('<input>', {
91
                 value: feature.properties._id,
92
                 name: radio_name,
93
                 type: 'radio',
94
                 'data-lat': latlng.lat,
95
                 'data-lng': latlng.lng
96
               });
97
               if (typeof initial_marker_id !== 'undefined' && feature.properties._id == initial_marker_id) {
98
                 $radio.attr('checked', 'checked');
99
               }
100
               $label.append($radio);
101
               $label.append($('<span></span>'));
102
               var div_marker = L.divIcon({
103
                 className: 'item-marker',
104
                 html: '<div>' + $label.prop('outerHTML') + '</div>'
105
               });
106
               return L.marker(latlng, {icon: div_marker});
107
             }
108
           });
109
           map.fitBounds(geo_json.getBounds());
110
           geo_json.addTo(map);
111
         }
112
       );
113
     } else if ($map_widget.data('readonly') && initial_marker_id) {
114
       // readonly and marker
115
         map.marker = L.marker(latlng);
116
         map.marker.addTo(map);
117
     }
118

  
119
     if (! $map_widget.data('readonly') && ! $map_widget.data('markers-url')) {
71 120
       map.on('click', function(e) {
72 121
         $map_widget.trigger('set-geolocation', e.latlng);
73 122
       });
wcs/qommon/templates/qommon/forms/widgets/map-marker-selection.html
1
{% extends "qommon/forms/widgets/map.html" %}
2

  
3
{% block widget-control-attributes %}
4
  {% if not widget.readonly %}
5
    data-markers-url="{{ widget.geojson_markers_url }}"
6
    data-markers-radio-name="{{widget.name}}$marker_id"
7
  {% endif %}
8
  {% if widget.value %}
9
    data-markers-initial-id="{{ widget.value }}"
10
  {% endif %}
11
{% endblock %}
12

  
13
{% block widget-control %}
14
{{ block.super }}
15

  
16
<style>
17
.item-marker input + span {
18
  position: relative;
19
  z-index: 1000;
20
  margin-top: -55px;
21
  margin-left: -5px;
22
  display: block;
23
  width: 25px;
24
  height: 41px;
25
  background: url(/static/images/blank-marker-icon.png);
26
}
27
.item-marker input:checked + span {
28
  background: url(/static/xstatic/images/marker-icon.png);
29
}
30
</style>
31
{% endblock %}
wcs/qommon/templates/qommon/forms/widgets/map.html
10 10
    data-init-lat="{{ widget.initial_position.lat }}"
11 11
    data-init-lng="{{ widget.initial_position.lng }}"
12 12
  {% endif %}
13
  {% block widget-control-attributes %}{% endblock %}
13 14
></div>
14 15
{% endblock %}
15
-