Projet

Général

Profil

0001-forms-always-use-a-template-to-render-map-widget-239.patch

Frédéric Péters, 23 mai 2018 09:07

Télécharger (14,4 ko)

Voir les différences:

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
tests/test_widgets.py
43 43
    def set_form_value(self, name, value):
44 44
        self.form.set_value(value, name)
45 45

  
46
    def set_form_hidden_value(self, name, value):
47
        self.form.find_control(name).readonly = False
48
        self.form.set_value(value, name)
49

  
46 50
    def get_parsed_query(self):
47 51
        return parse_query(self.form._request_data()[1], 'utf-8')
48 52

  
49
def mock_form_submission(req, widget, html_vars={}, click=None):
53
def mock_form_submission(req, widget, html_vars={}, click=None, hidden_html_vars={}):
50 54
    form = MockHtmlForm(widget)
51 55
    for k, v in html_vars.items():
52 56
        form.set_form_value(k, v)
57
    for k, v in hidden_html_vars.items():
58
        form.set_form_hidden_value(k, v)
53 59
    if click is not None:
54 60
        request = form.form.click(click)
55 61
        req.form = parse_query(request.data, 'utf-8')
......
571 577
    assert (html_frags.index('name="test$element0key"') <  # a
572 578
            html_frags.index('name="test$element2key"') <  # b
573 579
            html_frags.index('name="test$element1key"'))   # c
580

  
581
def test_map_widget():
582
    widget = MapWidget('test', title='Map')
583
    form = MockHtmlForm(widget)
584
    assert 'name="test$latlng"' in form.as_html
585
    req.form = {}
586
    assert widget.parse() is None
587

  
588
    widget = MapWidget('test', title='Map')
589
    mock_form_submission(req, widget, hidden_html_vars={'test$latlng': '1.23;2.34'})
590
    assert not widget.has_error()
591
    assert widget.parse() == '1.23;2.34'
592

  
593
    assert '<label' in str(widget.render())
594
    assert not '<label ' in str(widget.render_widget_content())
wcs/fields.py
1978 1978

  
1979 1979
    def get_view_value(self, value):
1980 1980
        widget = self.widget_class('x%s' % random.random(), value, readonly=True)
1981
        return widget.render_content()
1981
        return widget.render_widget_content()
1982 1982

  
1983 1983
    def get_rst_view_value(self, value, indent=''):
1984 1984
        return indent + value
wcs/qommon/form.py
72 72
import misc
73 73
from .misc import strftime, C_
74 74
from publisher import get_cfg
75
from .template_utils import render_block_to_string
75 76

  
76 77
QuixoteForm = Form
77 78

  
......
114 115
    else:
115 116
        return ''
116 117

  
117

  
118
def render(self):
119
    # quixote/form/widget.py, Widget::render
120
    def safe(text):
121
        return mark_safe(str(htmlescape(text)))
122
    if hasattr(self, 'add_media'):
123
        self.add_media()
124
    self.class_name = self.__class__.__name__
125
    self.rendered_title = lambda: safe(self.render_title(self.get_title()))
126
    self.rendered_error = lambda: safe(self.render_error(self.get_error()))
127
    self.rendered_hint = lambda: safe(self.render_hint(self.get_hint()))
128
    self.rendered_message = lambda: safe(self.render_message(self.get_message()))
129
    context = {'widget': self}
118
def get_template_names(widget):
130 119
    template_names = []
131
    widget_template_name = getattr(self, 'template_name', None)
132
    for extra_css_class in (getattr(self, 'extra_css_class', '') or '').split():
120
    widget_template_name = getattr(widget, 'template_name', None)
121
    for extra_css_class in (getattr(widget, 'extra_css_class', '') or '').split():
133 122
        if not extra_css_class.startswith('template-'):
134 123
            continue
135 124
        template_name = extra_css_class.split('-', 1)[1]
......
142 131
    if widget_template_name:
143 132
        template_names.append(widget_template_name)
144 133
    template_names.append('qommon/forms/widget.html')
134
    return template_names
135

  
136
def render(self):
137
    # quixote/form/widget.py, Widget::render
138
    def safe(text):
139
        return mark_safe(str(htmlescape(text)))
140
    if hasattr(self, 'add_media'):
141
        self.add_media()
142
    self.class_name = self.__class__.__name__
