Projet

Général

Profil

0001-fields-add-marker-selection-on-map-field-47066.patch

Frédéric Péters, 29 septembre 2020 19:26

Télécharger (37,6 ko)

Voir les différences:

Subject: [PATCH] fields: add marker selection on map field (#47066)

 tests/test_form_pages.py                      |  54 ++++++++++-
 tests/test_formdata.py                        |  41 ++++++++-
 tests/test_workflows.py                       |   4 +
 tests/utilities.py                            |  13 +++
 wcs/api.py                                    |  14 ++-
 wcs/data_sources.py                           |  84 +++++++++++++++---
 wcs/fields.py                                 |  78 +++++++++++++---
 wcs/formdata.py                               |   6 +-
 wcs/formdef.py                                |   5 +-
 wcs/forms/common.py                           |   2 +-
 wcs/qommon/form.py                            |   8 ++
 .../static/images/blank-marker-icon.png       | Bin 0 -> 1783 bytes
 wcs/qommon/static/js/qommon.map.js            |  55 +++++++++++-
 .../templates/qommon/forms/widgets/map.html   |  37 ++++++++
 wcs/variables.py                              |  14 ++-
 15 files changed, 375 insertions(+), 40 deletions(-)
 create mode 100644 wcs/qommon/static/images/blank-marker-icon.png
tests/test_form_pages.py
10 10
import time
11 11
import zipfile
12 12
import base64
13
from webtest import Upload, Hidden
13
from webtest import Upload, Hidden, Radio
14 14
import mock
15 15
import xml.etree.ElementTree as ET
16 16

  
......
4427 4427
    assert formdef.data_class().count() == 1
4428 4428
    data_id = formdef.data_class().select()[0].id
4429 4429
    data = formdef.data_class().get(data_id)
4430
    assert data.data == {'0': {'lat': 1.234, 'lon': -1.234}, '1': 'bla'}
4430
    assert data.data == {'0': {'lat': 1.234, 'lon': -1.234}, '0_display': '1.234;-1.234', '1': 'bla'}
4431 4431

  
4432 4432

  
4433 4433
def test_map_field_migration(pub):
......
4497 4497
    assert formdef.data_class().count() == 1
4498 4498
    data_id = formdef.data_class().select()[0].id
4499 4499
    data = formdef.data_class().get(data_id)
4500
    assert data.data == {'1': {'lat': 1.234, 'lon': -1.234}, '3': 'bar'}
4500
    assert data.data == {'1': {'lat': 1.234, 'lon': -1.234}, '1_display': '1.234;-1.234', '3': 'bar'}
4501

  
4502

  
4503
def test_form_map_data_source(pub, http_requests):
4504
    NamedDataSource.wipe()
4505
    data_source = NamedDataSource(name='foobar')
4506
    data_source.data_source = {
4507
        'type': 'geojson',
4508
        'value': 'http://remote.example.net/geojson',
4509
    }
4510
    data_source.id_property = 'id'
4511
    data_source.label_template_property = '{{ text }}'
4512
    data_source.store()
4513

  
4514
    formdef = create_formdef()
4515
    formdef.fields = [
4516
        fields.MapField(id='1', label='map', data_source={'type': 'foobar'}),
4517
    ]
4518
    formdef.store()
4519
    formdef.data_class().wipe()
4520
    resp = get_app(pub).get('/test/')
4521
    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-url'] == '/api/geojson/foobar'
4522
    assert resp.pyquery('div[data-markers-radio-name]')[0].attrib['data-markers-radio-name'] == 'f1$marker_id'
4523
    # simulate qommon.map.js that will create radio inputs
4524
    resp.form.fields['f1$marker_id'] = [Radio(form=resp.form, tag='input', name='f1$marker_id', pos=5)]
4525
    resp.form.fields['f1$marker_id'][0].options.append(('1', False, None))
4526
    resp.form.fields['f1$marker_id'][0].options.append(('2', False, None))
4527
    resp.form.fields['f1$marker_id'][0].optionPositions.append(5)
4528
    resp.form.fields['f1$marker_id'][0].optionPositions.append(6)
4529
    resp.form.field_order.append(('f1$marker_id', resp.form.fields['f1$marker_id'][0]))
4530
    resp.form['f1$marker_id'].value = '1'  # click on marker
4531
    resp.form['f1$latlng'] = '1;2'  # set via js
4532
    resp = resp.form.submit('submit')
4533
    assert 'Check values then click submit.' in resp
4534
    # selected option is displayed as readonly:
4535
    assert resp.pyquery('input[type=text][value=foo][readonly]')
4536
    assert resp.pyquery('input[type=hidden][name="f1$marker_id"][value="1"]')
4537
    assert int(float(resp.pyquery('.qommon-map')[0].attrib['data-init-lat'])) == 1
4538
    assert int(float(resp.pyquery('.qommon-map')[0].attrib['data-init-lng'])) == 2
4539
    resp = resp.form.submit('submit')
4540
    resp = resp.follow()
4541
    assert 'The form has been recorded' in resp
4542
    assert resp.pyquery('input[type=text][value=foo][readonly]')
4543
    assert int(float(resp.pyquery('.qommon-map')[0].attrib['data-init-lat'])) == 1
4544
    assert int(float(resp.pyquery('.qommon-map')[0].attrib['data-init-lng'])) == 2
4545
    assert formdef.data_class().count() == 1
4546
    data_id = formdef.data_class().select()[0].id
4547
    data = formdef.data_class().get(data_id)
4548
    assert data.data == {'1': {'lat': 1.0, 'lon': 2.0, 'marker_id': '1'}, '1_display': 'foo'}
4501 4549

  
4502 4550

  
4503 4551
def test_form_middle_session_change(pub):
tests/test_formdata.py
20 20
from wcs.carddef import CardDef
21 21
from wcs.categories import Category
22 22
from wcs.conditions import Condition
23
from wcs.data_sources import NamedDataSource
23 24
from wcs.formdef import FormDef
24 25
from wcs.formdata import Evolution
25 26
from wcs.roles import Role
......
628 629
        },
