WIP: general: switch map field value to be a dictionary (#46617) #1051

Draft
fpeters wants to merge 1 commits from wip/46617-map-field-jsonb into main
24 changed files with 250 additions and 120 deletions

View File

@ -1028,7 +1028,7 @@ def test_settings_geolocation(pub):
resp = resp.click('Geolocation')
assert 'value="1.234;-1.234' in resp.text
pub.reload_cfg()
assert pub.cfg['misc']['default-position'] == '1.234;-1.234'
assert pub.cfg['misc']['default-position'] == {'lat': 1.234, 'lon': -1.234}
assert pub.cfg['misc']['default-zoom-level'] == '13'
resp = resp.click('Geolocation')

View File

@ -1204,7 +1204,7 @@ def test_carddef_submit_with_varname(pub, local_user):
assert data_class.get(resp.json['data']['id']).data['4'].orig_filename == 'test.txt'
assert data_class.get(resp.json['data']['id']).data['4'].get_content() == b'test'
assert data_class.get(resp.json['data']['id']).data['5'] == '1.5;2.25'
assert data_class.get(resp.json['data']['id']).data['5'] == {'lat': 1.5, 'lon': 2.25}
# test bijectivity
assert (
carddef.fields[3].get_json_value(data_class.get(resp.json['data']['id']).data['3'])
@ -1282,7 +1282,7 @@ def test_carddef_submit_from_wscall(pub, local_user):
'2_display': 'bar',
'3': time.strptime('1970-01-01', '%Y-%m-%d'),
'4': upload,
'5': '1.5;2.25',
'5': {'lat': 1.5, 'lon': 2.25},
'bo1': 'backoffice field',
}
carddata.just_created()

View File

@ -824,7 +824,7 @@ def test_formdef_submit_with_varname(pub, local_user):
assert data_class.get(resp.json['data']['id']).data['4'].orig_filename == 'test.txt'
assert data_class.get(resp.json['data']['id']).data['4'].get_content() == b'test'
assert data_class.get(resp.json['data']['id']).data['5'] == '1.5;2.25'
assert data_class.get(resp.json['data']['id']).data['5'] == {'lat': 1.5, 'lon': 2.25}
assert data_class.get(resp.json['data']['id']).data['8'] == []
assert data_class.get(resp.json['data']['id']).data['9'] is False
assert data_class.get(resp.json['data']['id']).data['10'] is True
@ -876,32 +876,33 @@ def test_formdef_submit_from_wscall(pub, local_user):
'2_display': 'bar',
'3': time.strptime('1970-01-01', '%Y-%m-%d'),
'4': upload,
'5': '1.5;2.25',
'bo1': 'backoffice field',
}
formdata.just_created()
formdata.evolution[-1].status = 'wf-new'
formdata.store()
payload = json.loads(json.dumps(formdata.get_json_export_dict(), cls=qommon.misc.JSONEncoder))
signed_url = sign_url('http://example.net/api/formdefs/test/submit?orig=coucou', '1234')
url = signed_url[len('http://example.net') :]
for map_value in ('1.5;2.25', {'lat': 1.5, 'lon': 2.25}):
formdata.data['5'] = map_value
payload = json.loads(json.dumps(formdata.get_json_export_dict(), cls=qommon.misc.JSONEncoder))
signed_url = sign_url('http://example.net/api/formdefs/test/submit?orig=coucou', '1234')
url = signed_url[len('http://example.net') :]
resp = get_app(pub).post_json(url, payload)
assert resp.json['err'] == 0
new_formdata = formdef.data_class().get(resp.json['data']['id'])
assert new_formdata.data['0'] == formdata.data['0']
assert new_formdata.data['1'] == formdata.data['1']
assert new_formdata.data['1_display'] == formdata.data['1_display']
assert new_formdata.data['1_structured'] == formdata.data['1_structured']
assert new_formdata.data['2'] == formdata.data['2']
assert new_formdata.data['2_display'] == formdata.data['2_display']
assert new_formdata.data['3'] == formdata.data['3']
assert new_formdata.data['4'].get_content() == formdata.data['4'].get_content()
assert new_formdata.data['5'] == formdata.data['5']
assert new_formdata.data['bo1'] == formdata.data['bo1']
assert not new_formdata.data.get('6')
assert new_formdata.user_id is None
resp = get_app(pub).post_json(url, payload)
assert resp.json['err'] == 0
new_formdata = formdef.data_class().get(resp.json['data']['id'])
assert new_formdata.data['0'] == formdata.data['0']
assert new_formdata.data['1'] == formdata.data['1']
assert new_formdata.data['1_display'] == formdata.data['1_display']
assert new_formdata.data['1_structured'] == formdata.data['1_structured']
assert new_formdata.data['2'] == formdata.data['2']
assert new_formdata.data['2_display'] == formdata.data['2_display']
assert new_formdata.data['3'] == formdata.data['3']
assert new_formdata.data['4'].get_content() == formdata.data['4'].get_content()
assert new_formdata.data['5'] == {'lat': 1.5, 'lon': 2.25}
assert new_formdata.data['bo1'] == formdata.data['bo1']
assert not new_formdata.data.get('6')
assert new_formdata.user_id is None
# add an extra attribute
payload['extra'] = {'foobar6': 'YYY'}

