Пример #1
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'
    _description = 'Qweb'

    @api.model
    def _render(self, id_or_xml_id, values=None, **options):
        """ render(id_or_xml_id, values, **options)

        Render the template specified by the given name.

        :param id_or_xml_id: name or etree (see get_template)
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method
            * ``profile`` (float) profile the rendering (use astor lib) (filter
              profile line with time ms >= profile)
        """

        context = dict(self.env.context,
                       dev_mode='qweb' in tools.config['dev_mode'])
        context.update(options)

        result = super(IrQWeb, self)._render(id_or_xml_id,
                                             values=values,
                                             **context)

        if b'data-pagebreak=' not in result:
            return result

        fragments = html.fragments_fromstring(result.decode('utf-8'))

        for fragment in fragments:
            for row in fragment.iterfind('.//tr[@data-pagebreak]'):
                table = next(row.iterancestors('table'))
                newtable = html.Element('table', attrib=dict(table.attrib))
                thead = table.find('thead')
                if thead:
                    newtable.append(copy.deepcopy(thead))
                # TODO: copy caption & tfoot as well?
                # TODO: move rows in a tbody if row.getparent() is one?

                pos = row.get('data-pagebreak')
                assert pos in ('before', 'after')
                for sibling in row.getparent().iterchildren('tr'):
                    if sibling is row:
                        if pos == 'after':
                            newtable.append(sibling)
                        break
                    newtable.append(sibling)

                table.addprevious(newtable)
                table.addprevious(
                    html.Element('div',
                                 attrib={'style': 'page-break-after: always'}))

        return b''.join(html.tostring(f) for f in fragments)

    def default_values(self):
        """ attributes add to the values for each computed template
        """
        default = super(IrQWeb, self).default_values()
        default.update(
            request=request,
            cache_assets=round(time() / 180),
            true=True,
            false=False
        )  # true and false added for backward compatibility to remove after v10
        return default

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    def compile(self, id_or_xml_id, options):
        try:
            id_or_xml_id = int(id_or_xml_id)
        except:
            pass
        return super(IrQWeb, self).compile(id_or_xml_id, options=options)

    def _load(self, name, options):
        lang = options.get('lang', get_lang(self.env).code)
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        view_id = self.env['ir.ui.view'].get_view_id(name)
        template = env['ir.ui.view'].sudo()._read_template(view_id)

        # QWeb's `_read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].sudo().browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, int) or is_child_view(name):
            view = etree.fromstring(template)
            for node in view:
                if node.get('t-name'):
                    node.set('t-name', str(name))
            return view
        else:
            return template

    # order

    def _directives_eval_order(self):
        directives = super(IrQWeb, self)._directives_eval_order()
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile directives

    def _compile_directive_lang(self, el, options):
        lang = el.attrib.pop('t-lang', get_lang(self.env).code)
        if el.get('t-call-options'):
            el.set('t-call-options',
                   el.get('t-call-options')[0:-1] + u', "lang": %s}' % lang)
        else:
            el.set('t-call-options', u'{"lang": %s}' % lang)
        return self._compile_node(el, options)

    def _compile_directive_call_assets(self, el, options):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        # nodes = self._get_asset_nodes(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
        #
        # for index, (tagName, t_attrs, content) in enumerate(nodes):
        #     if index:
        #         append('\n        ')
        #     append('<')
        #     append(tagName)
        #
        #     self._post_processing_att(tagName, t_attrs, options)
        #     for name, value in t_attrs.items():
        #         if value or isinstance(value, string_types)):
        #             append(u' ')
        #             append(name)
        #             append(u'="')
        #             append(escape(pycompat.to_text((value)))
        #             append(u'"')
        #
        #     if not content and tagName in self._void_elements:
        #         append('/>')
        #     else:
        #         append('>')
        #         if content:
        #           append(content)
        #         append('</')
        #         append(tagName)
        #         append('>')
        #
        space = el.getprevious() is not None and el.getprevious(
        ).tail or el.getparent().text
        sep = u'\n' + space.rsplit('\n').pop()
        return [
            ast.Assign(
                targets=[ast.Name(id='nodes', ctx=ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(value=ast.Name(id='self',
                                                      ctx=ast.Load()),
                                       attr='_get_asset_nodes',
                                       ctx=ast.Load()),
                    args=[
                        ast.Str(el.get('t-call-assets')),
                        ast.Name(id='options', ctx=ast.Load()),
                    ],
                    keywords=[
                        ast.keyword('css',
                                    self._get_attr_bool(el.get('t-css',
                                                               True))),
                        ast.keyword('js',
                                    self._get_attr_bool(el.get('t-js', True))),
                        ast.keyword(
                            'debug',
                            ast.Call(func=ast.Attribute(value=ast.Name(
                                id='values', ctx=ast.Load()),
                                                        attr='get',
                                                        ctx=ast.Load()),
                                     args=[ast.Str('debug')],
                                     keywords=[],
                                     starargs=None,
                                     kwargs=None)),
                        ast.keyword(
                            'async_load',
                            self._get_attr_bool(el.get('async_load', False))),
                        ast.keyword(
                            'defer_load',
                            self._get_attr_bool(el.get('defer_load', False))),
                        ast.keyword(
                            'lazy_load',
                            self._get_attr_bool(el.get('lazy_load', False))),
                        ast.keyword('values',
                                    ast.Name(id='values', ctx=ast.Load())),
                    ],
                    starargs=None,
                    kwargs=None)),
            ast.For(
                target=ast.Tuple(elts=[
                    ast.Name(id='index', ctx=ast.Store()),
                    ast.Tuple(elts=[
                        ast.Name(id='tagName', ctx=ast.Store()),
                        ast.Name(id='t_attrs', ctx=ast.Store()),
                        ast.Name(id='content', ctx=ast.Store())
                    ],
                              ctx=ast.Store())
                ],
                                 ctx=ast.Store()),
                iter=ast.Call(func=ast.Name(id='enumerate', ctx=ast.Load()),
                              args=[ast.Name(id='nodes', ctx=ast.Load())],
                              keywords=[],
                              starargs=None,
                              kwargs=None),
                body=[
                    ast.If(test=ast.Name(id='index', ctx=ast.Load()),
                           body=[self._append(ast.Str(sep))],
                           orelse=[]),
                    self._append(ast.Str(u'<')),
                    self._append(ast.Name(id='tagName', ctx=ast.Load())),
                ] + self._append_attributes() + [
                    ast.If(test=ast.BoolOp(
                        op=ast.And(),
                        values=[
                            ast.UnaryOp(ast.Not(),
                                        ast.Name(id='content', ctx=ast.Load()),
                                        lineno=0,
                                        col_offset=0),
                            ast.Compare(
                                left=ast.Name(id='tagName', ctx=ast.Load()),
                                ops=[ast.In()],
                                comparators=[
                                    ast.Attribute(value=ast.Name(
                                        id='self', ctx=ast.Load()),
                                                  attr='_void_elements',
                                                  ctx=ast.Load())
                                ]),
                        ]),
                           body=[self._append(ast.Str(u'/>'))],
                           orelse=[
                               self._append(ast.Str(u'>')),
                               ast.If(test=ast.Name(id='content',
                                                    ctx=ast.Load()),
                                      body=[
                                          self._append(
                                              ast.Name(id='content',
                                                       ctx=ast.Load()))
                                      ],
                                      orelse=[]),
                               self._append(ast.Str(u'</')),
                               self._append(
                                   ast.Name(id='tagName', ctx=ast.Load())),
                               self._append(ast.Str(u'>')),
                           ])
                ],
                orelse=[])
        ]

    # method called by computing code

    def get_asset_bundle(self, xmlid, files, env=None):
        return AssetsBundle(xmlid, files, env=env)

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'async_load',
                               'defer_load',
                               'lazy_load',
                               keys=("website_id", )),
    )
    def _get_asset_nodes(self,
                         xmlid,
                         options,
                         css=True,
                         js=True,
                         debug=False,
                         async_load=False,
                         defer_load=False,
                         lazy_load=False,
                         values=None):
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, env=self.env)
        remains = [
            node for node in remains
            if (css and node[0] == 'link') or (js and node[0] != 'link')
        ]
        return remains + asset.to_node(css=css,
                                       js=js,
                                       debug=debug,
                                       async_load=async_load,
                                       defer_load=defer_load,
                                       lazy_load=lazy_load)

    def _get_asset_link_urls(self, xmlid, options):
        asset_nodes = self._get_asset_nodes(xmlid, options, js=False)
        return [node[1]['href'] for node in asset_nodes if node[0] == 'link']

    @tools.ormcache_context('xmlid',
                            'options.get("lang", "en_US")',
                            keys=("website_id", ))
    def _get_asset_content(self, xmlid, options):
        options = dict(options,
                       inherit_branding=False,
                       inherit_branding_auto=False,
                       edit_translations=False,
                       translatable=False,
                       rendering_bundle=True)

        options['website_id'] = self.env.context.get('website_id')
        IrQweb = self.env['ir.qweb'].with_context(options)

        def can_aggregate(url):
            return not urls.url_parse(url).scheme and not urls.url_parse(
                url).netloc and not url.startswith('/web/content')

        # TODO: This helper can be used by any template that wants to embedd the backend.
        #       It is currently necessary because the ir.ui.view bundle inheritance does not
        #       match the module dependency graph.
        def get_modules_order():
            if request:
                from odoo.addons.web.controllers.main import module_boot
                return json.dumps(module_boot())
            return '[]'

        template = IrQweb._render(xmlid,
                                  {"get_modules_order": get_modules_order})

        files = []
        remains = []
        for el in html.fragments_fromstring(template):
            if isinstance(el, html.HtmlElement):
                href = el.get('href', '')
                src = el.get('src', '')
                atype = el.get('type')
                media = el.get('media')

                if can_aggregate(href) and (
                        el.tag == 'style' or
                    (el.tag == 'link' and el.get('rel') == 'stylesheet')):
                    if href.endswith('.sass'):
                        atype = 'text/sass'
                    elif href.endswith('.scss'):
                        atype = 'text/scss'
                    elif href.endswith('.less'):
                        atype = 'text/less'
                    if atype not in ('text/less', 'text/scss', 'text/sass'):
                        atype = 'text/css'
                    path = [segment for segment in href.split('/') if segment]
                    filename = get_resource_path(*path) if path else None
                    files.append({
                        'atype': atype,
                        'url': href,
                        'filename': filename,
                        'content': el.text,
                        'media': media
                    })
                elif can_aggregate(src) and el.tag == 'script':
                    atype = 'text/javascript'
                    path = [segment for segment in src.split('/') if segment]
                    filename = get_resource_path(*path) if path else None
                    files.append({
                        'atype': atype,
                        'url': src,
                        'filename': filename,
                        'content': el.text,
                        'media': media
                    })
                else:
                    remains.append((el.tag, OrderedDict(el.attrib), el.text))
            else:
                # the other cases are ignored
                pass

        return (files, remains)

    def _get_field(self, record, field_name, expression, tagName,
                   field_options, options, values):
        field = record._fields[field_name]

        # adds template compile options for rendering fields
        field_options['template_options'] = options

        # adds generic field options
        field_options['tagName'] = tagName
        field_options['expression'] = expression
        field_options['type'] = field_options.get('widget', field.type)
        inherit_branding = options.get(
            'inherit_branding',
            options.get('inherit_branding_auto')
            and record.check_access_rights('write', False))
        field_options['inherit_branding'] = inherit_branding
        translate = options.get('edit_translations') and options.get(
            'translatable') and field.translate
        field_options['translate'] = translate

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.record_to_html(record, field_name, field_options)
        attributes = converter.attributes(record, field_name, field_options,
                                          values)

        return (attributes, content, inherit_branding or translate)

    def _get_widget(self, value, expression, tagName, field_options, options,
                    values):
        # adds template compile options for rendering fields
        field_options['template_options'] = options

        field_options['type'] = field_options['widget']
        field_options['tagName'] = tagName
        field_options['expression'] = expression

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.value_to_html(value, field_options)
        attributes = OrderedDict()
        attributes['data-oe-type'] = field_options['type']
        attributes['data-oe-expression'] = field_options['expression']

        return (attributes, content, None)

    # compile expression add safe_eval

    def _compile_expr(self, expr):
        """ Compiles a purported Python expression to ast, verifies that it's safe
        (according to safe_eval's semantics) and alter its variable references to
        access values data instead
        """
        # string must be stripped otherwise whitespace before the start for
        # formatting purpose are going to break parse/compile
        st = ast.parse(expr.strip(), mode='eval')
        assert_valid_codeobj(
            _SAFE_OPCODES,
            compile(st, '<>',
                    'eval'),  # could be expr, but eval *should* be fine
            expr)

        # ast.Expression().body -> expr
        return Contextifier(_BUILTINS).visit(st).body

    def _get_attr_bool(self, attr, default=False):
        if attr:
            if attr is True:
                return ast.Name(id='True', ctx=ast.Load())
            attr = attr.lower()
            if attr in ('false', '0'):
                return ast.Name(id='False', ctx=ast.Load())
            elif attr in ('true', '1'):
                return ast.Name(id='True', ctx=ast.Load())
        return ast.Name(id=str(attr if attr is False else default),
                        ctx=ast.Load())