629 630
    }
630 631
    formdata.data['5'].receive([b'hello world'])
632
    formdata.data['7_display'] = formdef.fields[7].store_display_value(formdata.data, '7')
631 633
    formdata.geolocations = {'base': {'lat': 1, 'lon': 2}}
632 634
    formdata.store()
633 635
    pub.substitutions.feed(pub)
......
762 764
    assert lazy_formdata.tracking_code == tracking_code.id
763 765

  
764 766

  
767
def test_lazy_formdata_map_item_field(pub, http_requests):
768
    NamedDataSource.wipe()
769
    data_source = NamedDataSource(name='foobar')
770
    data_source.data_source = {
771
        'type': 'geojson',
772
        'value': 'http://remote.example.net/geojson',
773
    }
774
    data_source.id_property = 'id'
775
    data_source.label_template_property = '{{ text }}'
776
    data_source.store()
777

  
778
    FormDef.wipe()
779
    formdef = FormDef()
780
    formdef.name = 'foobar map item'
781
    formdef.fields = [
782
        fields.MapField(id='14', label='map-item', varname='map_item', data_source={'type': 'foobar'}),
783
    ]
784
    formdef.store()
785
    formdef.data_class().wipe()
786
    formdata = formdef.data_class()()
787
    formdata.just_created()
788
    formdata.data = {
789
        '14': {'lat': 1, 'lon': 2, 'marker_id': '1'},  # map-item
790
    }
791
    formdata.data['14_display'] = formdef.fields[0].store_display_value(formdata.data, '14')
792
    formdata.store()
793
    pub.substitutions.feed(pub)
794
    pub.substitutions.feed(formdef)
795
    pub.substitutions.feed(formdata)
796

  
797
    context = pub.substitutions.get_context_variables(mode='lazy')
798
    assert Template('{{form_var_map_item}}').render(context) == 'foo'
