Projet

Général

Profil

0001-general-switch-map-field-value-to-be-a-dictionary-46.patch

Frédéric Péters, 14 septembre 2020 16:38

Télécharger (27,2 ko)

Voir les différences:

Subject: [PATCH 1/3] general: switch map field value to be a dictionary
 (#46617)

 tests/test_admin_pages.py                     |  2 +-
 tests/test_api.py                             | 48 +++++++--------
 tests/test_backoffice_pages.py                |  6 +-
 tests/test_fields.py                          |  8 +--
 tests/test_form_pages.py                      | 26 ++++++++-
 tests/test_formdata.py                        |  9 ++-
 tests/test_publisher.py                       |  2 +-
 tests/test_sql.py                             | 47 +++++++++++++++
 tests/test_widgets.py                         |  2 +-
 tests/test_workflows.py                       |  8 +--
 wcs/backoffice/data_management.py             |  2 +-
 wcs/backoffice/management.py                  |  2 +-
 wcs/fields.py                                 | 28 ++++-----
 wcs/formdef.py                                |  2 +-
 wcs/qommon/form.py                            | 23 ++++++--
 wcs/qommon/publisher.py                       | 15 +++--
 .../templates/qommon/forms/widgets/map.html   |  4 +-
 wcs/sql.py                                    | 58 ++++++++++++++++++-
 wcs/variables.py                              |  7 +++
 19 files changed, 224 insertions(+), 75 deletions(-)
tests/test_admin_pages.py
3044 3044
    resp = resp.click('Geolocation')
3045 3045
    assert 'value="1.234;-1.234' in resp.text
3046 3046
    pub.reload_cfg()
3047
    assert pub.cfg['misc']['default-position'] == '1.234;-1.234'
3047
    assert pub.cfg['misc']['default-position'] == {'lat': 1.234, 'lon': -1.234}
3048 3048

  
3049 3049

  
3050 3050
def test_wscalls_new(pub):
tests/test_api.py
854 854

  
855 855
    assert data_class.get(resp.json['data']['id']).data['4'].orig_filename == 'test.txt'
856 856
    assert data_class.get(resp.json['data']['id']).data['4'].get_content() == b'test'
857
    assert data_class.get(resp.json['data']['id']).data['5'] == '1.5;2.25'
857
    assert data_class.get(resp.json['data']['id']).data['5'] == {'lat': 1.5, 'lon': 2.25}
858 858
    # test bijectivity
859 859
    assert (formdef.fields[3].get_json_value(data_class.get(resp.json['data']['id']).data['3']) ==
860 860
            payload['data']['date'])
......
900 900
        '2_display': 'bar',
901 901
        '3': time.strptime('1970-01-01', '%Y-%m-%d'),
902 902
        '4': upload,
903
        '5': '1.5;2.25',
904 903
        'bo1': 'backoffice field',
905 904
    }
906 905
    formdata.just_created()
907 906
    formdata.evolution[-1].status = 'wf-new'
908 907
    formdata.store()
909 908

  
910
    payload = json.loads(
911
                json.dumps(formdata.get_json_export_dict(),
912
                    cls=qommon.misc.JSONEncoder))
913
    signed_url = sign_url('http://example.net/api/formdefs/test/submit?orig=coucou', '1234')
914
    url = signed_url[len('http://example.net'):]
915

  
916
    resp = get_app(pub).post_json(url, payload)
917
    assert resp.json['err'] == 0
918
    new_formdata = formdef.data_class().get(resp.json['data']['id'])
919
    assert new_formdata.data['0'] == formdata.data['0']
920
    assert new_formdata.data['1'] == formdata.data['1']
921
    assert new_formdata.data['1_display'] == formdata.data['1_display']
922
    assert new_formdata.data['1_structured'] == formdata.data['1_structured']
923
    assert new_formdata.data['2'] == formdata.data['2']
924
    assert new_formdata.data['2_display'] == formdata.data['2_display']
