From 77d816b1dc2049fbe03e410d4c3daf6c004f1b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Tue, 22 May 2018 15:24:36 +0200 Subject: [PATCH] forms: always use a template to render map widget (#23994) --- wcs/fields.py | 2 +- wcs/qommon/form.py | 80 +++++----- wcs/qommon/template_utils.py | 139 ++++++++++++++++++ .../templates/qommon/forms/widgets/map.html | 13 ++ 4 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 wcs/qommon/template_utils.py create mode 100644 wcs/qommon/templates/qommon/forms/widgets/map.html diff --git a/wcs/fields.py b/wcs/fields.py index aa44c6e7..0387cf97 100644 --- a/wcs/fields.py +++ b/wcs/fields.py @@ -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 diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py index 124d8888..1685c176 100644 --- a/wcs/qommon/form.py +++ b/wcs/qommon/form.py @@ -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,22 +115,10 @@ def render_title(self, title): else: return '' - -def render(self): - # quixote/form/widget.py, Widget::render - def safe(text): - return mark_safe(str(htmlescape(text))) - if hasattr(self, 'add_media'): - self.add_media() - self.class_name = self.__class__.__name__ - self.rendered_title = lambda: safe(self.render_title(self.get_title())) - self.rendered_error = lambda: safe(self.render_error(self.get_error())) - 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} +def get_template_names(widget): template_names = [] - widget_template_name = getattr(self, 'template_name', None) - for extra_css_class in (getattr(self, 'extra_css_class', '') or '').split(): + 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] @@ -142,6 +131,21 @@ def render(self): 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 + def safe(text): + return mark_safe(str(htmlescape(text))) + if hasattr(self, 'add_media'): + self.add_media() + self.class_name = self.__class__.__name__ + self.rendered_title = lambda: safe(self.render_title(self.get_title())) + self.rendered_error = lambda: safe(self.render_error(self.get_error())) + 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 = get_template_names(self) return htmltext(render_template(template_names, context)) Widget.get_error = get_i18n_error @@ -2178,32 +2182,30 @@ 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'): + self.map_attributes['data-' + attribute] = kwargs.pop(attribute, None) + 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 initial_position(self): + if self.value: + return {'lat': self.value.split(';')[0], + 'lng': self.value.split(';')[1]} + return None - def render_content(self): + def add_media(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, - } - 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('
' % ' '.join(['%s="%s"' % x for x in attrs.items()])) - return r.getvalue() + + 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) diff --git a/wcs/qommon/template_utils.py b/wcs/qommon/template_utils.py new file mode 100644 index 00000000..dd870f3c --- /dev/null +++ b/wcs/qommon/template_utils.py @@ -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 . + +# This is based on https://github.com/clokep/django-render-block, +# originally Django snippet 769, then Django snippet 942. +# +# Reduced to only sipport 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) diff --git a/wcs/qommon/templates/qommon/forms/widgets/map.html b/wcs/qommon/templates/qommon/forms/widgets/map.html new file mode 100644 index 00000000..d61d1c19 --- /dev/null +++ b/wcs/qommon/templates/qommon/forms/widgets/map.html @@ -0,0 +1,13 @@ +{% extends "qommon/forms/widget.html" %} + +{% block widget-control %} + +
+{% endblock %} -- 2.17.0