799
    assert Template('{{form_var_map_item_lat}}').render(context) == '1'
800
    assert Template('{{form_var_map_item_lon}}').render(context) == '2'
801
    assert Template('{{form_var_map_item_marker_id}}').render(context) == '1'
802

  
803

  
765 804
def test_lazy_formdata_duplicated_varname(pub, variable_test_data):
766 805
    formdef = FormDef.select()[0]
767 806
    formdata = FormDef.select()[0].data_class().select()[0]
......
1148 1187
        pub.substitutions.reset()
1149 1188
        pub.substitutions.feed(formdef)
1150 1189
        with pub.substitutions.temporary_feed(formdata, force_mode=mode):
1151
            assert WorkflowStatusItem.compute('=form_var_map["lat"]', raises=True) == 2
1152 1190
            assert WorkflowStatusItem.compute('{{ form_var_map }}', raises=True) == '2;4'
1153 1191
            assert WorkflowStatusItem.compute('{{ form_var_map|split:";"|first }}', raises=True) == '2'
1154 1192
            assert WorkflowStatusItem.compute('=form_var_map_lat', raises=True) == 2
......
1166 1204
            assert WorkflowStatusItem.compute('{{ form_var_map|distance:form|floatformat }}', raises=True) == '248515.5'
1167 1205

  
1168 1206
    formdata.data['7'] = None
1207
    formdata.data['7_display'] = None
1169 1208
    formdata.store()
1170 1209
    pub.substitutions.reset()
1171 1210
    pub.substitutions.feed(formdef)
tests/test_workflows.py
2965 2965

  
2966 2966
    formdata = formdef.data_class()()
2967 2967
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
2968
    formdata.data['2_display'] = formdef.fields[0].store_display_value(formdata.data, '2')
2968 2969
    formdata.just_created()
2969 2970
    formdata.store()
2970 2971
    pub.substitutions.feed(formdata)
......
2995 2996

  
2996 2997
    formdata = formdef.data_class()()
2997 2998
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
2999
    formdata.data['2_display'] = formdef.fields[0].store_display_value(formdata.data, '2')
2998 3000
    formdata.just_created()
2999 3001
    formdata.store()
3000 3002
    pub.substitutions.feed(formdata)
......
3008 3010
    assert int(formdata.geolocations['base']['lon']) == 2
3009 3011

  
3010 3012
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 3.3233693}}
3013
    formdata.data['2_display'] = formdef.fields[0].store_display_value(formdata.data, '2')
3011 3014
    item.perform(formdata)
3012 3015
    assert int(formdata.geolocations['base']['lat']) == 48
3013 3016
    assert int(formdata.geolocations['base']['lon']) == 3
3014 3017

  
3015 3018
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 4.3233693}}
3019
    formdata.data['2_display'] = formdef.fields[0].store_display_value(formdata.data, '2')
3016 3020
    item.overwrite = False
3017 3021
    item.perform(formdata)
3018 3022
    assert int(formdata.geolocations['base']['lat']) == 48
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
        }
340

  
329 341
        status, data, headers = {
330 342
            'http://remote.example.net/204': (204, None, None),
331 343
            'http://remote.example.net/400': (400, 'bad request', 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),
354
            'http://remote.example.net/geojson': (200, json.dumps(geojson), None),
342 355
            'http://remote.example.net/json-errstr': (200, '{"data": "", "err": "bug"}', None),
343 356
            'http://remote.example.net/json-errheader0': (200, '{"foo": "bar"}',
344 357
                                              {'x-error-code': '0'}),
wcs/api.py
37 37
from wcs.conditions import Condition, ValidationError
38 38
from wcs.carddef import CardDef
39 39
from wcs.formdef import FormDef
40
from wcs.data_sources import get_object as get_data_source_object
40 41
from wcs.roles import Role, logged_users_role
41 42
from wcs.forms.common import FormStatusPage
42 43
import wcs.qommon.storage as st
......
839 840
        return misc.urlopen(url).read()
840 841

  
841 842

  
843
class GeoJsonDirectory(Directory):
844
    def _q_lookup(self, component):
845
        try:
846
            data_source = get_data_source_object({'type': component})
847
        except KeyError:
848
            raise TraversalError()
849
        get_response().set_content_type('application/json')
850
        return json.dumps(data_source.get_geojson_data())
851

  
852

  
842 853
class ApiDirectory(Directory):
843 854
    _q_exports = ['forms', 'roles', ('reverse-geocoding', 'reverse_geocoding'),
844 855
            'formdefs', 'categories', 'user', 'users', 'code', 'autocomplete',
845
            'cards']
856
            'cards', 'geojson']