925
    assert new_formdata.data['3'] == formdata.data['3']
926
    assert new_formdata.data['4'].get_content() == formdata.data['4'].get_content()
927
    assert new_formdata.data['5'] == formdata.data['5']
928
    assert new_formdata.data['bo1'] == formdata.data['bo1']
929
    assert not new_formdata.data.get('6')
930
    assert new_formdata.user_id is None
909
    for map_value in ('1.5;2.25', {'lat': 1.5, 'lon': 2.25}):
910
        formdata.data['5'] = map_value
911

  
912
        payload = json.loads(
913
                    json.dumps(formdata.get_json_export_dict(),
914
                        cls=qommon.misc.JSONEncoder))
915
        signed_url = sign_url('http://example.net/api/formdefs/test/submit?orig=coucou', '1234')
916
        url = signed_url[len('http://example.net'):]
917

  
918
        resp = get_app(pub).post_json(url, payload)
919
        assert resp.json['err'] == 0
920
        new_formdata = formdef.data_class().get(resp.json['data']['id'])
921
        assert new_formdata.data['0'] == formdata.data['0']
922
        assert new_formdata.data['1'] == formdata.data['1']
923
        assert new_formdata.data['1_display'] == formdata.data['1_display']
924
        assert new_formdata.data['1_structured'] == formdata.data['1_structured']
925
        assert new_formdata.data['2'] == formdata.data['2']
926
        assert new_formdata.data['2_display'] == formdata.data['2_display']
927
        assert new_formdata.data['3'] == formdata.data['3']
928
        assert new_formdata.data['4'].get_content() == formdata.data['4'].get_content()
929
        assert new_formdata.data['5'] == {'lat': 1.5, 'lon': 2.25}
930
        assert new_formdata.data['bo1'] == formdata.data['bo1']
931
        assert not new_formdata.data.get('6')
932
        assert new_formdata.user_id is None
931 933

  
932 934
    # add an extra attribute
933 935
    payload['extra'] = {'foobar6': 'YYY'}
tests/test_backoffice_pages.py
6381 6381
            "will be ignored - type File Upload not supported,"
6382 6382
            "foo@example.com,"
6383 6383
            "value,"
6384
            "value\r\n" % (pub.get_default_position(), today))
6384
            "value\r\n" % (
6385
                '%(lat)s;%(lon)s' % pub.get_default_position(),
6386
                today))
6385 6387

  
6386 6388
    resp.forms[0]['file'] = Upload('test.csv', b'\0', 'text/csv')
6387 6389
    resp = resp.forms[0].submit()
......
6409 6411
    assert 'Column File will be ignored: type File Upload not supported.' in resp
6410 6412
    assert carddef.data_class().count() == 149
6411 6413
    card1, card2 = carddef.data_class().select(order_by='id')[:2]
6412
    assert card1.data['1'] == '48.81;2.37'
6414
    assert card1.data['1'] == {'lat': 48.81, 'lon': 2.37}
6413 6415
    assert card1.data['2'] == 'data1'
6414 6416
    assert card1.data['3'] is True
6415 6417
    assert card1.data['5'].tm_mday == 2
tests/test_fields.py
313 313

  
314 314

  
315 315
def test_map():
316
    assert fields.MapField().get_json_value('42.2;10.2') == {'lat': 42.2, 'lon': 10.2}
317
    assert fields.MapField().get_json_value('-42.2;10.2') == {'lat': -42.2, 'lon': 10.2}
318
    assert fields.MapField().get_json_value(' 42.2 ; 10.2 ') == {'lat': 42.2, 'lon': 10.2}
319
    assert fields.MapField().get_json_value('') == None
320
    assert fields.MapField().get_json_value('foobar') == None
316
    assert fields.MapField().get_json_value({'lat': 42.2, 'lon': 10.2}) == {'lat': 42.2, 'lon': 10.2}