Пример #2
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'
    _description = 'Qweb'

    _available_objects = dict(_BUILTINS)
    _empty_lines = re.compile(r'\n\s*\n')

    @QwebTracker.wrap_render
    @api.model
    def _render(self, template, values=None, **options):
        """ render(template, values, **options)

        Render the template specified by the given name.

        :param template: etree, xml_id, template name (see _get_template)
            * Call the method ``load`` is not an etree.
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method

        :returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
                  instead of `str`)
        :rtype: MarkupSafe
        """
        compile_options = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
        compile_options.update(options)

        result = super()._render(template, values=values, **compile_options)

        if not values or not values.get('__keep_empty_lines'):
            result = markupsafe.Markup(IrQWeb._empty_lines.sub('\n', result.strip()))

        if 'data-pagebreak=' not in result:
            return result

        fragments = html.fragments_fromstring(result)

        for fragment in fragments:
            for row in fragment.iterfind('.//tr[@data-pagebreak]'):
                table = next(row.iterancestors('table'))
                newtable = html.Element('table', attrib=dict(table.attrib))
                thead = table.find('thead')
                if thead:
                    newtable.append(copy.deepcopy(thead))
                # TODO: copy caption & tfoot as well?
                # TODO: move rows in a tbody if row.getparent() is one?

                pos = row.get('data-pagebreak')
                assert pos in ('before', 'after')
                for sibling in row.getparent().iterchildren('tr'):
                    if sibling is row:
                        if pos == 'after':
                            newtable.append(sibling)
                        break
                    newtable.append(sibling)

                table.addprevious(newtable)
                table.addprevious(html.Element('div', attrib={
                    'style': 'page-break-after: always'
                }))

        return markupsafe.Markup(''.join(html.tostring(f).decode() for f in fragments))

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'profile', 'raise_on_code']

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache('id_or_xml_id', 'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    @QwebTracker.wrap_compile
    def _compile(self, id_or_xml_id, options):
        try:
            id_or_xml_id = int(id_or_xml_id)
        except:
            pass
        return super()._compile(id_or_xml_id, options=options)

    def _load(self, name, options):
        lang = options.get('lang', get_lang(self.env).code)
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        view_id = self.env['ir.ui.view'].get_view_id(name)
        template = env['ir.ui.view'].sudo()._read_template(view_id)

        # QWeb's `_read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].sudo().browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, int) or is_child_view(name):
            view = etree.fromstring(template)
            for node in view:
                if node.get('t-name'):
                    node.set('t-name', str(name))
            return (view, view_id)
        else:
            return (template, view_id)

    # order

    def _directives_eval_order(self):
        directives = super()._directives_eval_order()
        directives.insert(directives.index('foreach'), 'groups')
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile

    def _compile_node(self, el, options, indent):
        if el.get("groups"):
            el.set("t-groups", el.attrib.pop("groups"))
        return super()._compile_node(el, options, indent)

    # compile directives

    @QwebTracker.wrap_compile_directive
    def _compile_directive(self, el, options, directive, indent):
        return super()._compile_directive(el, options, directive, indent)

    def _compile_directive_groups(self, el, options, indent):
        """Compile `t-groups` expressions into a python code as a list of
        strings.

        The code will contain the condition `if self.user_has_groups(groups)`
        part that wrap the rest of the compiled code of this element.
        """
        groups = el.attrib.pop('t-groups')
        code = self._flushText(options, indent)
        code.append(self._indent(f"if self.user_has_groups({repr(groups)}):", indent))
        code.extend(self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1) or [self._indent('pass', indent + 1)])
        return code

    def _compile_directive_lang(self, el, options, indent):
        el.attrib['t-options-lang'] = el.attrib.pop('t-lang')
        return self._compile_node(el, options, indent)

    def _compile_directive_call_assets(self, el, options, indent):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        code = self._flushText(options, indent)
        code.append(self._indent(dedent("""
            t_call_assets_nodes = self._get_asset_nodes(%(xmlid)s, css=%(css)s, js=%(js)s, debug=values.get("debug"), async_load=%(async_load)s, defer_load=%(defer_load)s, lazy_load=%(lazy_load)s, media=%(media)s)
            for index, (tagName, attrs, content) in enumerate(t_call_assets_nodes):
                if index:
                    yield '\\n        '
                yield '<'
                yield tagName
            """).strip() % {
                'xmlid': repr(el.get('t-call-assets')),
                'css': self._compile_bool(el.get('t-css', True)),
                'js': self._compile_bool(el.get('t-js', True)),
                'async_load': self._compile_bool(el.get('async_load', False)),
                'defer_load': self._compile_bool(el.get('defer_load', False)),
                'lazy_load': self._compile_bool(el.get('lazy_load', False)),
                'media': repr(el.get('media')) if el.get('media') else False,
            }, indent))
        code.extend(self._compile_attributes(options, indent + 1))
        code.append(self._indent(dedent("""
                if not content and tagName in self._void_elements:
                    yield '/>'
                else:
                    yield '>'
                    if content:
                      yield content
                    yield '</'
                    yield tagName
                    yield '>'
                """).strip(), indent + 1))

        return code

    # method called by computing code

    def get_asset_bundle(self, bundle_name, files, env=None, css=True, js=True):
        return AssetsBundle(bundle_name, files, env=env, css=css, js=js)

    def _get_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
        """Generates asset nodes.
        If debug=assets, the assets will be regenerated when a file which composes them has been modified.
        Else, the assets will be generated only once and then stored in cache.
        """
        if debug and 'assets' in debug:
            return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
        else:
            return self._generate_asset_nodes_cache(bundle, css, js, debug, async_load, defer_load, lazy_load, media)

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('bundle', 'css', 'js', 'debug', 'async_load', 'defer_load', 'lazy_load', keys=("website_id", "lang")),
    )
    def _generate_asset_nodes_cache(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
        return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)

    def _generate_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
        nodeAttrs = None
        if css and media:
            nodeAttrs = {
                'media': media,
            }
        files, remains = self._get_asset_content(bundle, nodeAttrs, defer_load=defer_load, lazy_load=lazy_load)
        asset = self.get_asset_bundle(bundle, files, env=self.env, css=css, js=js)
        remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] == 'script')]
        return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load, defer_load=defer_load, lazy_load=lazy_load)

    def _get_asset_link_urls(self, bundle):
        asset_nodes = self._get_asset_nodes(bundle, js=False)
        return [node[1]['href'] for node in asset_nodes if node[0] == 'link']

    @tools.ormcache_context('bundle', 'nodeAttrs and nodeAttrs.get("media")', 'defer_load', 'lazy_load', keys=("website_id", "lang"))
    def _get_asset_content(self, bundle, nodeAttrs=None, defer_load=False, lazy_load=False):
        asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle, css=True, js=True)

        files = []
        remains = []
        for path, *_ in asset_paths:
            ext = path.split('.')[-1]
            is_js = ext in SCRIPT_EXTENSIONS
            is_css = ext in STYLE_EXTENSIONS
            if not is_js and not is_css:
                continue

            mimetype = 'text/javascript' if is_js else 'text/%s' % ext
            if can_aggregate(path):
                segments = [segment for segment in path.split('/') if segment]
                files.append({
                    'atype': mimetype,
                    'url': path,
                    'filename': get_resource_path(*segments) if segments else None,
                    'content': '',
                    'media': nodeAttrs and nodeAttrs.get('media'),
                })
            else:
                if is_js:
                    tag = 'script'
                    attributes = {
                        "type": mimetype,
                    }
                    attributes["data-src" if lazy_load else "src"] = path
                    if defer_load or lazy_load:
                        attributes["defer"] = "defer"
                else:
                    tag = 'link'
                    attributes = {
                        "type": mimetype,
                        "rel": "stylesheet",
                        "href": path,
                        'media': nodeAttrs and nodeAttrs.get('media'),
                    }
                remains.append((tag, attributes, ''))

        return (files, remains)

    def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
        field = record._fields[field_name]

        # adds template compile options for rendering fields
        field_options['template_options'] = options

        # adds generic field options
        field_options['tagName'] = tagName
        field_options['expression'] = expression
        field_options['type'] = field_options.get('widget', field.type)
        inherit_branding = options.get('inherit_branding', options.get('inherit_branding_auto') and record.check_access_rights('write', False))
        field_options['inherit_branding'] = inherit_branding
        translate = options.get('edit_translations') and options.get('translatable') and field.translate
        field_options['translate'] = translate

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env['ir.qweb.field']

        # get content (the return values from fields are considered to be markup safe)
        content = converter.record_to_html(record, field_name, field_options)
        attributes = converter.attributes(record, field_name, field_options, values)

        return (attributes, content, inherit_branding or translate)

    def _get_widget(self, value, expression, tagName, field_options, options, values):
        # adds template compile options for rendering fields
        field_options['template_options'] = options

        field_options['type'] = field_options['widget']
        field_options['tagName'] = tagName
        field_options['expression'] = expression

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env['ir.qweb.field']

        # get content (the return values from widget are considered to be markup safe)
        content = converter.value_to_html(value, field_options)
        attributes = {}
        attributes['data-oe-type'] = field_options['type']
        attributes['data-oe-expression'] = field_options['expression']

        return (attributes, content, None)

    def _prepare_values(self, values, options):
        """ Prepare the context that will be sent to the evaluated function.

        :param values: template values to be used for rendering
        :param options: frozen dict of compilation parameters.
        """
        check_values(values)
        values['true'] = True
        values['false'] = False
        if 'request' not in values:
            values['request'] = request
        return super()._prepare_values(values, options)

    def _compile_expr(self, expr, raise_on_missing=False):
        """ Compiles a purported Python expression to compiled code, verifies
        that it's safe (according to safe_eval's semantics) and alter its
        variable references to access values data instead

        :param expr: string
        """
        namespace_expr = super()._compile_expr(expr, raise_on_missing=raise_on_missing)
        assert_valid_codeobj(_SAFE_QWEB_OPCODES, compile(namespace_expr, '<>', 'eval'), expr)
        return namespace_expr