846 857

  
847 858
    cards = ApiCardsDirectory()
848 859
    forms = ApiFormsDirectory()
......
852 863
    users = ApiUsersDirectory()
853 864
    code = ApiTrackingCodeDirectory()
854 865
    autocomplete = AutocompleteDirectory()
866
    geojson = GeoJsonDirectory()
855 867

  
856 868
    def roles(self):
857 869
        get_response().set_content_type('application/json')
wcs/data_sources.py
52 52

  
53 53
class DataSourceSelectionWidget(CompositeWidget):
54 54
    def __init__(self, name, value=None, allow_jsonp=True,
55
            allow_geojson=False, allow_named_sources=True, **kwargs):
55
            allow_geojson=False, allow_named_sources=True,
56
            require_configured_geographic_source=False, **kwargs):
56 57
        CompositeWidget.__init__(self, name, value, **kwargs)
57 58

  
58 59
        if not value:
......
60 61

  
61 62
        options = []
62 63
        if allow_named_sources:
63
            options.extend([(x.slug, x.name, x.slug) for x in NamedDataSource.select()])
64
            from wcs.carddef import CardDef
65
            options.extend(list(CardDef.get_as_data_source_options()))
64
            selected_sources = NamedDataSource.select()
65
            if require_configured_geographic_source:
66
                selected_sources = [x for x in selected_sources if x.type == 'geojson']
67
            options.extend([(x.slug, x.name, x.slug) for x in selected_sources])
68
            if not require_configured_geographic_source:
69
                from wcs.carddef import CardDef
70
                options.extend(list(CardDef.get_as_data_source_options()))
66 71
            options.sort(key=lambda x: misc.simplify(x[1]))
67 72

  
68 73
        options.insert(0, (None, _('None'), None))
69
        options.append(('json', _('JSON URL'), 'json'))
70
        if allow_jsonp:
71
            options.append(('jsonp', _('JSONP URL'), 'jsonp'))
72
        if allow_geojson:
73
            options.append(('geojson', _('GeoJSON URL'), 'geojson'))
74
        options.append(('formula', _('Python Expression'), 'python'))
74
        if not require_configured_geographic_source:
75
            options.append(('json', _('JSON URL'), 'json'))
76
            if allow_jsonp:
77
                options.append(('jsonp', _('JSONP URL'), 'jsonp'))
78
            if allow_geojson:
79
                options.append(('geojson', _('GeoJSON URL'), 'geojson'))
80
            options.append(('formula', _('Python Expression'), 'python'))
75 81

  
76 82
        self.add(SingleSelectWidget, 'type', options=options, value=value.get('type'),
77 83
                 attrs={'data-dynamic-display-parent': 'true'})
......
116 122
    return tupled_items
117 123

  
118 124

  
119
def request_json_items(url, data_source):
125
def get_json_from_url(url, data_source):
120 126
    url = sign_url_auto_orig(url)
121 127
    geojson = data_source.get('type') == 'geojson'
122 128
    try:
......
141 147
        else:
142 148
            get_logger().warning('Error reading JSON data source output (%s)' % str(e))
143 149
        return None
150
    return entries
151

  
152

  
153
def request_json_items(url, data_source):
154
    geojson = data_source.get('type') == 'geojson'
155
    entries = get_json_from_url(url, data_source)
156
    if entries is None:
157
        return None
144 158
    items = []