View File

@ -489,7 +489,7 @@ def test_backoffice_cards_import_data_from_csv(pub):
'"value",'
'"id1|id2|...",'
'"value"'
'\r\n' % (pub.get_default_position(), today)
'\r\n' % ('%(lat)s;%(lon)s' % pub.get_default_position(), today)
)
# missing file
@ -521,7 +521,7 @@ def test_backoffice_cards_import_data_from_csv(pub):
assert 'Importing data into cards' in resp
assert carddef.data_class().count() == 149
card1, card2 = carddef.data_class().select(order_by='id')[:2]
assert card1.data['1'] == '48.81;2.37'
assert card1.data['1'] == {'lat': 48.81, 'lon': 2.37}
assert card1.data['2'] == 'data1'
assert card1.data['3'] is True
assert card1.data['5'].tm_mday == 2

View File

@ -982,9 +982,8 @@ def test_inspect_page_map_field(pub, local_user):
formdata = formdef.data_class()()
formdata.just_created()
formdata.data = {
'1': '1.2345;6.789', # valid value
'1': {'lat': 1.2345, 'lon': 6.789}, # valid value
'2': None, # empty value
'3': 'XXX;YYY', # invalid value
}
formdata.jump_status('new')
formdata.store()
@ -997,9 +996,6 @@ def test_inspect_page_map_field(pub, local_user):
assert resp.pyquery('[title="form_var_map2"]')
assert not resp.pyquery('[title="form_var_map2_lat"]')
assert not resp.pyquery('[title="form_var_map2_lon"]')
assert resp.pyquery('[title="form_var_map3"]')
assert not resp.pyquery('[title="form_var_map3_lat"]')
assert not resp.pyquery('[title="form_var_map3_lon"]')
def test_inspect_page_lazy_list(pub):

View File

@ -90,7 +90,7 @@ def test_form_map_field_back_and_submit(pub):
assert formdef.data_class().count() == 1
data_id = formdef.data_class().select()[0].id
data = formdef.data_class().get(data_id)
assert data.data == {'1': 'bla', '0': '1.234;-1.234'}
assert data.data == {'0': {'lat': 1.234, 'lon': -1.234}, '1': 'bla'}
def test_form_map_initial_zoom_level(pub):
@ -191,7 +191,7 @@ def test_form_map_multi_page(pub):
assert formdef.data_class().count() == 1
data_id = formdef.data_class().select()[0].id
data = formdef.data_class().get(data_id)
assert data.data == {'1': '1.234;-1.234', '3': 'bar'}
assert data.data == {'1': {'lat': 1.234, 'lon': -1.234}, '3': 'bar'}
def test_form_map_field_default_position(pub):

View File