317
    assert fields.MapField().get_json_value({'lat': -42.2, 'lon': 10.2}) == {'lat': -42.2, 'lon': 10.2}
318
    assert fields.MapField().get_json_value(None) is None
321 319

  
322 320

  
323 321
def test_item_render():
tests/test_form_pages.py
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 == {'1': 'bla', '0': '1.234;-1.234'}
4430
    assert data.data == {'0': {'lat': 1.234, 'lon': -1.234}, '1': 'bla'}
4431

  
4432

  
4433
def test_map_field_migration(pub):
4434
    if pub.is_using_postgresql():
4435
        pytest.skip('this is not relevant in SQL mode')
4436
        return
4437

  
4438
    user = create_user(pub)
4439
    formdef = create_formdef()
4440
    formdef.data_class().wipe()
4441
    formdef.fields = [fields.MapField(id='0', label='map', type='map')]
4442
    formdef.store()
4443
    formdata = formdef.data_class()()
4444
    formdata.user_id = user.id
4445
    formdata.data = {'0': '1;2'}  # legacy data type
4446
    formdata.just_created()
4447
    formdata.store()
4448

  
4449
    app = login(get_app(pub), username='foo', password='foo')
4450
    resp = app.get(formdata.get_url())
4451
    assert 'data-init-lat="1"' in resp
4452
    assert 'data-init-lng="2"' in resp
4431 4453

  
4432 4454

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

  
4480 4502

  
4481 4503
def test_form_middle_session_change(pub):
tests/test_formdata.py
614 614
        '4_display': 'aa, ac',
615 615
        '5': PicklableUpload('test.txt', 'text/plain'),
616 616
        '6': 'other',
617
        '7': '2;4',  # map
617
        '7': {'lat': 2, 'lon': 4},  # map
618 618
        '8': time.strptime('2018-08-31', '%Y-%m-%d'),
619 619
        '9': '2018-07-31',
620 620
        '10': '3',
......
1120 1120
        pub.substitutions.reset()
1121 1121
        pub.substitutions.feed(formdef)
1122 1122
        with pub.substitutions.temporary_feed(formdata, force_mode=mode):
1123
            assert WorkflowStatusItem.compute('=form_var_map', raises=True) == '2;4'
1123
            assert WorkflowStatusItem.compute('=form_var_map["lat"]', raises=True) == 2
1124 1124
            assert WorkflowStatusItem.compute('{{ form_var_map }}', raises=True) == '2;4'
1125
            assert WorkflowStatusItem.compute('=form_var_map.split(";")[0]', raises=True) == '2'
1126 1125
            assert WorkflowStatusItem.compute('{{ form_var_map|split:";"|first }}', raises=True) == '2'
1127 1126
            assert WorkflowStatusItem.compute('=form_var_map_lat', raises=True) == 2
1128
            assert WorkflowStatusItem.compute('{{ form_var_map_lat }}', raises=True) == '2.0'
1127
            assert WorkflowStatusItem.compute('{{ form_var_map_lat }}', raises=True) == '2'
1129 1128
            assert WorkflowStatusItem.compute('=form_var_map_lon', raises=True) == 4
1130
            assert WorkflowStatusItem.compute('{{ form_var_map_lon }}', raises=True) == '4.0'
1129
            assert WorkflowStatusItem.compute('{{ form_var_map_lon }}', raises=True) == '4'
1131 1130

  
1132 1131
            assert WorkflowStatusItem.compute('{{ form_var_map|distance:form_var_map|floatformat }}', raises=True) == '0'
1133 1132
            assert WorkflowStatusItem.compute('{{ form_var_map|distance:"2.1;4.1"|floatformat }}', raises=True) == '15685.4'
tests/test_publisher.py
158 158

  
159 159

  
160 160
def test_get_default_position():
161
    assert pub.get_default_position() == '50.84;4.36'
161
    assert pub.get_default_position() == {'lat': 50.84, 'lon': 4.36}