145 159
    if geojson:
146 160
        id_property = data_source.get('id_property') or 'id'
......
419 433
                    get_session().get_data_source_query_url_token(self.get_json_query_url()))
420 434
        return None
421 435

  
436
    def get_geojson_url(self):
437
        assert self.type == 'geojson'
438
        return '/api/geojson/%s' % self.slug
439

  
440
    def get_geojson_data(self):
441
        url = self.data_source.get('value').strip()
442
        if Template.is_template_string(url):
443
            vars = get_publisher().substitutions.get_context_variables(mode='lazy')
444
            url = get_variadic_url(url, vars)
445

  
446
        request = get_request()
447
        if hasattr(request, 'datasources_cache') and url in request.datasources_cache:
448
            return request.datasources_cache[url]
449

  
450
        cache_duration = 0
451
        if self.cache_duration:
452
            cache_duration = int(self.cache_duration)
453

  
454
        if cache_duration:
455
            cache_key = 'geojson-data-source-%s' % force_str(hashlib.md5(force_bytes(url)).hexdigest())
456
            from django.core.cache import cache
457
            data = cache.get(cache_key)
458
            if data is not None:
459
                return data
460

  
461
        data = get_json_from_url(url, self.data_source)
462
        id_property = self.id_property or 'id'
463
        label_template_property = self.label_template_property or '{{ text }}'
464

  
465
        for feature in data['features']:
466
            feature['properties']['_id'] = feature['properties'][id_property]
467
            try:
468
                feature['properties']['_text'] = Template(
469
                        label_template_property).render(feature['properties'])
470
            except (TemplateSyntaxError, VariableDoesNotExist):
471
                pass
472
            if not feature['properties'].get('_text'):
473
                feature['properties']['_text'] = feature['properties']['_id']
474

  
475
        if hasattr(request, 'datasources_cache'):
476
            request.datasources_cache[url] = data
477
        if cache_duration:
478
            cache.set(cache_key, data, cache_duration)
479

  
480
        return data
481

  
422 482
    def get_value_by_id(self, param_name, param_value):
423 483
        url = self.data_source.get('value').strip()
424 484
        if Template.is_template_string(url):
......
465 525
        if self.type == 'json' and self.id_parameter:
466 526
            value = self.get_value_by_id(self.id_parameter, option_id)
467 527
        else:
468
            structured_items = get_structured_items(self.data_source, mode='lazy')
528
            structured_items = get_structured_items(self.extended_data_source, mode='lazy')
469 529
            for item in structured_items:
470 530
                if str(item['id']) == str(option_id):
471 531
                    value = item
wcs/fields.py
544 544
    def perform_more_widget_changes(self, form, kwargs, edit = True):
545 545
        pass
546 546

  
547

  
548
    def add_to_view_form(self, form, value = None):
549
        kwargs = {'render_br': False}
547
    def add_to_view_form(self, form, value=None, display_value=None, **kwargs):
548
        widget_kwargs = {'render_br': False}
550 549

  
551 550
        self.field_key = 'f%s' % self.id
552
        self.perform_more_widget_changes(form, kwargs, False)
551
        self.perform_more_widget_changes(form, widget_kwargs, False)
553 552

  
554 553
        for k in self.extra_attributes:
555 554
            if hasattr(self, k):
556
                kwargs[k] = getattr(self, k)
555
                widget_kwargs[k] = getattr(self, k)
557 556

  
558
        if self.widget_class is StringWidget and not 'size' in kwargs and value:
557
        if self.widget_class is StringWidget and not 'size' in widget_kwargs and value:
559 558
            # set a size if there is not one already defined, this will be for
560 559
            # example the case with ItemField
561
            kwargs['size'] = len(value)
560
            widget_kwargs['size'] = len(value)
562 561

  
563
        form.add(self.widget_class, self.field_key, title = self.label,
564
                value = value, readonly = 'readonly', **kwargs)
562
        if display_value and getattr(self.widget_class, 'allow_display_value', True):
