position de carte selon gabarit (#66959) #253

Merged
fpeters merged 1 commits from wip/66959-map-initial-position-template into main 2023-04-21 07:55:07 +02:00
7 changed files with 228 additions and 22 deletions

View File

@ -4493,6 +4493,58 @@ def test_form_map_multi_page(pub):
assert data.data == {'1': '1.234;-1.234', '3': 'bar'}
def test_form_map_field_default_position(pub):
formdef = create_formdef()
formdef.fields = [
fields.PageField(id='0', label='1st page', type='page'),
fields.StringField(id='1', label='address', required=True, varname='address'),
fields.PageField(id='2', label='2nd page', type='page'),
fields.MapField(id='3', label='map'),
]
formdef.store()
formdef.data_class().wipe()
resp = get_app(pub).get('/test/')
resp.form['f1'] = '169 rue du chateau, paris'
resp = resp.form.submit('submit')
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '50.84'
formdef.fields[3].initial_position = 'point'
formdef.fields[3].default_position = '13;12'
formdef.store()
resp = get_app(pub).get('/test/')
resp.form['f1'] = '169 rue du chateau, paris'
resp = resp.form.submit('submit')
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '13'
formdef.fields[3].initial_position = 'geoloc'
formdef.store()
resp = get_app(pub).get('/test/')
resp.form['f1'] = '169 rue du chateau, paris'
resp = resp.form.submit('submit')
assert resp.pyquery('.qommon-map').attr('data-init_with_geoloc')
formdef.fields[3].initial_position = 'template'
formdef.fields[3].position_template = '13;12'
formdef.store()
resp = get_app(pub).get('/test/')
resp.form['f1'] = '169 rue du chateau, paris'
resp = resp.form.submit('submit')
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '13'
formdef.fields[3].initial_position = 'template'
formdef.fields[3].position_template = '{{ form_var_address }}'
formdef.store()
resp = get_app(pub).get('/test/')
resp.form['f1'] = '169 rue du chateau, paris'
with responses.RequestsMock() as rsps:
rsps.get('https://nominatim.entrouvert.org/search', json=[{'lat': '48.8337085', 'lon': '2.3233693'}])
resp = resp.form.submit('submit')
assert resp.pyquery('.qommon-map').attr('data-def-lat') == '48.83370850'
def test_form_middle_session_change(pub):
formdef = create_formdef()
formdef.fields = [

View File

@ -450,6 +450,20 @@ def test_map():
assert fields.MapField().get_json_value('foobar') is None
def test_map_migrate():
field = fields.MapField()
field.init_with_geoloc = True
assert field.migrate()
assert field.initial_position == 'geoloc'
assert not field.migrate()
field = fields.MapField()
field.default_position = '1;2'
assert field.migrate()
assert field.initial_position == 'point'
assert not field.migrate()
def test_map_set_value(pub):
formdef = FormDef()
formdef.name = 'foobar'

View File

@ -2275,6 +2275,11 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
display_mode = 'list'
initial_date_alignment = None
# map options
initial_position = None
default_position = None
position_template = None
def __init__(self, **kwargs):
self.items = []
WidgetField.__init__(self, **kwargs)
@ -2298,7 +2303,14 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
@property
def extra_attributes(self):
if self.display_mode == 'map':
return ['initial_zoom', 'min_zoom', 'max_zoom', 'data_source']
return [
'initial_zoom',
'min_zoom',
'max_zoom',
'initial_position',
'position_template',
'data_source',

Avec le passage en plusieurs lignes on ne le voit pas bien, c'est l'ajout de initial_position et position_template.

Avec le passage en plusieurs lignes on ne le voit pas bien, c'est l'ajout de initial_position et position_template.
]
return []
def get_options(self, mode=None):
@ -2546,6 +2558,34 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
self.fill_zoom_admin_form(
form, attrs={'data-dynamic-display-child-of': 'display_mode', 'data-dynamic-display-value': 'map'}
)
initial_position_widget = form.add(
RadiobuttonsWidget,
'initial_position',
title=_('Initial Position'),
options=(
('', _('Default position (from markers)'), ''),
('template', _('From template'), 'template'),
),

La position de départ est soit déterminée selon les marqueurs (situation actuelle), soit selon ce qui sera donné dans le gabarit en question.

La position de départ est soit déterminée selon les marqueurs (situation actuelle), soit selon ce qui sera donné dans le gabarit en question.
value=self.initial_position or '',
extra_css_class='widget-inline-radio',
attrs={
'data-dynamic-display-child-of': 'display_mode',
'data-dynamic-display-value': 'map',
'data-dynamic-display-parent': 'true',
},
)
form.add(
StringWidget,
'position_template',
value=self.position_template,
size=80,
required=False,
validation_function=ComputedExpressionWidget.validate_template,
attrs={
'data-dynamic-display-child-of': initial_position_widget.get_name(),
'data-dynamic-display-value': 'template',
},
)
def get_admin_attributes(self):
return WidgetField.get_admin_attributes(self) + [
@ -2558,6 +2598,8 @@ class ItemField(WidgetField, MapOptionsMixin, ItemFieldMixin):
'initial_zoom',
'min_zoom',
'max_zoom',
'initial_position',
'position_template',
'initial_date_alignment',
]
@ -3529,31 +3571,73 @@ class MapField(WidgetField, MapOptionsMixin):
key = 'map'
description = _('Map')
initial_position = None
default_position = None
init_with_geoloc = False
position_template = None
widget_class = MapWidget
extra_attributes = ['initial_zoom', 'min_zoom', 'max_zoom', 'default_position', 'init_with_geoloc']
extra_attributes = [
'initial_zoom',
'min_zoom',
'max_zoom',
'initial_position',
'default_position',
'position_template',
]

Pour les champs carte il y a initial_position et position_template ajoutés, init_with_geoloc retiré (il devient une des valeurs possibles pour initial_position).

Pour les champs carte il y a initial_position et position_template ajoutés, init_with_geoloc retiré (il devient une des valeurs possibles pour initial_position).
def migrate(self):
changed = False
if not self.initial_position: # 2023-04-20
if getattr(self, 'init_with_geoloc', False):
self.initial_position = 'geoloc'
changed = True
elif self.default_position:
self.initial_position = 'point'
changed = True
return changed
Review

Code de migration, précédemment on pouvait avoir en parallèle la géoloc et une position par défaut, désormais il faut choisir, le choix de la géoloc était prioritaire avant, donc on garde de manière préférentielle celui-ci.

Code de migration, précédemment on pouvait avoir en parallèle la géoloc et une position par défaut, désormais il faut choisir, le choix de la géoloc était prioritaire avant, donc on garde de manière préférentielle celui-ci.
def fill_admin_form(self, form):
WidgetField.fill_admin_form(self, form)
self.fill_zoom_admin_form(form, tab=('position', _('Position')))
initial_position_widget = form.add(
RadiobuttonsWidget,
'initial_position',
title=_('Initial Position'),
options=(
('', _('Default position'), ''),
('point', _('Specific point'), 'point'),
('geoloc', _('Device geolocation'), 'geoloc'),
('template', _('From template'), 'template'),
),
Review

Pour les champs "carte" on a ces 4 options, les 3 qui existaient + la possibilité de gabarit.

Pour les champs liste affichés sous forme de carte on a uniquement 2 options, le choix par défaut (position selon les marqueurs) et le choix "gabarit" (qui est la demande à l'origine du ticket). Ça pourrait être étendu pour avoir les options point et géoloc mais je me suis dit que j'attendrais la demande.

Pour les champs "carte" on a ces 4 options, les 3 qui existaient + la possibilité de gabarit. Pour les champs liste affichés sous forme de carte on a uniquement 2 options, le choix par défaut (position selon les marqueurs) et le choix "gabarit" (qui est la demande à l'origine du ticket). Ça pourrait être étendu pour avoir les options point et géoloc mais je me suis dit que j'attendrais la demande.
value=self.initial_position or '',
extra_css_class='widget-inline-radio',
tab=('position', _('Position')),
attrs={'data-dynamic-display-parent': 'true'},
)
form.add(
MapWidget,
'default_position',
title=_('Initial Position'),
value=self.default_position,
default_zoom='9',
required=False,
tab=('position', _('Position')),
attrs={
'data-dynamic-display-child-of': initial_position_widget.get_name(),
'data-dynamic-display-value': 'point',
},
)
form.add(
CheckboxWidget,
'init_with_geoloc',
title=_('Initialize position using device geolocation'),
value=self.init_with_geoloc,
StringWidget,
'position_template',
value=self.position_template,
size=80,
required=False,
validation_function=ComputedExpressionWidget.validate_template,
tab=('position', _('Position')),
attrs={
'data-dynamic-display-child-of': initial_position_widget.get_name(),
'data-dynamic-display-value': 'template',
},
)
def check_admin_form(self, form):
@ -3564,8 +3648,9 @@ class MapField(WidgetField, MapOptionsMixin):
'initial_zoom',
'min_zoom',
'max_zoom',
'initial_position',
'default_position',
'init_with_geoloc',
'position_template',
]
def get_prefill_value(self, user=None, force_string=True):

View File

@ -3359,6 +3359,9 @@ class MapWidget(CompositeWidget):
CompositeWidget.__init__(self, name, value, **kwargs)
self.add(HiddenWidget, 'latlng', value=value)
self.readonly = kwargs.pop('readonly', False)
self.init_map_attributes(value, **kwargs)
def init_map_attributes(self, value, **kwargs):
self.map_attributes = {}
self.map_attributes.update(get_publisher().get_map_attributes())
self.sync_map_and_address_fields = get_publisher().has_site_option(
@ -3366,12 +3369,45 @@ class MapWidget(CompositeWidget):
)
if kwargs.get('initial_zoom') is None:
kwargs['initial_zoom'] = get_publisher().get_default_zoom_level()
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom'):
if attribute in kwargs:
self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
if kwargs.get('default_position'):
self.map_attributes['data-def-lat'] = kwargs['default_position'].split(';')[0]
self.map_attributes['data-def-lng'] = kwargs['default_position'].split(';')[1]
initial_position = kwargs.pop('initial_position', None)
default_position = kwargs.pop('default_position', None)
position_template = kwargs.pop('position_template', None)
if not value:
if initial_position == 'geoloc':
self.map_attributes['data-init_with_geoloc'] = 'true'
elif initial_position == 'point' and default_position:
self.map_attributes['data-def-lat'] = default_position.split(';')[0]
self.map_attributes['data-def-lng'] = default_position.split(';')[1]
elif initial_position == 'template' and position_template:
from wcs.workflows import WorkflowStatusItem
try:
position = WorkflowStatusItem.compute(
position_template, raises=True, allow_complex=False, record_errors=False
)
except TemplateError:
pass
else:
if re.match(r'-?\d+(\.\d+)?;-?\d+(\.\d+)?$', position):
# lat;lon
self.map_attributes['data-def-lat'] = position.split(';')[0]
self.map_attributes['data-def-lng'] = position.split(';')[1]
else:
# address?
Review

C'est le même comportement que celui adopté pour le préremplissage par un gabarit d'un champ carte, soit le gabarit donne des coordonnées, soit on on part sur le géocodage.

C'est le même comportement que celui adopté pour le préremplissage par un gabarit d'un champ carte, soit le gabarit donne des coordonnées, soit on on part sur le géocodage.
from wcs.wf.geolocate import GeolocateWorkflowStatusItem
geolocate = GeolocateWorkflowStatusItem()
geolocate.method = 'address_string'
geolocate.address_string = position
coords = geolocate.geolocate_address_string(None, compute_template=False)
if coords:
self.map_attributes['data-def-lat'] = '%.8f' % coords['lat']
self.map_attributes['data-def-lng'] = '%.8f' % coords['lon']
self.map_attributes['data-def-template'] = 'true'
def initial_position(self):
if self.value and ';' in self.value:
@ -3406,14 +3442,7 @@ class MapMarkerSelectionWidget(MapWidget):
self.add(HiddenWidget, 'marker_id', value=value)
self.readonly = kwargs.pop('readonly', False)
self.map_attributes = {}
self.map_attributes.update(get_publisher().get_map_attributes())
self.sync_map_and_address_fields = get_publisher().has_site_option(
'sync-map-and-address-fields', default=True
)
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
if attribute in kwargs:
self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
self.init_map_attributes(value, **kwargs)
from wcs import data_sources

View File

@ -2639,3 +2639,7 @@ span.test-failure::before {
.application-logo, .application-icon {
vertical-align: middle;
}
div[data-dynamic-display-child-of="initial_position"] {
margin-top: -1.5em;
}
Review

Dans l'admin, pour coller davantage le champ qui vient directement sous la sélection du type de position initiale.

Dans l'admin, pour coller davantage le champ qui vient directement sous la sélection du type de position initiale.

View File

@ -13,6 +13,17 @@ $(function() {
$(sel1 + sel3).removeClass('widget-hidden');
$(sel1 + sel4).removeClass('widget-hidden');
$(sel1 + sel5).removeClass('widget-hidden');
// cascade .widget-hidden to grand children
$(sel1 + '.widget-hidden[data-dynamic-display-parent]').each(function(i, elem) {
$('[data-dynamic-display-child-of="' + $(elem).attr('name') + '"]').addClass('widget-hidden');
});
$(sel1 + ':not(.widget-hidden)[data-dynamic-display-parent]').each(function(i, elem) {
if ($(elem).is('input:checked') || $(elem).is('select')) {
$(elem).trigger('change');
}
});
// refresh maps that may have been shown
$(this).parents('form').find('.qommon-map').trigger('qommon:invalidate');
Review

Pour la configuration des champs liste en mode carte, on a désormais une cascade possible, affichage carte -> le champ position initiale s'affiche -> le champ gabarit associé peut s'afficher, ou pas.

Le code ici cache tout ce qui serait dépendant du champ "position initiale" quand celui-ci est caché, et joue le trigger "change" quand il est affiché (ce qui amènera les champs dépendants à s'afficher).

Pour la configuration des champs liste en mode carte, on a désormais une cascade possible, affichage carte -> le champ position initiale s'affiche -> le champ gabarit associé peut s'afficher, ou pas. Le code ici cache tout ce qui serait dépendant du champ "position initiale" quand celui-ci est caché, et joue le trigger "change" quand il est affiché (ce qui amènera les champs dépendants à s'afficher).
});
$('[data-dynamic-display-child-of]').addClass('widget-hidden');
$('select[data-dynamic-display-parent]').trigger('change');

View File

@ -82,6 +82,8 @@ $(window).on('wcs:maps-init', function() {
});
$.getJSON($map_widget.data('markers-url')).done(
function(data) {
var checked_lat = null;
var checked_lng = null;
var geo_json = L.geoJson(data, {
pointToLayer: function (feature, latlng) {
var $label = $('<label>', {
@ -95,6 +97,8 @@ $(window).on('wcs:maps-init', function() {
'data-lng': latlng.lng
});
if (typeof initial_marker_id !== 'undefined' && feature.properties._id == initial_marker_id) {
checked_lat = latlng.lat;
checked_lng = lnglng.lng;
$radio.attr('checked', 'checked');
}
$label.append($radio);
@ -106,7 +110,14 @@ $(window).on('wcs:maps-init', function() {
return L.marker(latlng, {icon: div_marker});
}
});
map.fitBounds(geo_json.getBounds());
if (checked_lat !== null) {
map.setView([checked_lat, checked_lng], map_options.zoom);
} else if ($map_widget.data('def-template')) {
// do not adjust map to fit markers as a specific location string
// has been given.
} else {
map.fitBounds(geo_json.getBounds());
}
Review

Si la carte a une position par défaut via un gabarit, on ne fait rien (elle a été centrée dessus plus tôt), sinon s'il y a un marqueur sélectionné on se centre dessus (nouveau comportement), sinon on reste sur l'ancien comportement d'ajuster la carte pour afficher les marqueurs.

Si la carte a une position par défaut via un gabarit, on ne fait rien (elle a été centrée dessus plus tôt), sinon s'il y a un marqueur sélectionné on se centre dessus (nouveau comportement), sinon on reste sur l'ancien comportement d'ajuster la carte pour afficher les marqueurs.
geo_json.addTo(map);
}
);