2013-08-10 17:01:10 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2013-12-17 11:20:34 +00:00
|
|
|
import copy
|
2013-08-10 17:01:10 +00:00
|
|
|
|
2013-09-06 13:09:16 +00:00
|
|
|
from lxml import etree, html
|
2013-08-10 17:01:10 +00:00
|
|
|
|
2014-12-23 10:38:18 +00:00
|
|
|
from openerp import SUPERUSER_ID, api
|
2014-02-06 14:39:19 +00:00
|
|
|
from openerp.addons.website.models import website
|
|
|
|
from openerp.http import request
|
2013-10-22 13:06:08 +00:00
|
|
|
from openerp.osv import osv, fields
|
[FIX] website, base: escaping and unescaping html
When saving a template in version 8.0, html would be saved as it should
be displayed once on the site. In particular, if some text should be
escaped once send to the browser, it will be saved as such.
But when rendering, a text node content is unescaped two times:
* for translation which seems wrong since we already use .text of a node
which already escaped it, doing it one more time is bad,
* when rendering the template, since the html template is stored in xml,
This commit remove superfluous unescaping for translation, and add an
escaping when saving the changed template content.
closes #7967
opw-646889
2015-08-10 07:20:45 +00:00
|
|
|
from openerp.tools import html_escape
|
2013-08-10 17:01:10 +00:00
|
|
|
|
|
|
|
class view(osv.osv):
|
|
|
|
_inherit = "ir.ui.view"
|
|
|
|
_columns = {
|
2013-09-06 09:13:51 +00:00
|
|
|
'page': fields.boolean("Whether this view is a web page template (complete)"),
|
2013-10-14 13:56:47 +00:00
|
|
|
'website_meta_title': fields.char("Website meta title", size=70, translate=True),
|
2013-10-16 16:02:32 +00:00
|
|
|
'website_meta_description': fields.text("Website meta description", size=160, translate=True),
|
|
|
|
'website_meta_keywords': fields.char("Website meta keywords", translate=True),
|
2014-08-31 14:56:44 +00:00
|
|
|
'customize_show': fields.boolean("Show As Optional Inherit"),
|
2013-09-06 09:13:51 +00:00
|
|
|
}
|
|
|
|
_defaults = {
|
|
|
|
'page': False,
|
2014-08-31 14:56:44 +00:00
|
|
|
'customize_show': False,
|
2013-08-10 17:01:10 +00:00
|
|
|
}
|
|
|
|
|
2014-05-27 09:31:44 +00:00
|
|
|
|
|
|
|
def _view_obj(self, cr, uid, view_id, context=None):
|
|
|
|
if isinstance(view_id, basestring):
|
|
|
|
return self.pool['ir.model.data'].xmlid_to_object(
|
|
|
|
cr, uid, view_id, raise_if_not_found=True, context=context
|
|
|
|
)
|
|
|
|
elif isinstance(view_id, (int, long)):
|
|
|
|
return self.browse(cr, uid, view_id, context=context)
|
|
|
|
|
|
|
|
# assume it's already a view object (WTF?)
|
|
|
|
return view_id
|
|
|
|
|
2013-08-10 17:01:10 +00:00
|
|
|
# Returns all views (called and inherited) related to a view
|
|
|
|
# Used by translation mechanism, SEO and optional templates
|
2014-05-27 09:31:44 +00:00
|
|
|
def _views_get(self, cr, uid, view_id, options=True, context=None, root=True):
|
2014-05-27 09:33:37 +00:00
|
|
|
""" For a given view ``view_id``, should return:
|
|
|
|
|
|
|
|
* the view itself
|
|
|
|
* all views inheriting from it, enabled or not
|
2014-05-27 09:34:49 +00:00
|
|
|
- but not the optional children of a non-enabled child
|
2014-05-27 09:33:37 +00:00
|
|
|
* all views called from it (via t-call)
|
|
|
|
"""
|
2014-01-27 11:40:34 +00:00
|
|
|
try:
|
2014-05-27 09:31:44 +00:00
|
|
|
view = self._view_obj(cr, uid, view_id, context=context)
|
2014-01-27 11:40:34 +00:00
|
|
|
except ValueError:
|
|
|
|
# Shall we log that ?
|
|
|
|
return []
|
2013-08-14 10:18:57 +00:00
|
|
|
|
2013-08-10 17:01:10 +00:00
|
|
|
while root and view.inherit_id:
|
|
|
|
view = view.inherit_id
|
|
|
|
|
|
|
|
result = [view]
|
2014-01-30 19:42:27 +00:00
|
|
|
|
|
|
|
node = etree.fromstring(view.arch)
|
|
|
|
for child in node.xpath("//t[@t-call]"):
|
|
|
|
try:
|
2014-05-27 09:33:37 +00:00
|
|
|
called_view = self._view_obj(cr, uid, child.get('t-call'), context=context)
|
2014-01-30 19:42:27 +00:00
|
|
|
except ValueError:
|
|
|
|
continue
|
2014-05-27 09:33:37 +00:00
|
|
|
if called_view not in result:
|
|
|
|
result += self._views_get(cr, uid, called_view, options=options, context=context)
|
2014-01-30 19:42:27 +00:00
|
|
|
|
2014-05-27 09:54:01 +00:00
|
|
|
extensions = view.inherit_children_ids
|
|
|
|
if not options:
|
|
|
|
# only active children
|
2014-08-31 14:56:44 +00:00
|
|
|
extensions = (v for v in view.inherit_children_ids if v.active)
|
2014-05-27 09:33:37 +00:00
|
|
|
|
|
|
|
# Keep options in a deterministic order regardless of their applicability
|
2014-05-27 09:34:49 +00:00
|
|
|
for extension in sorted(extensions, key=lambda v: v.id):
|
2014-05-27 09:33:37 +00:00
|
|
|
for r in self._views_get(
|
2014-05-27 09:34:49 +00:00
|
|
|
cr, uid, extension,
|
2014-05-27 09:33:37 +00:00
|
|
|
# only return optional grandchildren if this child is enabled
|
2014-08-31 14:56:44 +00:00
|
|
|
options=extension.active,
|
2014-05-27 09:33:37 +00:00
|
|
|
context=context, root=False):
|
2013-11-16 08:53:50 +00:00
|
|
|
if r not in result:
|
|
|
|
result.append(r)
|
2013-08-10 17:01:10 +00:00
|
|
|
return result
|
2013-09-06 13:09:16 +00:00
|
|
|
|
2013-09-09 09:51:12 +00:00
|
|
|
def extract_embedded_fields(self, cr, uid, arch, context=None):
|
|
|
|
return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
|
|
|
|
|
|
|
|
def save_embedded_field(self, cr, uid, el, context=None):
|
2013-09-18 11:51:36 +00:00
|
|
|
Model = self.pool[el.get('data-oe-model')]
|
|
|
|
field = el.get('data-oe-field')
|
|
|
|
|
[IMP] use model._fields instead of model._all_columns to cover all fields
The old-api model._all_columns contains information about model._columns and
inherited columns. This dictionary is missing new-api computed non-stored
fields, and the new field objects provide a more readable api...
This commit contains the following changes:
- adapt several methods of BaseModel to use fields instead of columns and
_all_columns
- copy all semantic-free attributes of related fields from their source
- add attribute 'group_operator' on integer and float fields
- base, base_action_rule, crm, edi, hr, mail, mass_mailing, pad,
payment_acquirer, share, website, website_crm, website_mail: simply use
_fields instead of _all_columns
- base, decimal_precision, website: adapt qweb rendering methods to use fields
instead of columns
2014-11-03 15:00:50 +00:00
|
|
|
converter = self.pool['website.qweb'].get_converter_for(el.get('data-oe-type'))
|
|
|
|
value = converter.from_html(cr, uid, Model, Model._fields[field], el)
|
2013-09-30 14:53:58 +00:00
|
|
|
|
2013-12-18 14:09:17 +00:00
|
|
|
if value is not None:
|
|
|
|
# TODO: batch writes?
|
|
|
|
Model.write(cr, uid, [int(el.get('data-oe-id'))], {
|
|
|
|
field: value
|
|
|
|
}, context=context)
|
2013-09-09 09:51:12 +00:00
|
|
|
|
|
|
|
def to_field_ref(self, cr, uid, el, context=None):
|
2013-09-19 09:25:46 +00:00
|
|
|
# filter out meta-information inserted in the document
|
|
|
|
attributes = dict((k, v) for k, v in el.items()
|
|
|
|
if not k.startswith('data-oe-'))
|
|
|
|
attributes['t-field'] = el.get('data-oe-expression')
|
|
|
|
|
|
|
|
out = html.html_parser.makeelement(el.tag, attrib=attributes)
|
2013-09-10 14:34:06 +00:00
|
|
|
out.tail = el.tail
|
|
|
|
return out
|
2013-09-09 09:51:12 +00:00
|
|
|
|
|
|
|
def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None):
|
2013-12-17 11:20:34 +00:00
|
|
|
# the root of the arch section shouldn't actually be replaced as it's
|
|
|
|
# not really editable itself, only the content truly is editable.
|
2013-10-22 13:06:08 +00:00
|
|
|
|
2013-12-17 11:20:34 +00:00
|
|
|
[view] = self.browse(cr, uid, [view_id], context=context)
|
|
|
|
arch = etree.fromstring(view.arch.encode('utf-8'))
|
|
|
|
# => get the replacement root
|
2013-10-22 13:06:08 +00:00
|
|
|
if not section_xpath:
|
2013-12-17 11:20:34 +00:00
|
|
|
root = arch
|
2013-10-22 13:06:08 +00:00
|
|
|
else:
|
2013-09-09 09:51:12 +00:00
|
|
|
# ensure there's only one match
|
2013-12-17 11:20:34 +00:00
|
|
|
[root] = arch.xpath(section_xpath)
|
|
|
|
|
[FIX] website, base: escaping and unescaping html
When saving a template in version 8.0, html would be saved as it should
be displayed once on the site. In particular, if some text should be
escaped once send to the browser, it will be saved as such.
But when rendering, a text node content is unescaped two times:
* for translation which seems wrong since we already use .text of a node
which already escaped it, doing it one more time is bad,
* when rendering the template, since the html template is stored in xml,
This commit remove superfluous unescaping for translation, and add an
escaping when saving the changed template content.
closes #7967
opw-646889
2015-08-10 07:20:45 +00:00
|
|
|
# html text need to be escaped for xml storage
|
|
|
|
def escape_node(node):
|
|
|
|
node.text = node.text and html_escape(node.text)
|
|
|
|
node.tail = node.tail and html_escape(node.tail)
|
|
|
|
escape_node(replacement)
|
|
|
|
for descendant in replacement.iterdescendants():
|
|
|
|
escape_node(descendant)
|
|
|
|
|
2013-12-17 11:20:34 +00:00
|
|
|
root.text = replacement.text
|
|
|
|
root.tail = replacement.tail
|
|
|
|
# replace all children
|
|
|
|
del root[:]
|
|
|
|
for child in replacement:
|
|
|
|
root.append(copy.deepcopy(child))
|
2013-10-22 13:06:08 +00:00
|
|
|
|
2013-09-09 09:51:12 +00:00
|
|
|
return arch
|
|
|
|
|
2014-12-23 10:38:18 +00:00
|
|
|
@api.cr_uid_ids_context
|
2014-02-06 14:39:19 +00:00
|
|
|
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
|
2014-04-02 16:13:49 +00:00
|
|
|
if request and getattr(request, 'website_enabled', False):
|
2014-02-06 14:39:19 +00:00
|
|
|
engine='website.qweb'
|
|
|
|
|
|
|
|
if isinstance(id_or_xml_id, list):
|
|
|
|
id_or_xml_id = id_or_xml_id[0]
|
|
|
|
|
|
|
|
if not context:
|
|
|
|
context = {}
|
|
|
|
|
2014-08-04 19:08:18 +00:00
|
|
|
company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, context=context)
|
|
|
|
|
2014-05-05 16:38:41 +00:00
|
|
|
qcontext = dict(
|
2014-02-10 14:35:46 +00:00
|
|
|
context.copy(),
|
2014-02-06 14:39:19 +00:00
|
|
|
website=request.website,
|
|
|
|
url_for=website.url_for,
|
|
|
|
slug=website.slug,
|
2014-08-04 19:08:18 +00:00
|
|
|
res_company=company,
|
2014-02-06 14:39:19 +00:00
|
|
|
user_id=self.pool.get("res.users").browse(cr, uid, uid),
|
2014-04-22 13:47:48 +00:00
|
|
|
translatable=context.get('lang') != request.website.default_lang_code,
|
2014-05-05 16:38:41 +00:00
|
|
|
editable=request.website.is_publisher(),
|
2014-06-30 17:45:12 +00:00
|
|
|
menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None,
|
2014-02-06 14:39:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
# add some values
|
|
|
|
if values:
|
|
|
|
qcontext.update(values)
|
|
|
|
|
|
|
|
# in edit mode ir.ui.view will tag nodes
|
2014-09-25 13:32:25 +00:00
|
|
|
if qcontext.get('editable'):
|
|
|
|
context = dict(context, inherit_branding=True)
|
|
|
|
elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'):
|
|
|
|
context = dict(context, inherit_branding_auto=True)
|
2014-02-06 14:39:19 +00:00
|
|
|
|
|
|
|
view_obj = request.website.get_template(id_or_xml_id)
|
|
|
|
if 'main_object' not in qcontext:
|
|
|
|
qcontext['main_object'] = view_obj
|
|
|
|
|
|
|
|
values = qcontext
|
|
|
|
|
|
|
|
return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context)
|
|
|
|
|
2014-02-27 12:59:34 +00:00
|
|
|
def _pretty_arch(self, arch):
|
|
|
|
# remove_blank_string does not seem to work on HTMLParser, and
|
|
|
|
# pretty-printing with lxml more or less requires stripping
|
|
|
|
# whitespace: http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output
|
|
|
|
# so serialize to XML, parse as XML (remove whitespace) then serialize
|
|
|
|
# as XML (pretty print)
|
|
|
|
arch_no_whitespace = etree.fromstring(
|
|
|
|
etree.tostring(arch, encoding='utf-8'),
|
|
|
|
parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True))
|
2014-03-21 07:54:19 +00:00
|
|
|
return etree.tostring(
|
2014-02-27 12:59:34 +00:00
|
|
|
arch_no_whitespace, encoding='unicode', pretty_print=True)
|
|
|
|
|
2013-09-09 09:51:12 +00:00
|
|
|
def save(self, cr, uid, res_id, value, xpath=None, context=None):
|
|
|
|
""" Update a view section. The view section may embed fields to write
|
2013-09-06 13:09:16 +00:00
|
|
|
|
|
|
|
:param str model:
|
|
|
|
:param int res_id:
|
|
|
|
:param str xpath: valid xpath to the tag to replace
|
|
|
|
"""
|
2013-09-09 09:51:12 +00:00
|
|
|
res_id = int(res_id)
|
|
|
|
|
2013-09-30 14:53:58 +00:00
|
|
|
arch_section = html.fromstring(
|
|
|
|
value, parser=html.HTMLParser(encoding='utf-8'))
|
2013-09-09 09:51:12 +00:00
|
|
|
|
2013-09-17 08:57:53 +00:00
|
|
|
if xpath is None:
|
|
|
|
# value is an embedded field on its own, not a view section
|
|
|
|
self.save_embedded_field(cr, uid, arch_section, context=context)
|
|
|
|
return
|
|
|
|
|
2013-09-09 09:51:12 +00:00
|
|
|
for el in self.extract_embedded_fields(cr, uid, arch_section, context=context):
|
|
|
|
self.save_embedded_field(cr, uid, el, context=context)
|
|
|
|
|
|
|
|
# transform embedded field back to t-field
|
|
|
|
el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context))
|
|
|
|
|
|
|
|
arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context)
|
|
|
|
self.write(cr, uid, res_id, {
|
2014-02-27 12:59:34 +00:00
|
|
|
'arch': self._pretty_arch(arch)
|
2013-09-09 09:51:12 +00:00
|
|
|
}, context=context)
|
2014-05-30 14:00:28 +00:00
|
|
|
|
|
|
|
view = self.browse(cr, SUPERUSER_ID, res_id, context=context)
|
|
|
|
if view.model_data_id:
|
|
|
|
view.model_data_id.write({'noupdate': True})
|
2014-08-31 14:56:44 +00:00
|
|
|
|
2015-02-13 12:30:53 +00:00
|
|
|
def customize_template_get(self, cr, uid, xml_id, full=False, bundles=False , context=None):
|
|
|
|
""" Get inherit view's informations of the template ``key``. By default, only
|
|
|
|
returns ``customize_show`` templates (which can be active or not), if
|
|
|
|
``full=True`` returns inherit view's informations of the template ``key``.
|
|
|
|
``bundles=True`` returns also the asset bundles
|
|
|
|
"""
|
|
|
|
imd = request.registry['ir.model.data']
|
|
|
|
view_model, view_theme_id = imd.get_object_reference(cr, uid, 'website', 'theme')
|
|
|
|
user = request.registry['res.users'].browse(cr, uid, uid, context)
|
|
|
|
user_groups = set(user.groups_id)
|
|
|
|
views = self._views_get(cr, uid, xml_id, context=dict(context or {}, active_test=False))
|
|
|
|
done = set()
|
|
|
|
result = []
|
|
|
|
for v in views:
|
|
|
|
if not user_groups.issuperset(v.groups_id):
|
|
|
|
continue
|
|
|
|
if full or (v.customize_show and v.inherit_id.id != view_theme_id):
|
|
|
|
if v.inherit_id not in done:
|
|
|
|
result.append({
|
|
|
|
'name': v.inherit_id.name,
|
|
|
|
'id': v.id,
|
|
|
|
'xml_id': v.xml_id,
|
|
|
|
'inherit_id': v.inherit_id.id,
|
|
|
|
'header': True,
|
|
|
|
'active': False
|
|
|
|
})
|
|
|
|
done.add(v.inherit_id)
|
|
|
|
result.append({
|
|
|
|
'name': v.name,
|
|
|
|
'id': v.id,
|
|
|
|
'xml_id': v.xml_id,
|
|
|
|
'inherit_id': v.inherit_id.id,
|
|
|
|
'header': False,
|
|
|
|
'active': v.active,
|
|
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
|
|
def get_view_translations(self, cr, uid, xml_id, lang, field=['id', 'res_id', 'value', 'state', 'gengo_translation'], context=None):
|
|
|
|
views = self.customize_template_get(cr, uid, xml_id, full=True, context=context)
|
|
|
|
views_ids = [view.get('id') for view in views if view.get('active')]
|
|
|
|
domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
|
|
|
|
irt = request.registry.get('ir.translation')
|
|
|
|
return irt.search_read(cr, uid, domain, field, context=context)
|
|
|
|
|