563
            widget_kwargs['display_value'] = display_value
564

  
565
        form.add(self.widget_class, self.field_key, title=self.label,
566
                value=value, readonly='readonly', **widget_kwargs)
565 567
        widget = form.get_widget(self.field_key)
566 568
        widget.transfer_form_value(get_request())
567 569
        widget.field = self
......
570 572
                widget.extra_css_class = '%s %s' % (widget.extra_css_class, self.extra_css_class)
571 573
            else:
572 574
                widget.extra_css_class = self.extra_css_class
575
        return widget
573 576

  
574 577
    def get_display_locations_options(self):
575 578
        return [('validation', _('Validation Page')),
......
1370 1373
            value = self.convert_value_to_str(value)
1371 1374
        return WidgetField.add_to_form(self, form, value=value)
1372 1375

  
1373
    def add_to_view_form(self, form, value=None):
1376
    def add_to_view_form(self, form, value=None, **kwargs):
1374 1377
        value = strftime(misc.date_format(), value)
1375
        return super().add_to_view_form(form, value=value)
1378
        return super().add_to_view_form(form, value=value, **kwargs)
1376 1379

  
1377 1380
    def get_view_value(self, value, **kwargs):
1378 1381
        try:
......
1582 1585
        span.text = od_clean_text(force_text(value))
1583 1586
        return span
1584 1587

  
1585
    def add_to_view_form(self, form, value = None):
1588
    def add_to_view_form(self, form, value=None, **kwargs):
1586 1589
        real_value = value
1587 1590
        label_value = ''
1588 1591
        if value is not None:
......
2406 2409
    max_zoom = None
2407 2410
    default_position = None
2408 2411
    init_with_geoloc = False
2412
    data_source = {}
2409 2413

  
2410 2414
    widget_class = MapWidget
2411 2415
    extra_attributes = ['initial_zoom', 'min_zoom', 'max_zoom',
......
2434 2438
        form.add(CheckboxWidget, 'init_with_geoloc',
2435 2439
                title=_('Initialize position using device geolocation'),
2436 2440
                value=self.init_with_geoloc, required=False)
2441
        form.add(data_sources.DataSourceSelectionWidget, 'data_source',
2442
                 value=self.data_source,
2443
                 title=_('Markers data source'),
2444
                 hint=_('This will fill the map with markers from an external source.'),
2445
                 required=False,
2446
                 require_configured_geographic_source=True,
2447
                 advanced=False)
2437 2448

  
2438 2449
    def check_admin_form(self, form):
2439 2450
        initial_zoom = form.get_widget('initial_zoom').parse()
......
2451 2462
    def get_admin_attributes(self):
2452 2463
        return WidgetField.get_admin_attributes(self) + ['initial_zoom',
2453 2464
                'min_zoom', 'max_zoom', 'default_position',
2454
                'init_with_geoloc']
2465
                'init_with_geoloc', 'data_source']
2466

  
2467
    def add_to_view_form(self, form, value=None, **kwargs):
2468
        widget = super().add_to_view_form(form, value=value, **kwargs)
2469
        if self.data_source:
2470
            data_source = data_sources.get_object(self.data_source)
2471
            widget.add_geojson_markers(data_source.get_geojson_url())
2472
        return widget
2473

  
2474
    def add_to_form(self, form, value=None):
2475
        widget = super().add_to_form(form, value=value)
2476
        if self.data_source:
2477
            data_source = data_sources.get_object(self.data_source)
2478
            widget.add_geojson_markers(data_source.get_geojson_url())
2479
        return widget
2455 2480

  
2456 2481
    def get_prefill_value(self, user=None, force_string=True):
2457 2482
        if self.prefill.get('type') != 'string' or not self.prefill.get('value'):
......
2466 2491
        return (coords, False)
2467 2492

  
2468 2493
    def get_view_value(self, value, **kwargs):
2469
        widget = self.widget_class('x%s' % random.random(), value, readonly=True)
2494
        widget_kwargs = {}
2495
        if 'value_id' in kwargs:  # get raw value
2496
            widget_kwargs['display_value'] = value
2497
            value = kwargs.get('value_id')
2498
        widget = self.widget_class('x%s' % random.random(), value, readonly=True, **widget_kwargs)
2470 2499
        return widget.render_widget_content()
2471 2500

  
2501
    def get_view_short_value(self, value, max_len=30, raw_value=None, **kwargs):
2502
        if self.data_source:
2503
            # value will be _display value
2504
            return value or ''
2505
        return self.get_view_value(value, value_id=raw_value)
2506

  
2472 2507
    def get_rst_view_value(self, value, indent=''):
2473 2508
        if isinstance(value, str):  # compatiblity with old pickled data
2474 2509
            return indent + value
......
2490 2525
            return self.convert_value_from_str(value)
2491 2526
        return value
2492 2527

  
2528
    def get_display_value(self, value):
2529
        if not self.data_source:
2530
            if value:
2531
                # backward compatibility
2532
                return '%(lat)s;%(lon)s' % value
2533
            return ''
2534
        data_source = data_sources.get_object(self.data_source)
2535
        if data_source is None or value is None:
2536
            return ''
2537
        return data_source.get_display_value(value.get('marker_id'))
2538

  
2539
    def store_display_value(self, data, field_id):
2540
        value = data.get(field_id)
2541
        return self.get_display_value(value)
2542

  
2493 2543
    def get_structured_value(self, data):
2494 2544
        return self.get_json_value(data.get(self.id))
2495 2545

  
wcs/formdata.py
676 676
        if max_length is not None:
677 677
            # if max_length is set the target is a backoffice listing/table,
678 678
            # return an html value, appropriately shortened.
679
            field_value = self.data.get('%s_display' % field.id, field_value)
680
            return field.get_view_short_value(field_value, max_length)
679
            display_value = self.data.get('%s_display' % field.id, field_value)
680
            return field.get_view_short_value(display_value, max_length, raw_value=field_value)
681 681
        else:
682 682
            # otherwise return the actual "raw" field value
683 683
            return field_value
......
1172 1172
                continue
1173 1173

  
1174 1174
            value, value_details = f.get_value_info(self.data)
1175
            if value is None and not (f.required and include_unset_required_fields):
1175
            if (value is None and not value_details) and not (f.required and include_unset_required_fields):
1176 1176
                continue
1177 1177

  
1178 1178
            current_page_fields.append({'field': f, 'value': value, 'value_details': value_details})
wcs/formdef.py
665 665
                    field.add_to_view_form(form, value)
666 666
                    form.widgets.append(HtmlWidget(htmltext('</div>')))
667 667
                else:
668
                    field.add_to_view_form(form, value)
668
                    kwargs = {}
669
                    if field.store_display_value:
670
                        kwargs['display_value'] = dict.get(field.id + '_display')
671
                    field.add_to_view_form(form, value, **kwargs)
669 672

  
670 673
            if visible_contents:
671 674
                form.widgets.append(HtmlWidget(htmltext('</div></div>')))
wcs/forms/common.py
491 491
            r += htmltext('<div class="%s">' % ' '.join(css_classes))
492 492
            r += htmltext('<span class="label">%s</span> ') % f.label
493 493
            value, value_details = field_value_info['value'], field_value_info['value_details']
494
            if value is None:
494
            if value is None and not value_details:
495 495
                r += htmltext('<div class="value"><i>%s</i></div>') % _('Not set')
496 496
            else:
497 497
                r += htmltext('<div class="value">')
wcs/qommon/form.py
2361 2361

  
2362 2362
class MapWidget(CompositeWidget):
2363 2363
    template_name = 'qommon/forms/widgets/map.html'
2364
    geojson_markers_url = None
2364 2365

  
2365 2366
    def __init__(self, name, value=None, **kwargs):
2367
        self.display_value = kwargs.pop('display_value', None)
2366 2368
        CompositeWidget.__init__(self, name, value, **kwargs)
2367 2369
        latlng_value = None
2368 2370
        if isinstance(value, str):  # legacy data type
......
2390 2392
    def transfer_form_value(self, request):
2391 2393
        request.form[self.get_widget('latlng').name] = self.point2str(self.value)
2392 2394

  
2395
    def add_geojson_markers(self, url):
2396
        self.geojson_markers_url = url
2397
        self.add(HiddenWidget, 'marker_id')
2398

  
2393 2399
    def initial_position(self):
2394 2400
        if isinstance(self.value, str):
2395 2401
            return {'lat': self.value.split(';')[0],
......
2408 2414
        if self.value:
2409 2415
            lat, lon = self.value.split(';')
2410 2416
            self.value = misc.normalize_geolocation({'lat': lat, 'lon': lon})
2417
            if self.geojson_markers_url:
2418
                self.value['marker_id'] = self.get('marker_id')
2411 2419

  
2412 2420

  
2413 2421
class HiddenErrorWidget(HiddenWidget):
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
           geo_json.addTo(map);
110
         }
111
       );