162 162

  
163 163

  
164 164
def test_import_config_zip():
tests/test_sql.py
2063 2063
    assert sql.SqlUser.count() == 2
2064 2064
    assert sql.SqlUser.get(id=user.id).is_active is False
2065 2065
    assert sql.SqlUser.get(id=user2.id).is_active is True
2066

  
2067

  
2068
@postgresql
2069
def test_migration_43_map_data_type():
2070
    formdef = FormDef()
2071
    formdef.name = 'test map migration'
2072
    formdef.fields = [
2073
        fields.MapField(id='1', label='map', type='map'),
2074
    ]
2075
    formdef.store()
2076

  
2077
    formdata1 = formdef.data_class(mode='sql')()
2078
    formdata1.just_created()
2079
    formdata1.store()
2080

  
2081
    formdata2 = formdef.data_class(mode='sql')()
2082
    formdata2.just_created()
2083
    formdata2.store()
2084

  
2085
    formdata3 = formdef.data_class(mode='sql')()
2086
    formdata3.just_created()
2087
    formdata3.store()
2088

  
2089
    conn, cur = sql.get_connection_and_cursor()
2090
    cur.execute('UPDATE wcs_meta SET value = 42 WHERE key = %s', ('sql_level',))
2091
    conn.commit()
2092
    cur.close()
2093

  
2094
    conn, cur = sql.get_connection_and_cursor()
2095
    cur.execute('ALTER TABLE %s DROP COLUMN f1 CASCADE' % sql.get_formdef_table_name(formdef))
2096
    cur.execute('ALTER TABLE %s ADD COLUMN f1 VARCHAR' % sql.get_formdef_table_name(formdef))
2097
    cur.execute('UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', ('1;2', formdata1.id))
2098
    cur.execute('UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', ('', formdata2.id))
2099
    cur.execute('UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', (None, formdata3.id))
2100
    conn.commit()
2101
    cur.close()
2102

  
2103
    sql.migrate()
2104

  
2105
    conn, cur = sql.get_connection_and_cursor()
2106
    assert migration_level(cur) >= 43
2107
    conn.commit()
2108
    cur.close()
2109

  
2110
    assert formdef.data_class(mode='sql').get(formdata1.id).data['1'] == {'lat': 1, 'lon': 2}
2111
    assert formdef.data_class(mode='sql').get(formdata2.id).data['1'] is None
2112
    assert formdef.data_class(mode='sql').get(formdata3.id).data['1'] is None
tests/test_widgets.py
970 970
    widget = MapWidget('test', title='Map')
971 971
    mock_form_submission(req, widget, hidden_html_vars={'test$latlng': '1.23;2.34'})
972 972
    assert not widget.has_error()
973
    assert widget.parse() == '1.23;2.34'
973
    assert widget.parse() == {'lat': 1.23, 'lon': 2.34}
974 974

  
975 975
    assert '<label' in str(widget.render())
976 976
    assert not '<label ' in str(widget.render_widget_content())
tests/test_workflows.py
2964 2964
    formdef.store()
2965 2965

  
2966 2966
    formdata = formdef.data_class()()
2967
    formdata.data = {'2': '48.8337085;2.3233693'}
2967
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
2968 2968
    formdata.just_created()
2969 2969
    formdata.store()
2970 2970
    pub.substitutions.feed(formdata)
......
2994 2994
    formdef.store()
2995 2995

  
2996 2996
    formdata = formdef.data_class()()
2997
    formdata.data = {'2': '48.8337085;2.3233693'}
2997
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
2998 2998
    formdata.just_created()
2999 2999
    formdata.store()
3000 3000
    pub.substitutions.feed(formdata)
......
3007 3007
    assert int(formdata.geolocations['base']['lat']) == 48
3008 3008
    assert int(formdata.geolocations['base']['lon']) == 2
3009 3009

  
3010
    formdata.data = {'2': '48.8337085;3.3233693'}
