forms: always use a template to render map widget (#23994)

This commit is contained in:
Frédéric Péters 2018-05-22 15:24:36 +02:00
parent 4f138d141e
commit ec79067e51
5 changed files with 217 additions and 41 deletions

View File

@ -43,13 +43,19 @@ class MockHtmlForm(object):
def set_form_value(self, name, value):
self.form.set_value(value, name)
def set_form_hidden_value(self, name, value):
self.form.find_control(name).readonly = False
self.form.set_value(value, name)
def get_parsed_query(self):
return parse_query(self.form._request_data()[1], 'utf-8')
def mock_form_submission(req, widget, html_vars={}, click=None):
def mock_form_submission(req, widget, html_vars={}, click=None, hidden_html_vars={}):
form = MockHtmlForm(widget)
for k, v in html_vars.items():
form.set_form_value(k, v)
for k, v in hidden_html_vars.items():
form.set_form_hidden_value(k, v)
if click is not None:
request = form.form.click(click)
req.form = parse_query(request.data, 'utf-8')
@ -571,3 +577,18 @@ def test_widgetdict_widget():
assert (html_frags.index('name="test$element0key"') < # a
html_frags.index('name="test$element2key"') < # b
html_frags.index('name="test$element1key"')) # c
def test_map_widget():
widget = MapWidget('test', title='Map')
form = MockHtmlForm(widget)
assert 'name="test$latlng"' in form.as_html
req.form = {}
assert widget.parse() is None
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 '<label' in str(widget.render())
assert not '<label ' in str(widget.render_widget_content())

View File

@ -1978,7 +1978,7 @@ class MapField(WidgetField):
def get_view_value(self, value):
widget = self.widget_class('x%s' % random.random(), value, readonly=True)
return widget.render_content()
return widget.render_widget_content()
def get_rst_view_value(self, value, indent=''):
return indent + value

View File

@ -72,6 +72,7 @@ from qommon import _, ngettext
import misc
from .misc import strftime, C_
from publisher import get_cfg
from .template_utils import render_block_to_string
QuixoteForm = Form
@ -114,6 +115,23 @@ def render_title(self, title):
else:
return ''
def get_template_names(widget):
template_names = []
widget_template_name = getattr(widget, 'template_name', None)
for extra_css_class in (getattr(widget, 'extra_css_class', '') or '').split():
if not extra_css_class.startswith('template-'):
continue
template_name = extra_css_class.split('-', 1)[1]
# full template
template_names.append('qommon/forms/widgets/%s.html' % template_name)
if widget_template_name:
# widget specific variation
template_names.append(widget_template_name.replace(
'.html', '--%s.html' % template_name))
if widget_template_name:
template_names.append(widget_template_name)
template_names.append('qommon/forms/widget.html')
return template_names
def render(self):
# quixote/form/widget.py, Widget::render
@ -127,21 +145,7 @@ def render(self):
self.rendered_hint = lambda: safe(self.render_hint(self.get_hint()))
self.rendered_message = lambda: safe(self.render_message(self.get_message()))
context = {'widget': self}
template_names = []
widget_template_name = getattr(self, 'template_name', None)
for extra_css_class in (getattr(self, 'extra_css_class', '') or '').split():
if not extra_css_class.startswith('template-'):
continue
template_name = extra_css_class.split('-', 1)[1]
# full template
template_names.append('qommon/forms/widgets/%s.html' % template_name)
if widget_template_name:
# widget specific variation
template_names.append(widget_template_name.replace(
'.html', '--%s.html' % template_name))
if widget_template_name:
template_names.append(widget_template_name)
template_names.append('qommon/forms/widget.html')
template_names = get_template_names(self)
return htmltext(render_template(template_names, context))
Widget.get_error = get_i18n_error
@ -2178,32 +2182,31 @@ class MapWidget(CompositeWidget):
if value and get_request().form and not get_request().form.get(widget.name):
get_request().form[widget.name] = value
self.readonly = kwargs.pop('readonly', False)
self.kwargs = kwargs
self.map_attributes = {}
self.map_attributes.update(get_publisher().get_map_attributes())
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
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]
def render_content(self):
get_response().add_javascript(['qommon.map.js'])
r = TemplateIO(html=True)
for widget in self.get_widgets():
r += widget.render()
attrs = {
'class': 'qommon-map',
'id': 'map-%s' % self.name,
}
def initial_position(self):
if self.value:
attrs['data-init-lat'], attrs['data-init-lng'] = self.value.split(';')
if self.readonly:
attrs['data-readonly'] = 'true'
for attribute in ('initial_zoom', 'min_zoom', 'max_zoom'):
if attribute in self.kwargs and self.kwargs.get(attribute) is not None:
attrs['data-%s' % attribute] = self.kwargs.get(attribute)
attrs.update(get_publisher().get_map_attributes())
default_position = self.kwargs.get('default_position')
if default_position:
attrs['data-def-lat'], attrs['data-def-lng'] = default_position.split(';')
if self.kwargs.get('init_with_geoloc'):
attrs['data-init-with-geoloc'] = 1
r += htmltext('<div %s></div>' % ' '.join(['%s="%s"' % x for x in attrs.items()]))
return r.getvalue()
return {'lat': self.value.split(';')[0],
'lng': self.value.split(';')[1]}
return None
def add_media(self):
get_response().add_javascript(['qommon.map.js'])
def render_widget_content(self):
# widget content (without label, hint, etc.) is reused on status page;
# render the appropriate block.
self.add_media()
template_names = get_template_names(self)
context = {'widget': self}
return htmltext(render_block_to_string(template_names, 'widget-content', context).encode('utf-8'))
def _parse(self, request):
CompositeWidget._parse(self, request)