112
     } else if ($map_widget.data('readonly') && initial_marker_id) {
113
       // readonly and marker
114
       console.log('plop');
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.html
2 2

  
3 3
{% block widget-control %}
4 4
{% localize off %}
5
{% if widget.display_value and widget.readonly %}
6
<div class="display-value"><input type="text"
7
  value="{{widget.display_value}}" readonly size="{{widget.display_value|length}}"></div>
8
{% endif %}
5 9
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value.lat}};{{widget.value.lon}}"{% endif %}>
6 10
<div id="map-{{widget.name}}" class="qommon-map"
7 11
  {% if widget.readonly %}data-readonly="true"{% endif %}
......
11 15
    data-init-lat="{{ widget.initial_position.lat }}"
12 16
    data-init-lng="{{ widget.initial_position.lng }}"
13 17
  {% endif %}
18
  {% if widget.geojson_markers_url %}
19
    {% if not widget.readonly %}
20
    data-markers-url="{{ widget.geojson_markers_url }}"
21
    data-markers-radio-name="{{widget.name}}$marker_id"
22
    {% endif %}
23
    {% if widget.value.marker_id %}
24
    data-markers-initial-id="{{ widget.value.marker_id }}"
25
    {% endif %}
26
  {% endif %}
14 27
></div>
28
{% if widget.readonly and widget.geojson_markers_url and widget.value.marker_id %}
29
<input type="hidden" name="{{widget.name}}$marker_id" value="{{ widget.value.marker_id }}">
30
{% endif %}
15 31
{% endlocalize %}
32

  
33
{% if widget.geojson_markers_url %}
34
<style>
35
.item-marker input + span {
36
  position: relative;
37
  z-index: 1000;
38
  margin-top: -55px;
39
  margin-left: -5px;
40
  display: block;
41
  width: 25px;
42
  height: 41px;
43
  background: url(/static/images/blank-marker-icon.png);
44
}
45
.item-marker input:checked + span {
46
  background: url(/static/xstatic/images/marker-icon.png);
47
}
48
.MapWidget div.display-value {
49
  max-width: 100%;
50
}
51
</style>
52
{% endif %}
16 53
{% endblock %}
wcs/variables.py
840 840
        return self._data.get(self._field.id).split(*args, **kwargs)
841 841

  
842 842
    def inspect_keys(self):
843
        if self._field.data_source:
844
            return ['lat', 'lon', 'marker_id']
843 845
        return ['lat', 'lon']
844 846

  
847
    @property
848
    def marker_id(self):
849
        value = self._data.get(self._field.id)
850
        if value:
851
            return value.get('marker_id')
852

  
845 853
    def __str__(self):
846
        # backward compatibility
854
        if self._field.data_source:
855
            value = self._data.get(self._field.id + '_display')
856
            if value:
857
                return value
858
        # "lat;lon" for backward compatibility
847 859
        value = self._data.get(self._field.id)
848 860
        if not value:
849 861
            return ''
850
-