3010
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 3.3233693}}
3011 3011
    item.perform(formdata)
3012 3012
    assert int(formdata.geolocations['base']['lat']) == 48
3013 3013
    assert int(formdata.geolocations['base']['lon']) == 3
3014 3014

  
3015
    formdata.data = {'2': '48.8337085;4.3233693'}
3015
    formdata.data = {'2': {'lat': 48.8337085, 'lon': 4.3233693}}
3016 3016
    item.overwrite = False
3017 3017
    item.perform(formdata)
3018 3018
    assert int(formdata.geolocations['base']['lat']) == 48
wcs/backoffice/data_management.py
149 149
            elif isinstance(f, fields.EmailField):
150 150
                value = 'foo@example.com'
151 151
            elif isinstance(f, fields.MapField):
152
                value = get_publisher().get_default_position()
152
                value = '%(lat)s;%(lon)s' % get_publisher().get_default_position()
153 153
            else:
154 154
                value = 'value'
155 155
            sample_line.append(value)
wcs/backoffice/management.py
2792 2792
                geoloc_value = formdata.geolocations[geoloc_key]
2793 2793
                map_widget = MapWidget('geoloc_%s' % geoloc_key,
2794 2794
                        readonly=True,
2795
                        value='%(lat)s;%(lon)s' % geoloc_value,
2795
                        value=geoloc_value,
2796 2796
                        render_br=False)
2797 2797
                r += map_widget.render()
2798 2798
            r += htmltext('</div>')
wcs/fields.py
2449 2449
        geolocate.method = 'address_string'
2450 2450
        geolocate.address_string = self.prefill.get('value')
2451 2451
        coords = geolocate.geolocate_address_string(None)
2452
        if not coords:
2453
            return (None, False)
2454
        return ('%(lat)s;%(lon)s' % coords, False)
2452
        return (coords, False)
2455 2453

  
2456 2454
    def get_view_value(self, value, **kwargs):
2457 2455
        widget = self.widget_class('x%s' % random.random(), value, readonly=True)
2458 2456
        return widget.render_widget_content()
2459 2457

  
2460 2458
    def get_rst_view_value(self, value, indent=''):
2461
        return indent + value
2459
        if isinstance(value, str):  # compatiblity with old pickled data
2460
            return indent + value
2461
        return indent + '%(lat)s;%(lon)s' % value
2462 2462

  
2463 2463
    def convert_value_from_str(self, value):
2464 2464
        try:
2465 2465
            lat, lon = [float(x) for x in value.split(';')]
2466 2466
        except (AttributeError, ValueError):
2467 2467
            return None
2468
        return value
2468
        return {'lat': lat, 'lon': lon}
2469 2469

  
2470 2470
    def get_json_value(self, value, **kwargs):
2471
        if not value or ';' not in value:
2472
            return None
2473
        lat, lon = value.split(';')
2474
        try:
2475
            lat = float(lat)
2476
            lon = float(lon)
2477
        except ValueError:
2478
            return None
2479
        return {'lat': lat, 'lon': lon}
2471
        return value
2480 2472

  
2481 2473
    def from_json_value(self, value):
2482
        if 'lat' in value and 'lon' in value:
2483
            return '%s;%s' % (float(value['lat']), float(value['lon']))
2484
        else:
2485
            return None
2474
        if isinstance(value, str):
2475
            # backward compatibility
2476
            return self.convert_value_from_str(value)
2477
        return value
2486 2478

  
2487 2479
    def get_structured_value(self, data):
2488 2480
        return self.get_json_value(data.get(self.id))
wcs/formdef.py
714 714
    def get_field_data(cls, field, widget):
715 715
        d = {}
716 716
        d[field.id] = widget.parse()
717
        if d.get(field.id) is not None and field.convert_value_from_str:
717
        if isinstance(d.get(field.id), str) and field.convert_value_from_str:
718 718
            d[field.id] = field.convert_value_from_str(d[field.id])