143
    self.rendered_title = lambda: safe(self.render_title(self.get_title()))
144
    self.rendered_error = lambda: safe(self.render_error(self.get_error()))
145
    self.rendered_hint = lambda: safe(self.render_hint(self.get_hint()))
146
    self.rendered_message = lambda: safe(self.render_message(self.get_message()))
147
    context = {'widget': self}
148
    template_names = get_template_names(self)
145 149
    return htmltext(render_template(template_names, context))
146 150

  
147 151
Widget.get_error = get_i18n_error
......
2178 2182
        if value and get_request().form and not get_request().form.get(widget.name):
2179 2183
            get_request().form[widget.name] = value
2180 2184
        self.readonly = kwargs.pop('readonly', False)
2181
        self.kwargs = kwargs
2185
        self.map_attributes = {}
2186
        self.map_attributes.update(get_publisher().get_map_attributes())
2187
        for attribute in ('initial_zoom', 'min_zoom', 'max_zoom', 'init_with_geoloc'):
2188
            if attribute in kwargs:
2189
                self.map_attributes['data-' + attribute] = kwargs.pop(attribute)
2190
        if kwargs.get('default_position'):
2191
            self.map_attributes['data-def-lat'] = kwargs['default_position'].split(';')[0]
2192
            self.map_attributes['data-def-lng'] = kwargs['default_position'].split(';')[1]
2193

  
2194
    def initial_position(self):
2195
        if self.value:
2196
            return {'lat': self.value.split(';')[0],
2197
                    'lng': self.value.split(';')[1]}
2198
        return None
2182 2199

  
2183
    def render_content(self):
2200
    def add_media(self):
2184 2201
        get_response().add_javascript(['qommon.map.js'])
2185
        r = TemplateIO(html=True)
2186
        for widget in self.get_widgets():
2187
            r += widget.render()
2188
        attrs = {
2189
            'class': 'qommon-map',
2190
            'id': 'map-%s' % self.name,
2191
        }
2192
        if self.value:
2193
            attrs['data-init-lat'], attrs['data-init-lng'] = self.value.split(';')
2194
        if self.readonly:
2195
            attrs['data-readonly'] = 'true'
2196
        for attribute in ('initial_zoom', 'min_zoom', 'max_zoom'):
2197
            if attribute in self.kwargs and self.kwargs.get(attribute) is not None:
2198
                attrs['data-%s' % attribute] = self.kwargs.get(attribute)
2199
        attrs.update(get_publisher().get_map_attributes())
2200
        default_position = self.kwargs.get('default_position')
2201
        if default_position:
2202
            attrs['data-def-lat'], attrs['data-def-lng'] = default_position.split(';')
2203
        if self.kwargs.get('init_with_geoloc'):
2204
            attrs['data-init-with-geoloc'] = 1
2205
        r += htmltext('<div %s></div>' % ' '.join(['%s="%s"' % x for x in attrs.items()]))
2206
        return r.getvalue()
2202

  
2203
    def render_widget_content(self):
2204
        # widget content (without label, hint, etc.) is reused on status page;
2205
        # render the appropriate block.
2206
        self.add_media()
2207
        template_names = get_template_names(self)
2208
        context = {'widget': self}
2209
        return htmltext(render_block_to_string(template_names, 'widget-content', context).encode('utf-8'))
2207 2210

  
2208 2211
    def _parse(self, request):
2209 2212
        CompositeWidget._parse(self, request)
wcs/qommon/template_utils.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2018  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
# This is based on https://github.com/clokep/django-render-block,
18
# originally Django snippet 769, then Django snippet 942.
19
#
20
# Reduced to only support Django templates.
21

  
22
from __future__ import absolute_import
23

  
24
from django.template import loader, Context
25
from django.template.base import TextNode
26
from django.template.loader_tags import (
27
        BLOCK_CONTEXT_KEY, BlockContext, BlockNode, ExtendsNode)
28

  
29

  
30
class BlockNotFound(Exception):
31
    pass
32

  
33

  
34
def render_block_to_string(template_name, block_name, context=None):
35
    """
36
    Loads the given template_name and renders the given block with the given
37
    dictionary as context. Returns a string.
38

  
39
        template_name
40
            The name of the template to load and render. If it's a list of
41
            template names, Django uses select_template() instead of
42
            get_template() to find the template.
43
    """