View File

@ -0,0 +1,139 @@
# w.c.s. - web application for online forms
# Copyright (C) 2005-2018 Entr'ouvert
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
# This is based on https://github.com/clokep/django-render-block,
# originally Django snippet 769, then Django snippet 942.
#
# Reduced to only support Django templates.
from __future__ import absolute_import
from django.template import loader, Context
from django.template.base import TextNode
from django.template.loader_tags import (
BLOCK_CONTEXT_KEY, BlockContext, BlockNode, ExtendsNode)
class BlockNotFound(Exception):
pass
def render_block_to_string(template_name, block_name, context=None):
"""
Loads the given template_name and renders the given block with the given
dictionary as context. Returns a string.
template_name
The name of the template to load and render. If it's a list of
template names, Django uses select_template() instead of
get_template() to find the template.
"""
# Like render_to_string, template_name can be a string or a list/tuple.
if isinstance(template_name, (tuple, list)):
t = loader.select_template(template_name)
else:
t = loader.get_template(template_name)
# Create a Django Context.
context_instance = Context(context or {})
# Get the underlying django.template.base.Template object.
template = t.template
# Bind the template to the context.
with context_instance.bind_template(template):
# Before trying to render the template, we need to traverse the tree of
# parent templates and find all blocks in them.
parent_template = _build_block_context(template, context_instance)
try:
return _render_template_block(template, block_name, context_instance)
except BlockNotFound:
# The block wasn't found in the current template.
# If there's no parent template (i.e. no ExtendsNode), re-raise.
if not parent_template:
raise
# Check the parent template for this block.
return _render_template_block(
parent_template, block_name, context_instance)
def _build_block_context(template, context):
"""Populate the block context with BlockNodes from parent templates."""
# Ensure there's a BlockContext before rendering. This allows blocks in
# ExtendsNodes to be found by sub-templates (allowing {{ block.super }} and
# overriding sub-blocks to work).
if BLOCK_CONTEXT_KEY not in context.render_context:
context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
block_context = context.render_context[BLOCK_CONTEXT_KEY]
for node in template.nodelist:
if isinstance(node, ExtendsNode):
compiled_parent = node.get_parent(context)
# Add the parent node's blocks to the context. (This ends up being
# similar logic to ExtendsNode.render(), where we're adding the
# parent's blocks to the context so a child can find them.)
block_context.add_blocks(
{n.name: n for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)})
_build_block_context(compiled_parent, context)
return compiled_parent
# The ExtendsNode has to be the first non-text node.
if not isinstance(node, TextNode):
break
def _render_template_block(template, block_name, context):
"""Renders a single block from a template."""
return _render_template_block_nodelist(template.nodelist, block_name, context)
def _render_template_block_nodelist(nodelist, block_name, context):
"""Recursively iterate over a node to find the wanted block."""
# Attempt to find the wanted block in the current template.
for node in nodelist:
# If the wanted block was found, return it.
if isinstance(node, BlockNode):
# No matter what, add this block to the rendering context.
context.render_context[BLOCK_CONTEXT_KEY].push(node.name, node)
# If the name matches, you're all set and we found the block!
if node.name == block_name:
return node.render(context)
# If a node has children, recurse into them. Based on
# django.template.base.Node.get_nodes_by_type.
for attr in node.child_nodelists:
try:
new_nodelist = getattr(node, attr)
except AttributeError:
continue
# Try to find the block recursively.
try:
return _render_template_block_nodelist(new_nodelist, block_name, context)
except BlockNotFound:
continue
# The wanted block_name was not found.
raise BlockNotFound("block with name '%s' does not exist" % block_name)

View File

@ -0,0 +1,13 @@
{% 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.name}}" class="qommon-map"
{% if widget.readonly %}data-readonly="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 %}
></div>
{% endblock %}