719 719
        if d.get(field.id) is not None and field.store_display_value:
720 720
            display_value = field.store_display_value(d, field.id)
wcs/qommon/form.py
2364 2364

  
2365 2365
    def __init__(self, name, value=None, **kwargs):
2366 2366
        CompositeWidget.__init__(self, name, value, **kwargs)
2367
        self.add(HiddenWidget, 'latlng', value=value)
2367
        latlng_value = None
2368
        if isinstance(value, str):  # legacy data type
2369
            latlng_value = value
2370
        elif value:
2371
            latlng_value = '%s;%s' % (value['lat'], value['lon'])
2372
        self.add(HiddenWidget, 'latlng', value=latlng_value)
2368 2373
        widget = self.get_widget('latlng')
2369 2374
        self.readonly = kwargs.pop('readonly', False)
2370 2375
        self.map_attributes = {}
......
2377 2382
            self.map_attributes['data-def-lat'] = kwargs['default_position'].split(';')[0]
2378 2383
            self.map_attributes['data-def-lng'] = kwargs['default_position'].split(';')[1]
2379 2384

  
2385
    def point2str(self, value):
2386
        if not value:
2387
            return None
2388
        return '%s;%s' % (value['lat'], value['lon'])
2389

  
2390
    def transfer_form_value(self, request):
2391
        request.form[self.get_widget('latlng').name] = self.point2str(self.value)
2392

  
2380 2393
    def initial_position(self):
2381
        if self.value:
2394
        if isinstance(self.value, str):
2382 2395
            return {'lat': self.value.split(';')[0],
2383 2396
                    'lng': self.value.split(';')[1]}
2397
        if isinstance(self.value, dict):
2398
            return {'lat': self.value['lat'],
2399
                    'lng': self.value['lon']}
2384 2400
        return None
2385 2401

  
2386 2402
    def add_media(self):
......
2391 2407
        self.value = self.get('latlng')
2392 2408
        if self.value:
2393 2409
            lat, lon = self.value.split(';')
2394
            lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
2395
            self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon'])
2410
            self.value = misc.normalize_geolocation({'lat': lat, 'lon': lon})
2396 2411

  
2397 2412

  
2398 2413
class HiddenErrorWidget(HiddenWidget):
wcs/qommon/publisher.py
855 855
    def get_default_position(self):
856 856
        default_position = self.cfg.get('misc', {}).get('default-position', None)
857 857
        if not default_position:
858
            default_position = self.get_site_option('default_position')
859
        if not default_position:
860
            default_position = '50.84;4.36'
858
            default_position = self.get_site_option('default_position') or '50.84;4.36'
859

  
860
        if isinstance(default_position, str):
861
            default_position = {
862
                'lat': float(default_position.split(';')[0]),
863
                'lon': float(default_position.split(';')[1]),
864
            }
865

  
861 866
        return default_position
862 867

  
863 868
    def get_map_attributes(self):
864 869
        attrs = {}
865
        attrs['data-def-lat'], attrs['data-def-lng'] = self.get_default_position().split(';')
870
        default_position = self.get_default_position()
871
        attrs['data-def-lat'] = default_position['lat']
872
        attrs['data-def-lng'] = default_position['lon']
866 873
        if self.get_site_option('map-bounds-top-left'):
867 874
            attrs['data-max-bounds-lat1'], attrs['data-max-bounds-lng1'] = \
868 875
                    self.get_site_option('map-bounds-top-left').split(';')
wcs/qommon/templates/qommon/forms/widgets/map.html
1 1
{% extends "qommon/forms/widget.html" %}
2 2

  
3 3
{% block widget-control %}
4
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value}}"{% endif %}>
4
{% localize off %}
5
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value.lat}};{{widget.value.lon}}"{% endif %}>
5 6
<div id="map-{{widget.name}}" class="qommon-map"
6 7
  {% if widget.readonly %}data-readonly="true"{% endif %}