44

  
45
    # Like render_to_string, template_name can be a string or a list/tuple.
46
    if isinstance(template_name, (tuple, list)):
47
        t = loader.select_template(template_name)
48
    else:
49
        t = loader.get_template(template_name)
50

  
51
    # Create a Django Context.
52
    context_instance = Context(context or {})
53

  
54
    # Get the underlying django.template.base.Template object.
55
    template = t.template
56

  
57
    # Bind the template to the context.
58
    with context_instance.bind_template(template):
59
        # Before trying to render the template, we need to traverse the tree of
60
        # parent templates and find all blocks in them.
61
        parent_template = _build_block_context(template, context_instance)
62

  
63
        try:
64
            return _render_template_block(template, block_name, context_instance)
65
        except BlockNotFound:
66
            # The block wasn't found in the current template.
67

  
68
            # If there's no parent template (i.e. no ExtendsNode), re-raise.
69
            if not parent_template:
70
                raise
71

  
72
            # Check the parent template for this block.
73
            return _render_template_block(
74
                parent_template, block_name, context_instance)
75

  
76

  
77
def _build_block_context(template, context):
78
    """Populate the block context with BlockNodes from parent templates."""
79

  
80
    # Ensure there's a BlockContext before rendering. This allows blocks in
81
    # ExtendsNodes to be found by sub-templates (allowing {{ block.super }} and
82
    # overriding sub-blocks to work).
83
    if BLOCK_CONTEXT_KEY not in context.render_context:
84
        context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
85
    block_context = context.render_context[BLOCK_CONTEXT_KEY]
86

  
87
    for node in template.nodelist:
88
        if isinstance(node, ExtendsNode):
89
            compiled_parent = node.get_parent(context)
90

  
91
            # Add the parent node's blocks to the context. (This ends up being
92
            # similar logic to ExtendsNode.render(), where we're adding the
93
            # parent's blocks to the context so a child can find them.)
94
            block_context.add_blocks(
95
                {n.name: n for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)})
96

  
97
            _build_block_context(compiled_parent, context)
98
            return compiled_parent
99

  
100
        # The ExtendsNode has to be the first non-text node.
101
        if not isinstance(node, TextNode):
102
            break
103

  
104

  
105
def _render_template_block(template, block_name, context):
106
    """Renders a single block from a template."""
107
    return _render_template_block_nodelist(template.nodelist, block_name, context)
108

  
109

  
110
def _render_template_block_nodelist(nodelist, block_name, context):
111
    """Recursively iterate over a node to find the wanted block."""
112

  
113
    # Attempt to find the wanted block in the current template.
114
    for node in nodelist:
115
        # If the wanted block was found, return it.
116
        if isinstance(node, BlockNode):
117
            # No matter what, add this block to the rendering context.
118
            context.render_context[BLOCK_CONTEXT_KEY].push(node.name, node)
119

  
120
            # If the name matches, you're all set and we found the block!
121
            if node.name == block_name:
122
                return node.render(context)
123

  
124
        # If a node has children, recurse into them. Based on
125
        # django.template.base.Node.get_nodes_by_type.
126
        for attr in node.child_nodelists:
127
            try:
128
                new_nodelist = getattr(node, attr)
129
            except AttributeError:
130
                continue
131

  
132
            # Try to find the block recursively.
133
            try:
134
                return _render_template_block_nodelist(new_nodelist, block_name, context)
135
            except BlockNotFound:
136
                continue
137

  
138
    # The wanted block_name was not found.
139
    raise BlockNotFound("block with name '%s' does not exist" % block_name)
wcs/qommon/templates/qommon/forms/widgets/map.html
1
{% extends "qommon/forms/widget.html" %}
2

  
3
{% block widget-control %}
4
<input type="hidden" name="{{widget.name}}$latlng" {% if widget.value %}value="{{widget.value}}"{% endif %}>
5
<div id="map-{{widget.name}}" class="qommon-map"
6
  {% if widget.readonly %}data-readonly="true"{% endif %}
7
  {% for key, value in widget.map_attributes.items %}{{key}}="{{value}}" {% endfor %}
8
  {% if widget.initial_position %}
9
    data-init-lat="{{ widget.initial_position.lat }}"
10
    data-init-lng="{{ widget.initial_position.lng }}"
11
  {% endif %}
12
></div>
13
{% endblock %}
0
-