Пример #3
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'

    @api.model
    def render(self, id_or_xml_id, values=None, **options):
        """ render(id_or_xml_id, values, **options)

        Render the template specified by the given name.

        :param id_or_xml_id: name or etree (see get_template)
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method
            * ``profile`` (float) profile the rendering (use astor lib) (filter
              profile line with time ms >= profile)
        """
        for method in dir(self):
            if method.startswith('render_'):
                _logger.warning("Unused method '%s' is found in ir.qweb." %
                                method)

        context = dict(self.env.context,
                       dev_mode='qweb' in tools.config['dev_mode'])
        context.update(options)

        return super(IrQWeb, self).render(id_or_xml_id,
                                          values=values,
                                          **context)

    def default_values(self):
        """ attributes add to the values for each computed template
        """
        default = super(IrQWeb, self).default_values()
        default.update(
            request=request,
            cache_assets=round(time() / 180),
            true=True,
            false=False
        )  # true and false added for backward compatibility to remove after v10
        return default

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    def compile(self, id_or_xml_id, options):
        return super(IrQWeb, self).compile(id_or_xml_id, options=options)

    def load(self, name, options):
        lang = options.get('lang', 'en_US')
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        template = env['ir.ui.view'].read_template(name)

        # QWeb's `read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, pycompat.integer_types) or is_child_view(name):
            for node in etree.fromstring(template):
                if node.get('t-name'):
                    node.set('t-name', str(name))
                    return node.getparent()
            return None  # trigger "template not found" in QWeb
        else:
            return template

    # order

    def _directives_eval_order(self):
        directives = super(IrQWeb, self)._directives_eval_order()
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile directives

    def _compile_directive_lang(self, el, options):
        lang = el.attrib.pop('t-lang', 'en_US')
        if el.get('t-call-options'):
            el.set('t-call-options',
                   el.get('t-call-options')[0:-1] + u', "lang": %s}' % lang)
        else:
            el.set('t-call-options', u'{"lang": %s}' % lang)
        return self._compile_node(el, options)

    def _compile_directive_call_assets(self, el, options):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        # self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
        return [
            self._append(
                ast.Call(
                    func=ast.Attribute(value=ast.Name(id='self',
                                                      ctx=ast.Load()),
                                       attr='_get_asset',
                                       ctx=ast.Load()),
                    args=[
                        ast.Str(el.get('t-call-assets')),
                        ast.Name(id='options', ctx=ast.Load()),
                    ],
                    keywords=[
                        ast.keyword('css',
                                    self._get_attr_bool(el.get('t-css',
                                                               True))),
                        ast.keyword('js',
                                    self._get_attr_bool(el.get('t-js', True))),
                        ast.keyword(
                            'debug',
                            ast.Call(func=ast.Attribute(value=ast.Name(
                                id='values', ctx=ast.Load()),
                                                        attr='get',
                                                        ctx=ast.Load()),
                                     args=[ast.Str('debug')],
                                     keywords=[],
                                     starargs=None,
                                     kwargs=None)),
                        ast.keyword(
                            'async',
                            self._get_attr_bool(el.get('async', False))),
                        ast.keyword('values',
                                    ast.Name(id='values', ctx=ast.Load())),
                    ],
                    starargs=None,
                    kwargs=None))
        ]

    # for backward compatibility to remove after v10
    def _compile_widget_options(self, el, directive_type):
        field_options = super(IrQWeb, self)._compile_widget_options(
            el, directive_type)

        if ('t-%s-options' % directive_type) in el.attrib:
            if tools.config['dev_mode']:
                _logger.warning(
                    "Use new syntax t-options instead of t-%s-options" %
                    directive_type)
            if not field_options:
                field_options = el.attrib.pop('t-%s-options' % directive_type)

        if field_options and 'monetary' in field_options:
            try:
                options = "{'widget': 'monetary'"
                for k, v in json.loads(field_options).items():
                    if k in ('display_currency', 'from_currency'):
                        options = "%s, '%s': %s" % (options, k, v)
                    else:
                        options = "%s, '%s': '%s'" % (options, k, v)
                options = "%s}" % options
                field_options = options
                _logger.warning(
                    "Use new syntax for '%s' monetary widget t-options (python dict instead of deprecated JSON syntax)."
                    % etree.tostring(el))
            except ValueError:
                pass

        return field_options

    # end backward

    # method called by computing code

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'async',
                               keys=("website_id", )),
    )
    def _get_asset(self,
                   xmlid,
                   options,
                   css=True,
                   js=True,
                   debug=False,
                   async=False,
                   values=None):
