forms: always use a template to render map widget (#23994)
This commit is contained in:
parent
4f138d141e
commit
ec79067e51
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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 %}
|
Loading…
Reference in New Issue