From 22f6f518f9fa200e5a015ac93074b1e8bc996241 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 1/2] forms: always use a template to render map widget (#23994) --- tests/test_widgets.py | 23 ++- wcs/fields.py | 2 +- wcs/qommon/form.py | 81 +++++----- wcs/qommon/template_utils.py | 139 ++++++++++++++++++ .../templates/qommon/forms/widgets/map.html | 13 ++ 5 files changed, 217 insertions(+), 41 deletions(-) create mode 100644 wcs/qommon/template_utils.py create mode 100644 wcs/qommon/templates/qommon/forms/widgets/map.html diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 4c2a2064..90a4caa6 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -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 '' % ' '.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..81617bcc --- /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 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) 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