Пример #4
0
        asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
        return asset.to_html(css=css,
                             js=js,
                             debug=debug,
                             async=async,
                             url_for=(values
                                      or {}).get('url_for', lambda url: url))

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'async',
                               keys=("website_id", )),
    )
    def _get_asset_nodes(self,
                         xmlid,
                         options,
                         css=True,
                         js=True,
                         debug=False,
                         async=False,
                         values=None):
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, env=self.env)
        remains = [
Пример #5
0
# Monkey Patch to change the ormcache_context decorator of '_get_asset_nodes' to
# add 'active_company_id' context key. This is done to avoid "clear_caches" usage
# that works in a more aggressive way to the LRU cache.

_orig_get_asset_nodes = unwrap(IrQWeb._get_asset_nodes)


@tools.conditional(
    "xml" not in tools.config["dev_mode"],
    tools.ormcache_context(
        "xmlid",
        'options.get("lang", "en_US")',
        "css",
        "js",
        "debug",
        "async_load",
        "defer_load",
        "lazy_load",
        keys=("website_id", "active_company_id"),
    ),
)
def _get_asset_nodes__mp(
    self,
    xmlid,
    options,
    css=True,
    js=True,
    debug=False,
    async_load=False,
    defer_load=False,
Пример #6
0
    # compatibility to remove after v11 - DEPRECATED
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
    )
    def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
        return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url))

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
    )
    def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, env=self.env)
        remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')]
        return remains + asset.to_node(css=css, js=js, debug=debug, async=async)

    @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
    def _get_asset_content(self, xmlid, options):
        options = dict(options,
            inherit_branding=False, inherit_branding_auto=False,
            edit_translations=False, translatable=False,
            rendering_bundle=True)

        env = self.env(context=options)