@ -442,7 +442,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'a',
'8': ['b'],
'8_display': 'b',
'9': '1.5;2.25',
'9': {'lat': 1.5, 'lon': 2.25},
'10': {'cleartext': 'foo'},
'11': 'computed',
'12': {
@ -458,7 +458,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'b',
'8': ['a', 'b'],
'8_display': 'a, b',
'9': '1.6;2.26',
'9': {'lat': 1.6, 'lon': 2.26},
'10': {'cleartext': 'bar'},
},
],
@ -483,7 +483,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'a',
'8': ['a', 'b'], # changed
'8_display': 'a, b',
'9': '1.5;2.25',
'9': {'lat': 1.5, 'lon': 2.25},
'10': {'cleartext': 'fooo'}, # changed
'11': 'computed',
'12': {
@ -499,7 +499,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'b',
'8': ['a', 'b'],
'8_display': 'a, b',
'9': '1.6;2.27', # changed
'9': {'lat': 1.6, 'lon': 2.27}, # changed
'10': {'cleartext': 'barr'}, # changed
},
{ # new element
@ -513,7 +513,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'b',
'8': ['a', 'b'],
'8_display': 'a, b',
'9': '1.6;2.26',
'9': {'lat': 1.6, 'lon': 2.26},
'10': {'cleartext': 'bar'},
},
],
@ -534,7 +534,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'b',
'8': ['a', 'b'],
'8_display': 'a, b',
'9': '1.5;2.26', # changed
'9': {'lat': 1.5, 'lon': 2.26}, # changed
'10': {'cleartext': 'fooo'},
'11': 'computed',
'12': {
@ -572,7 +572,7 @@ def test_backoffice_show_history(pub, user, formdef_class):
'7_display': 'b',
'8': ['a', 'b'],
'8_display': 'a, b',
'9': '1.5;2.26',
'9': {'lat': 1.5, 'lon': 2.26},
'10': {'cleartext': 'fooo'},
'11': 'computed',
'12': {

View File

@ -480,11 +480,9 @@ def test_comment(pub):
def test_map():
assert fields.MapField().get_json_value('42.2;10.2') == {'lat': 42.2, 'lon': 10.2}
assert fields.MapField().get_json_value('-42.2;10.2') == {'lat': -42.2, 'lon': 10.2}
assert fields.MapField().get_json_value(' 42.2 ; 10.2 ') == {'lat': 42.2, 'lon': 10.2}
assert fields.MapField().get_json_value('') is None
assert fields.MapField().get_json_value('foobar') is None
assert fields.MapField().get_json_value({'lat': 42.2, 'lon': 10.2}) == {'lat': 42.2, 'lon': 10.2}
assert fields.MapField().get_json_value({'lat': -42.2, 'lon': 10.2}) == {'lat': -42.2, 'lon': 10.2}
assert fields.MapField().get_json_value(None) is None
def test_map_migrate():
@ -511,7 +509,7 @@ def test_map_set_value(pub):
formdata.data = {}
formdef.fields[0].set_value(formdata.data, '42;10')
assert formdata.data['5'] == '42;10'
assert formdata.data['5'] == {'lat': 42, 'lon': 10}
substvars = CompatibilityNamesDict()
substvars.update(formdata.get_substitution_variables())
keys = substvars.get_flat_keys()
@ -530,13 +528,8 @@ def test_map_set_value(pub):
# set invalid value, it is ignored
with pytest.raises(fields.SetValueError):
formdef.fields[0].set_value(formdata.data, 'XXX;YYY')
# set invalid value without using set_value()
formdata.data['5'] = 'XXX;YYY'
substvars = CompatibilityNamesDict()
substvars.update(formdata.get_substitution_variables())
keys = substvars.get_flat_keys()
assert 'form_var_map_lon' not in keys
with pytest.raises(fields.SetValueError):
formdef.fields[0].set_value(formdata.data, {'lat': 'XXX', 'lon': 'YYY'})
def test_item_render():

View File

@ -808,7 +808,7 @@ def variable_test_data(pub):
'4_display': 'aa, ac',
'5': PicklableUpload('test.txt', 'text/plain'),
'6': 'other',
'7': '2;4', # map
'7': {'lat': 2, 'lon': 4}, # map
'8': time.strptime('2018-08-31', '%Y-%m-%d'),
'9': '2018-07-31',
'10': '3',
@ -2317,14 +2317,13 @@ def test_lazy_map_variable(pub, variable_test_data):
pub.substitutions.reset()
pub.substitutions.feed(formdef)
with pub.substitutions.temporary_feed(formdata, force_mode=mode):
assert WorkflowStatusItem.compute('=form_var_map', raises=True) == '2;4'
assert WorkflowStatusItem.compute('=form_var_map["lat"]', raises=True) == 2
assert WorkflowStatusItem.compute('{{ form_var_map }}', raises=True) == '2;4'
assert WorkflowStatusItem.compute('=form_var_map.split(";")[0]', raises=True) == '2'
assert WorkflowStatusItem.compute('{{ form_var_map|split:";"|first }}', raises=True) == '2'
assert WorkflowStatusItem.compute('=form_var_map_lat', raises=True) == 2
assert WorkflowStatusItem.compute('{{ form_var_map_lat }}', raises=True) == '2.0'
assert WorkflowStatusItem.compute('{{ form_var_map_lat }}', raises=True) == '2'
assert WorkflowStatusItem.compute('=form_var_map_lon', raises=True) == 4
assert WorkflowStatusItem.compute('{{ form_var_map_lon }}', raises=True) == '4.0'
assert WorkflowStatusItem.compute('{{ form_var_map_lon }}', raises=True) == '4'
assert (
WorkflowStatusItem.compute(
@ -5550,7 +5549,7 @@ def test_rst_form_details_all_fields(pub):
'4': 'string',
'5': 'foo@localhost',
'6': 'para1\npara2',
'7': '2;4', # map
'7': {'lat': 2, 'lon': 4}, # map
'8': False,
'9': upload,
'10': time.strptime('2015-05-12', '%Y-%m-%d'),

View File

@ -186,7 +186,7 @@ def test_register_cronjobs():
def test_get_default_position():
assert pub.get_default_position() == '50.84;4.36'
assert pub.get_default_position() == {'lat': 50.84, 'lon': 4.36}
def test_import_config_zip():

View File

@ -2917,3 +2917,55 @@ def test_sql_data_views(pub_with_views, formdef_class):
assert column_exists_in_table(cur, f'{prefix}_test', 'geoloc_base_x')
conn.commit()
cur.close()
def test_migration_99_map_data_type(pub):
formdef = FormDef()
formdef.name = 'test map migration'
formdef.fields = [
fields.MapField(id='1', label='map'),
]
formdef.store()
formdata1 = formdef.data_class(mode='sql')()
formdata1.just_created()
formdata1.store()
formdata2 = formdef.data_class(mode='sql')()
formdata2.just_created()
formdata2.store()
formdata3 = formdef.data_class(mode='sql')()
formdata3.just_created()
formdata3.store()
conn, cur = sql.get_connection_and_cursor()
cur.execute('UPDATE wcs_meta SET value = 42 WHERE key = %s', ('sql_level',))
conn.commit()
cur.close()
conn, cur = sql.get_connection_and_cursor()
cur.execute('ALTER TABLE %s DROP COLUMN f1 CASCADE' % sql.get_formdef_table_name(formdef))
cur.execute('ALTER TABLE %s ADD COLUMN f1 VARCHAR' % sql.get_formdef_table_name(formdef))
cur.execute(
'UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', ('1;2', formdata1.id)
)
cur.execute(
'UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', ('', formdata2.id)
)
cur.execute(
'UPDATE ' + sql.get_formdef_table_name(formdef) + ' SET f1 = %s WHERE id = %s', (None, formdata3.id)
)
conn.commit()
cur.close()
sql.migrate()
conn, cur = sql.get_connection_and_cursor()
assert migration_level(cur) >= 99
conn.commit()
cur.close()
assert formdef.data_class(mode='sql').get(formdata1.id).data['1'] == {'lat': 1, 'lon': 2}
assert formdef.data_class(mode='sql').get(formdata2.id).data['1'] is None
assert formdef.data_class(mode='sql').get(formdata3.id).data['1'] is None

View File

@ -1465,7 +1465,7 @@ def test_map_widget():
widget = MapWidget('test', title='Map')
mock_form_submission(req, widget, hidden_html_vars={'test$latlng': '1.23;2.34'})
assert not widget.has_error()
assert widget.parse() == '1.23;2.34'
assert widget.parse() == {'lat': 1.23, 'lon': 2.34}
assert '<label' in str(widget.render())
assert '<label ' not in str(widget.render_widget_content())

View File

@ -203,7 +203,7 @@ def test_set_backoffice_field_map(http_requests, pub):
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'1': '42;10', '2': None}
formdata.data = {'1': {'lat': 42, 'lon': 10}, '2': None}
formdata.just_created()
formdata.store()
pub.substitutions.feed(formdata)
@ -214,7 +214,7 @@ def test_set_backoffice_field_map(http_requests, pub):
item.fields = [{'field_id': 'bo1', 'value': '{{ form_var_map1|default:"" }}'}]
item.perform(formdata)
formdata = formdef.data_class().get(formdata.id)
assert formdata.data.get('bo1') == '42;10'
assert formdata.data.get('bo1') == {'lat': 42, 'lon': 10}
item.fields = [{'field_id': 'bo1', 'value': '{{ form_var_map2|default:"" }}'}]
item.perform(formdata)

View File

@ -384,7 +384,7 @@ def test_create_carddata_with_map_field(pub):
formdata.store()
formdata.perform_workflow()
assert carddef.data_class().count() == 1
assert carddef.data_class().select()[0].data.get('1') == '1;2'
assert carddef.data_class().select()[0].data.get('1') == {'lat': 1, 'lon': 2}
# invalid value
create.mappings[0].expression = 'plop'
@ -409,12 +409,12 @@ def test_create_carddata_with_map_field(pub):
formdef.refresh_from_storage()
carddef.data_class().wipe()
formdata = formdef.data_class()()
formdata.data = {'1': '2;3'}
formdata.data = {'1': {'lat': 2, 'lon': 3}}
formdata.just_created()
formdata.store()
formdata.perform_workflow()
assert carddef.data_class().count() == 1
assert carddef.data_class().select()[0].data.get('1') == '2;3'
assert carddef.data_class().select()[0].data.get('1') == {'lat': 2, 'lon': 3}
def test_create_carddata_user_association(pub):

View File

@ -253,7 +253,7 @@ def test_geolocate_map(pub):
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'2': '48.8337085;2.3233693'}
formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
formdata.just_created()
formdata.store()
pub.substitutions.feed(formdata)
@ -294,7 +294,7 @@ def test_geolocate_overwrite(pub):
formdef.store()
formdata = formdef.data_class()()
formdata.data = {'2': '48.8337085;2.3233693'}
formdata.data = {'2': {'lat': 48.8337085, 'lon': 2.3233693}}
formdata.just_created()
formdata.store()
pub.substitutions.feed(formdata)
@ -307,12 +307,12 @@ def test_geolocate_overwrite(pub):
assert int(formdata.geolocations['base']['lat']) == 48
assert int(formdata.geolocations['base']['lon']) == 2
formdata.data = {'2': '48.8337085;3.3233693'}
formdata.data = {'2': {'lat': 48.8337085, 'lon': 3.3233693}}
item.perform(formdata)
assert int(formdata.geolocations['base']['lat']) == 48
assert int(formdata.geolocations['base']['lon']) == 3
formdata.data = {'2': '48.8337085;4.3233693'}
formdata.data = {'2': {'lat': 48.8337085, 'lon': 4.3233693}}
item.overwrite = False
item.perform(formdata)
assert int(formdata.geolocations['base']['lat']) == 48

View File

@ -204,7 +204,7 @@ class CardPage(FormPage):
elif isinstance(f, fields.EmailField):
value = 'foo@example.com'
elif isinstance(f, fields.MapField):
value = get_publisher().get_default_position()
value = '%(lat)s;%(lon)s' % get_publisher().get_default_position()
elif isinstance(f, fields.ItemsField):
value = 'id1|id2|...'
else:

View File

@ -3668,10 +3668,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
r += htmltext('<h3>%s</h3>') % formdata.formdef.geolocations[geoloc_key]
geoloc_value = formdata.geolocations[geoloc_key]
map_widget = MapWidget(
'geoloc_%s' % geoloc_key,
readonly=True,
value='%(lat)s;%(lon)s' % geoloc_value,
render_br=False,
'geoloc_%s' % geoloc_key, readonly=True, value=geoloc_value, render_br=False
)
r += map_widget.render()
r += htmltext('</div>')

View File

@ -196,7 +196,7 @@ class MapField(WidgetField, MapOptionsMixin):
# otherwise it will be interpreted as an address that will be geocoded.
prefill_value, explicit_lock = super().get_prefill_value()
if re.match(r'-?\d+(\.\d+)?;-?\d+(\.\d+)?$', prefill_value):
return (prefill_value, explicit_lock)
return (self.convert_value_from_str(prefill_value), explicit_lock)
from wcs.wf.geolocate import GeolocateWorkflowStatusItem
@ -204,55 +204,57 @@ class MapField(WidgetField, MapOptionsMixin):
geolocate.method = 'address_string'
geolocate.address_string = prefill_value
coords = geolocate.geolocate_address_string(None, compute_template=False)
if not coords:
return (None, False)
return ('%(lat)s;%(lon)s' % coords, False)
return (coords, False)
def get_view_value(self, value, **kwargs):
widget = self.widget_class('x%s' % random.random(), value, readonly=True)
return widget.render_widget_content()
def get_rst_view_value(self, value, indent=''):
return indent + value
try:
return indent + '%(lat)s;%(lon)s' % value
except TypeError:
return ''
def convert_value_from_str(self, value):
try:
dummy, dummy = (float(x) for x in value.split(';'))
lat, lon = (float(x) for x in value.split(';'))
except (AttributeError, ValueError):
return None
return value
def get_json_value(self, value, **kwargs):
if not value or ';' not in value:
return None
lat, lon = value.split(';')
try:
lat = float(lat)
lon = float(lon)
except ValueError:
return None
return {'lat': lat, 'lon': lon}
def get_json_value(self, value, **kwargs):
return value
def from_json_value(self, value):
if 'lat' in value and 'lon' in value:
return '%s;%s' % (float(value['lat']), float(value['lon']))
else:
return None
if isinstance(value, str):
# backward compatibility
return self.convert_value_from_str(value)
return value
def get_structured_value(self, data):
return self.get_json_value(data.get(self.id))
def set_value(self, data, value, raise_on_error=False):
if value == '':
if isinstance(value, dict):
try:
value = {
'lat': float(value['lat']),
'lon': float(value['lon']),
}
except (KeyError, ValueError, TypeError):
raise SetValueError(_('invalid coordinates %r (field id: %s)') % (value, self.id))
elif value == '':
value = None
elif value and ';' not in value:
raise SetValueError(_('invalid coordinates %r (missing ;) (field id: %s)') % (value, self.id))
elif value:
try:
dummy, dummy = (float(x) for x in value.split(';'))
lat, lon = (float(x) for x in value.split(';'))
except ValueError:
# will catch both "too many values to unpack" and invalid float values
raise SetValueError(_('invalid coordinates %r (field id: %s)') % (value, self.id))
value = {'lat': lat, 'lon': lon}
super().set_value(data, value)

View File

@ -1053,7 +1053,7 @@ class FormDef(StorableObject):
def get_field_data(cls, field, widget, raise_on_error=False):
d = {}
d[field.id] = widget.parse()
if d.get(field.id) is not None and field.convert_value_from_str:
if isinstance(d.get(field.id), str) and field.convert_value_from_str:
d[field.id] = field.convert_value_from_str(d[field.id])
field.set_value(d, d[field.id], raise_on_error=raise_on_error)
if getattr(widget, 'cleanup', None):

View File

@ -3600,7 +3600,12 @@ class MapWidget(CompositeWidget):
def __init__(self, name, value=None, **kwargs):
CompositeWidget.__init__(self, name, value, **kwargs)
self.add(HiddenWidget, 'latlng', value=value)
latlng_value = None
if isinstance(value, str): # legacy data type
latlng_value = value
elif value:
latlng_value = '%s;%s' % (value['lat'], value['lon'])
self.add(HiddenWidget, 'latlng', value=latlng_value)
self.readonly = kwargs.pop('readonly', False)
self.init_map_attributes(value, **kwargs)
@ -3651,9 +3656,19 @@ class MapWidget(CompositeWidget):
self.map_attributes['data-def-lng'] = '%.8f' % coords['lon']
self.map_attributes['data-def-template'] = 'true'
def point2str(self, value):
if not value:
return None
return '%s;%s' % (value['lat'], value['lon'])
def transfer_form_value(self, request):
request.form[self.get_widget('latlng').name] = self.point2str(self.value)
def initial_position(self):
if self.value and ';' in self.value:
if isinstance(self.value, str) and ';' in self.value:
return {'lat': self.value.split(';')[0], 'lng': self.value.split(';')[1]}
if isinstance(self.value, dict):
return {'lat': self.value['lat'], 'lng': self.value['lon']}
return None
def add_media(self):
@ -3666,10 +3681,10 @@ class MapWidget(CompositeWidget):
try:
lat, lon = self.value.split(';')
except ValueError:
self.value = None
self.set_error_code('bad_input')
else:
lat_lon = misc.normalize_geolocation({'lat': lat, 'lon': lon})
self.value = '%s;%s' % (lat_lon['lat'], lat_lon['lon']) if lat_lon else None
self.value = misc.normalize_geolocation({'lat': lat, 'lon': lon})
def set_value(self, value):
super().set_value(value)
@ -3695,6 +3710,9 @@ class MapMarkerSelectionWidget(MapWidget):
def initial_position(self):
return None
def transfer_form_value(self, request):
request.form[self.name] = self.value
def _parse(self, request):
CompositeWidget._parse(self, request)
self.value = self.get('marker_id')

View File

@ -795,9 +795,14 @@ class QommonPublisher(Publisher):
def get_default_position(self):
default_position = self.cfg.get('misc', {}).get('default-position', None)
if not default_position:
default_position = self.get_site_option('default_position')
if not default_position:
default_position = '50.84;4.36'
default_position = self.get_site_option('default_position') or '50.84;4.36'
if isinstance(default_position, str):
default_position = {
'lat': float(default_position.split(';')[0]),
'lon': float(default_position.split(';')[1]),
}
return default_position
def get_default_zoom_level(self):
@ -805,7 +810,9 @@ class QommonPublisher(Publisher):
def get_map_attributes(self):
attrs = {}
attrs['data-def-lat'], attrs['data-def-lng'] = self.get_default_position().split(';')
default_position = self.get_default_position()
attrs['data-def-lat'] = default_position['lat']
attrs['data-def-lng'] = default_position['lon']
if self.get_site_option('map-bounds-top-left'):
attrs['data-max-bounds-lat1'], attrs['data-max-bounds-lng1'] = self.get_site_option(
'map-bounds-top-left'

View File

@ -1,16 +1,18 @@
{% extends "qommon/forms/widget.html" %}
{% block widget-control %}
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value}}"{% endif %}>
<div id="map-{{widget.get_name_for_id}}" class="qommon-map"
{% if widget.readonly %}data-readonly="true"{% endif %}
{% if widget.sync_map_and_address_fields %}data-address-sync="true"{% endif %}
{% for key, value in widget.map_attributes.items %}{{key}}="{{value}}" {% endfor %}
{% if widget.initial_position %}
data-init-lat="{{ widget.initial_position.lat }}"
data-init-lng="{{ widget.initial_position.lng }}"
{% endif %}
{% if not widget.readonly %}data-search-url="{% url 'api-geocoding' %}"{% endif %}
{% block widget-control-attributes %}{% endblock %}
></div>
{% localize off %}
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value.lat}};{{widget.value.lon}}"{% endif %}>
<div id="map-{{widget.get_name_for_id}}" class="qommon-map"
{% if widget.readonly %}data-readonly="true"{% endif %}
{% if widget.sync_map_and_address_fields %}data-address-sync="true"{% endif %}
{% for key, value in widget.map_attributes.items %}{{key}}="{{value}}" {% endfor %}
{% if widget.initial_position %}
data-init-lat="{{ widget.initial_position.lat }}"
data-init-lng="{{ widget.initial_position.lng }}"
{% endif %}
{% if not widget.readonly %}data-search-url="{% url 'api-geocoding' %}"{% endif %}
{% block widget-control-attributes %}{% endblock %}
></div>
{% endlocalize %}
{% endblock %}

