position de carte selon gabarit (#66959) #253
|
@ -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 = [
|
||||
|
|
|
@ -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'
|
||||
|
|
103
wcs/fields.py
|
@ -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',
|
||||
|
||||
]
|
||||
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'),
|
||||
),
|
||||
fpeters
commented
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',
|
||||
]
|
||||
fpeters
commented
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
|
||||
fpeters
commented
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'),
|
||||
),
|
||||
fpeters
commented
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):
|
||||
|
|
|
@ -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?
|
||||
fpeters
commented
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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
fpeters
commented
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.
|
||||
|
|
|
@ -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');
|
||||
fpeters
commented
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');
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
fpeters
commented
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);
|
||||
}
|
||||
);
|
||||
|
|
Avec le passage en plusieurs lignes on ne le voit pas bien, c'est l'ajout de initial_position et position_template.