7 8
  {% if widget.sync_map_and_address_fields %}data-address-sync="true"{% endif %}
......
11 12
    data-init-lng="{{ widget.initial_position.lng }}"
12 13
  {% endif %}
13 14
></div>
15
{% endlocalize %}
14 16
{% endblock %}
wcs/sql.py
67 67
    'bool': 'boolean',
68 68
    'file': 'bytea',
69 69
    'date': 'date',
70
    'map': 'jsonb',
70 71
    'items': 'text[]',
71 72
    'table': 'text[][]',
72 73
    'table-select': 'text[][]',
......
2672 2673
    return result
2673 2674

  
2674 2675

  
2675
SQL_LEVEL = 42
2676
SQL_LEVEL = 43
2677

  
2678

  
2679
@guard_postgres
2680
def migrate_map_data_type():
2681
    conn, cur = get_connection_and_cursor()
2682

  
2683
    from wcs.formdef import FormDef
2684
    from wcs.carddef import CardDef
2685
    had_changes = False
2686
    for formdef in FormDef.select() + CardDef.select():
2687
        table_name = get_formdef_table_name(formdef)
2688
        cur.execute('''SELECT column_name, udt_name FROM information_schema.columns
2689
                        WHERE table_schema = 'public'
2690
                          AND table_name = %s''', (table_name,))
2691
        existing_fields = {x[0]: x[1] for x in cur.fetchall()}
2692

  
2693
        for field in formdef.get_all_fields():
2694
            if field.type != 'map':
2695
                continue
2696
            database_field_id = get_field_id(field)
2697
            if existing_fields.get(database_field_id) == 'jsonb':
2698
                # already ok
2699
                continue
2700
            sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar')
2701
            cur.execute('''ALTER TABLE %s ADD COLUMN %s %s''' % (
2702
                                    table_name,
2703
                                    'tmp_' + database_field_id,
2704
                                    sql_type))
2705
            cur.execute('''UPDATE %(table_name)s
2706
                              SET tmp_%(column)s = jsonb_build_object(
2707
                                        'lat', split_part(%(column)s, ';', 1)::float,
2708
                                        'lon', split_part(%(column)s, ';', 2)::float)
2709
                              WHERE %(column)s IS NOT NULL
2710
                                AND %(column)s != '' ''' % {
2711
                             'table_name': table_name,
2712
                             'column': database_field_id
2713
                        })
2714
            cur.execute('''ALTER TABLE %s DROP COLUMN %s CASCADE''' % (
2715
                table_name,
2716
                database_field_id))
2717
            cur.execute('''ALTER TABLE %s RENAME COLUMN tmp_%s TO %s''' % (
2718
                table_name,
2719
                database_field_id,
2720
                database_field_id))
2721
            had_changes = True
2722

  
2723
    if had_changes:
2724
        # views have to be recreated
2725
        migrate_views(conn, cur)
2726

  
2727
    conn.commit()
2728
    cur.close()
2676 2729

  
2677 2730

  
2678 2731
def migrate_global_views(conn, cur):
......
2823 2876
    if sql_level < 42:
2824 2877
        # 42: create snapshots table
2825 2878
        do_snapshots_table()
2879
    if sql_level < 43:
2880
        # 43: migrate map field data type
2881
        migrate_map_data_type()
2826 2882

  
2827 2883
    cur.execute('''UPDATE wcs_meta SET value = %s WHERE key = %s''', (
2828 2884
        str(SQL_LEVEL), 'sql_level'))
wcs/variables.py
822 822
    def inspect_keys(self):
823 823
        return ['lat', 'lon']
824 824

  
825
    def __str__(self):
826
        # backward compatibility
827
        value = self._data.get(self._field.id)
828
        if not value:
829
            return ''
830
        return '%(lat)s;%(lon)s' % value
831

  
825 832

  
826 833
class LazyFieldVarPassword(LazyFieldVar):
827 834
    def __getitem__(self, key):
828
-