View File

@ -82,6 +82,7 @@ SQL_TYPE_MAPPING = {
'numeric': 'numeric',
'file': 'bytea',
'date': 'date',
'map': 'jsonb',
'items': 'text[]',
'table': 'text[][]',
'table-select': 'text[][]',
@ -5120,7 +5121,60 @@ def get_period_total(
# latest migration, number + description (description is not used
# programmaticaly but will make sure git conflicts if two migrations are
# separately added with the same number)
SQL_LEVEL = (106, 'add context column to logged_errors table')
SQL_LEVEL = (107, 'change map columns to jsonb')
@atomic
def migrate_map_data_type():
conn, cur = get_connection_and_cursor()
from wcs.carddef import CardDef
from wcs.formdef import FormDef
had_changes = False
for formdef in FormDef.select() + CardDef.select():
table_name = get_formdef_table_name(formdef)
cur.execute(
'''SELECT column_name, udt_name FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = %s''',
(table_name,),
)
existing_fields = {x[0]: x[1] for x in cur.fetchall()}
for field in formdef.get_all_fields():
if field.key != 'map':
continue
database_field_id = get_field_id(field)
if existing_fields.get(database_field_id) == 'jsonb':
# already ok
continue
sql_type = SQL_TYPE_MAPPING.get(field.key, 'varchar')
cur.execute(
'''ALTER TABLE %s ADD COLUMN %s %s''' % (table_name, 'tmp_' + database_field_id, sql_type)
)
cur.execute(
'''UPDATE %(table_name)s
SET tmp_%(column)s = jsonb_build_object(
'lat', split_part(%(column)s, ';', 1)::float,
'lon', split_part(%(column)s, ';', 2)::float)
WHERE %(column)s IS NOT NULL
AND %(column)s != '' '''
% {'table_name': table_name, 'column': database_field_id}
)
cur.execute('''ALTER TABLE %s DROP COLUMN %s CASCADE''' % (table_name, database_field_id))
cur.execute(
'''ALTER TABLE %s RENAME COLUMN tmp_%s TO %s'''
% (table_name, database_field_id, database_field_id)
)
had_changes = True
if had_changes:
# views have to be recreated
migrate_views(conn, cur)
conn.commit()
cur.close()
def migrate_global_views(conn, cur):
@ -5321,6 +5375,9 @@ def migrate():
# 84: add application tables
Application.do_table()
ApplicationElement.do_table()
if sql_level < 107:
# 107: migrate map field data type
migrate_map_data_type()
if sql_level < 52:
# 2: introduction of formdef_id in views
# 5: add concerned_roles_array, is_at_endpoint and fts to views
@ -5421,7 +5478,6 @@ def migrate():
init_global_table(conn, cur)
for formdef in FormDef.select():
do_formdef_tables(formdef, rebuild_views=False, rebuild_global_views=False)
if sql_level < 71:
# 71: python datasource migration
set_reindex('python_ds_migration', 'needed', conn=conn, cur=cur)

View File

@ -1684,6 +1684,13 @@ class LazyFieldVarMap(LazyFieldVarStructured):
def inspect_keys(self):
return ['lat', 'lon'] if self.get_field_var_value() else []
def __str__(self):
# backward compatibility
value = self._data.get(self._field.id)
if not value:
return ''
return '%(lat)s;%(lon)s' % value
class LazyFieldVarLiveSequenceItem(LazyFieldVarLiveCardMixin):
def __init__(self, field, data, idx):