From 400ffb7f07c512a983859dadc7fea00f0ec1be5c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 2 Oct 2013 16:45:54 +0200 Subject: [PATCH 01/68] [FIX] disable relative positioning of cke widget wrapper It's only used to be able to position the drag & drop widget absolutely, and we've already disabled that one. Product page is a gory mess of absolute positioning, if product images are changed into a t-field the relative positioning on the wrapper makes the image itself disappear completely. bzr revid: xmo@openerp.com-20131002144554-1rg4zmwu0btf0r0f --- addons/website/static/src/css/editor.css | 4 ++++ addons/website/static/src/css/editor.sass | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 51ed40bcf42..b77e900d9f2 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -154,6 +154,10 @@ table.editorbar-panel td.selected { margin-bottom: 0.5em; } +.cke_widget_wrapper { + position: static !important; +} + .cke_widget_drag_handler_container { display: none !important; } diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index e74c81932a4..4f962f02151 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -129,6 +129,10 @@ table.editorbar-panel .modal .image-preview margin-bottom: 0.5em +// wrapper positioned relatively for drag&drop widget which is disabled below. +// Breaks completely horribly crazy products listing page, so take it out. +.cke_widget_wrapper + position: static !important // "remove" drag & drop of CKE widgets .cke_widget_drag_handler_container display: none !important From 6ed1edf538e94c17d60bab4d443cd8f2feb00e02 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 4 Oct 2013 11:17:31 +0200 Subject: [PATCH 02/68] [IMP] turns out PIL provides its own mime mapping... bzr revid: xmo@openerp.com-20131004091731-8r235491f3h2af2q --- addons/website/controllers/main.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index f7854a90794..10f0a130697 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -34,8 +34,6 @@ def auth_method_public(): http.auth_methods['public'] = auth_method_public NOPE = object() -# PIL images have a type flag, but no MIME. Reverse type flag to MIME. -PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif', } # Completely arbitrary limits MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768) class Website(openerp.addons.web.controllers.main.Home): @@ -327,7 +325,7 @@ class Images(http.Controller): # FIXME: unknown format or not an image image = Image.open(buf) - response.mimetype = PIL_MIME_MAPPING[image.format] + response.mimetype = Image.MIME[image.format] w, h = image.size max_w, max_h = fit From e826d0e7f8c5899be157a288c79f729da026d5b2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 4 Oct 2013 15:46:37 +0200 Subject: [PATCH 03/68] [IMP] openerp-ified qweb bzr revid: xmo@openerp.com-20131004134637-l2mpxsjwgu4pff2z --- addons/website/tests/test_converter.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index 7f042988916..4c819abd037 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -3,13 +3,10 @@ from collections import namedtuple from functools import partial from xml.dom.minidom import getDOMImplementation -from lxml import etree, html +from lxml import html from openerp.tests import common -from openerp.tools import qweb -import unittest2 -renderer = qweb.QWebXml() impl = getDOMImplementation() document = impl.createDocument(None, None, None) @@ -39,7 +36,7 @@ class TestConvertBack(common.TransactionCase): field_value = 'record.%s' % field e.setAttribute('t-field', field_value) - rendered = renderer.render_tag_field( + rendered = self.registry('ir.templating.qweb').render_tag_field( e, {'field': field_value}, '', { 'record': record, 'request': Request(self.cr, self.uid, RegistryProxy( self.registry)), From 3d5857d504291fe83cba74dad2e00dd5a7c94773 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 7 Oct 2013 11:24:05 +0200 Subject: [PATCH 04/68] [FIX] qweb oe object renamed in registry bzr revid: xmo@openerp.com-20131007092405-2ffrntwlzup6lvu1 --- addons/website/tests/test_converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index 4c819abd037..bd7d44906bd 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -36,10 +36,9 @@ class TestConvertBack(common.TransactionCase): field_value = 'record.%s' % field e.setAttribute('t-field', field_value) - rendered = self.registry('ir.templating.qweb').render_tag_field( + rendered = self.registry('ir.qweb').render_tag_field( e, {'field': field_value}, '', { 'record': record, - 'request': Request(self.cr, self.uid, RegistryProxy( self.registry)), }) element = html.fromstring( rendered, parser=html.HTMLParser(encoding='utf-8')) From 510a4f51d2a5b75f7b4c3b8b345f693eb044b337 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 8 Oct 2013 11:22:33 +0200 Subject: [PATCH 05/68] [ADD] customizable qweb rendering, add translate flag on fields through this bzr revid: xmo@openerp.com-20131008092233-9su66923zthl9jyg --- addons/website/models/__init__.py | 1 + addons/website/models/ir_qweb.py | 62 +++++++++++++++++++++ addons/website/models/website.py | 5 +- addons/website/security/ir.model.access.csv | 13 ++++- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 addons/website/models/ir_qweb.py diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 4977118e925..5fcab989d6c 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -3,3 +3,4 @@ import ir_fields import view import website +import ir_qweb diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py new file mode 100644 index 00000000000..eeaee4a94e0 --- /dev/null +++ b/addons/website/models/ir_qweb.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +import itertools + +from openerp.osv import orm, fields + +class QWeb(orm.AbstractModel): + """ QWeb object for rendering stuff in the website context + + Website-context rendering needs to add some metadata to rendered fields, + as well as render a few fields differently. + + Also, add methods to convert values back to openerp models. + """ + _name = 'website.qweb' + _inherit = 'ir.qweb' + + def get_converter_for(self, field_type): + return self.pool.get( + 'website.qweb.field.' + field_type, + self.pool['website.qweb.field']) + +class Field(orm.AbstractModel): + _name = 'website.qweb.field' + _inherit = 'ir.qweb.field' + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context): + column = record._model._all_columns[field_name].column + return itertools.chain( + super(Field, self).attributes(cr, uid, field_name, record, options, + source_element, g_att, t_att, + qweb_context), + [('data-oe-translate', 1 if column.translate else 0)] + ) + +class Float(orm.AbstractModel): + _name = 'website.qweb.field.float' + _inherit = ['website.qweb.field', 'ir.qweb.field.float'] + +class Text(orm.AbstractModel): + _name = 'website.qweb.field.text' + _inherit = ['website.qweb.field', 'ir.qweb.field.text'] + +class Selection(orm.AbstractModel): + _name = 'website.qweb.field.selection' + _inherit = ['website.qweb.field', 'ir.qweb.field.selection'] + +class ManyToOne(orm.AbstractModel): + _name = 'website.qweb.field.many2one' + _inherit = ['website.qweb.field', 'ir.qweb.field.many2one'] + +class HTML(orm.AbstractModel): + _name = 'website.qweb.field.html' + _inherit = ['website.qweb.field', 'ir.qweb.field.html'] + +class Image(orm.AbstractModel): + _name = 'website.qweb.field.image' + _inherit = ['website.qweb.field', 'ir.qweb.field.image'] + +class Currency(orm.AbstractModel): + _name = 'website.qweb.field.currency' + _inherit = ['website.qweb.field', 'ir.qweb.field.currency'] diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 90957b584a4..c36ed829699 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -168,8 +168,9 @@ class website(osv.osv): return self.render(cr, uid, ids, 'website.404', qweb_context) try: - return view.render(cr, uid, "%s.%s" % (module, xmlid), - qweb_context, context=context) + return view.render( + cr, uid, "%s.%s" % (module, xmlid), qweb_context, + engine='website.qweb', context=context) except (AccessError, AccessDenied), err: logger.error(err) qweb_context['error'] = err[1] diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index d2c3d4ee534..e9679c50ba9 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -1,3 +1,14 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_website_public,website,website.model_website,base.group_public,1,0,0,0 -access_website,website,website.model_website,base.group_user,1,0,0,0 \ No newline at end of file +access_website,website,website.model_website,base.group_user,1,0,0,0 +access_website_converter_test,access_website_converter_test,model_website_converter_test,,1,1,1,1 +access_website_converter_test_sub,access_website_converter_test_sub,model_website_converter_test_sub,,1,1,1,1 +access_website_qweb,access_website_qweb,model_website_qweb,,0,0,0,0 +access_website_qweb_field,access_website_qweb_field,model_website_qweb_field,,0,0,0,0 +access_website_qweb_field_float,access_website_qweb_field_float,model_website_qweb_field_float,,0,0,0,0 +access_website_qweb_field_text,access_website_qweb_field_text,model_website_qweb_field_text,,0,0,0,0 +access_website_qweb_field_selection,access_website_qweb_field_selection,model_website_qweb_field_selection,,0,0,0,0 +access_website_qweb_field_many2one,access_website_qweb_field_many2one,model_website_qweb_field_many2one,,0,0,0,0 +access_website_qweb_field_html,access_website_qweb_field_html,model_website_qweb_field_html,,0,0,0,0 +access_website_qweb_field_image,access_website_qweb_field_image,model_website_qweb_field_image,,0,0,0,0 +access_website_qweb_field_currency,access_website_qweb_field_currency,model_website_qweb_field_currency,,0,0,0,0 From d8c837ef5b469dc94786d455ab9939dfb32fbe88 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 8 Oct 2013 11:26:12 +0200 Subject: [PATCH 06/68] [IMP] promote part of the website.qweb docstring to module-level bzr revid: xmo@openerp.com-20131008092612-oqaesax6qjl6bse0 --- addons/website/models/ir_qweb.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index eeaee4a94e0..fcfd3861c2c 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- +""" +Website-context rendering needs to add some metadata to rendered fields, +as well as render a few fields differently. + +Also, adds methods to convert values back to openerp models. +""" + import itertools from openerp.osv import orm, fields class QWeb(orm.AbstractModel): """ QWeb object for rendering stuff in the website context - - Website-context rendering needs to add some metadata to rendered fields, - as well as render a few fields differently. - - Also, add methods to convert values back to openerp models. """ _name = 'website.qweb' _inherit = 'ir.qweb' From 426757ad920fe7736f7ce23e02bbeb17c846e79b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 8 Oct 2013 14:04:56 +0200 Subject: [PATCH 07/68] [ADD] linked image fields bzr revid: xmo@openerp.com-20131008120456-r8tqd5iyq1zfsdvi --- addons/website/models/ir_qweb.py | 27 ++++++++++++++++++++++ addons/website_sale/views/website_sale.xml | 17 +++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index fcfd3861c2c..e2472df9bb3 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -8,6 +8,8 @@ Also, adds methods to convert values back to openerp models. import itertools +import werkzeug.utils + from openerp.osv import orm, fields class QWeb(orm.AbstractModel): @@ -56,9 +58,34 @@ class HTML(orm.AbstractModel): _inherit = ['website.qweb.field', 'ir.qweb.field.html'] class Image(orm.AbstractModel): + """ + Widget options: + + ``class`` + set as attribute on the generated tag + """ _name = 'website.qweb.field.image' _inherit = ['website.qweb.field', 'ir.qweb.field.image'] + def to_html(self, cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context): + assert source_element.nodeName != 'img',\ + "Oddly enough, the root tag of an image field can not be img. " \ + "That is because the image goes into the tag, or it gets the " \ + "hose again." + + return super(Image, self).to_html( + cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context) + + def record_to_html(self, cr, uid, field_name, record, column, options=None): + cls = '' + if 'class' in options: + cls = ' class="%s"' % werkzeug.utils.escape(options['class']) + + return '' % ( + cls, record._model._name, field_name, record.id) + class Currency(orm.AbstractModel): _name = 'website.qweb.field.currency' _inherit = ['website.qweb.field', 'ir.qweb.field.currency'] diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index 1c096ecd3de..03b786ece08 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -124,7 +124,7 @@ @@ -328,7 +328,7 @@
- +

Product Name

@@ -381,7 +381,7 @@
- +
@@ -441,7 +441,8 @@ - +
@@ -560,7 +561,8 @@ - + @@ -796,7 +798,10 @@ - + + + From a3f7cfd66973a57785daa29fe2b3983ce908cb1a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 8 Oct 2013 14:20:39 +0200 Subject: [PATCH 08/68] [FIX] renaming of currency widget to monetary to match view bzr revid: xmo@openerp.com-20131008122039-tzw7nq8cx0y0zh5p --- addons/website/models/ir_qweb.py | 6 +++--- addons/website/security/ir.model.access.csv | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index e2472df9bb3..ec60ce9d0e2 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -86,6 +86,6 @@ class Image(orm.AbstractModel): return '' % ( cls, record._model._name, field_name, record.id) -class Currency(orm.AbstractModel): - _name = 'website.qweb.field.currency' - _inherit = ['website.qweb.field', 'ir.qweb.field.currency'] +class Monetary(orm.AbstractModel): + _name = 'website.qweb.field.monetary' + _inherit = ['website.qweb.field', 'ir.qweb.field.monetary'] diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index e9679c50ba9..4fe9a0888b4 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -11,4 +11,4 @@ access_website_qweb_field_selection,access_website_qweb_field_selection,model_we access_website_qweb_field_many2one,access_website_qweb_field_many2one,model_website_qweb_field_many2one,,0,0,0,0 access_website_qweb_field_html,access_website_qweb_field_html,model_website_qweb_field_html,,0,0,0,0 access_website_qweb_field_image,access_website_qweb_field_image,model_website_qweb_field_image,,0,0,0,0 -access_website_qweb_field_currency,access_website_qweb_field_currency,model_website_qweb_field_currency,,0,0,0,0 +access_website_qweb_field_monetary,access_website_qweb_field_monetary,model_website_qweb_field_monetary,,0,0,0,0 From eaef36c91046d48f766986bbf86461be90207b0c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 9 Oct 2013 15:31:12 +0200 Subject: [PATCH 09/68] [FIX] move conversion code from ir.fields.converter into website.qweb structures bzr revid: xmo@openerp.com-20131009133112-05dglhptiw019838 --- addons/website/models/__init__.py | 3 +- addons/website/models/ir_fields.py | 87 -------------------------- addons/website/models/ir_qweb.py | 47 ++++++++++++++ addons/website/models/test_models.py | 39 ++++++++++++ addons/website/models/view.py | 8 +-- addons/website/tests/test_converter.py | 21 ++----- addons/website/tests/test_views.py | 27 ++++---- 7 files changed, 111 insertions(+), 121 deletions(-) delete mode 100644 addons/website/models/ir_fields.py create mode 100644 addons/website/models/test_models.py diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 5fcab989d6c..af592ba012c 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -import ir_fields import view import website import ir_qweb + +import test_models diff --git a/addons/website/models/ir_fields.py b/addons/website/models/ir_fields.py deleted file mode 100644 index fbccf9c296d..00000000000 --- a/addons/website/models/ir_fields.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -from lxml import etree, html - -from openerp.osv import orm, fields -from openerp.tools import ustr - -class converter(orm.Model): - _inherit = 'ir.fields.converter' - - def _html_to_integer(self, cr, uid, model, column, value, context=None): - return int(value.text_content().strip()), [] - - def _html_to_float(self, cr, uid, model, column, value, context=None): - return float(value.text_content().strip()), [] - - def _html_to_passthrough(self, cr, uid, model, column, value, context=None): - return value.text_content().strip(), [] - - _html_to_char = _html_to_date = _html_to_datetime = _html_to_passthrough - - def _html_to_text(self, cr, uid, model, column, value, context=None): - return value.text_content(), [] - - def _html_to_selection(self, cr, uid, model, column, value, context=None): - text = value.text_content().strip() - - selection = column.reify(cr, uid, model, column, context=context) - for k, v in selection: - if isinstance(v, str): - v = ustr(v) - if text == v: - return k, [] - - warning = u"No value found for label %s in selection %s" % (text, selection) - # FIXME: ? - return False, [Warning(warning.encode('utf-8'))] - - def _html_to_many2one(self, cr, uid, model, column, value, context=None): - matches = self.pool[column._obj].name_search( - cr, uid, name=value.text_content().strip(), context=context) - # FIXME: more than one match, error reporting - return matches[0][0], [] - - def _html_to_html(self, cr, uid, model, column, value, context=None): - content = [] - if value.text: content.append(value.text) - content.extend(html.tostring(child) - for child in value.iterchildren(tag=etree.Element)) - return '\n'.join(content), [] - - -class test_converter(orm.Model): - _name = 'website.converter.test' - - _columns = { - 'char': fields.char(), - 'integer': fields.integer(), - 'float': fields.float(), - 'numeric': fields.float(digits=(16, 2)), - 'many2one': fields.many2one('website.converter.test.sub'), - 'binary': fields.binary(), - 'date': fields.date(), - 'datetime': fields.datetime(), - 'selection': fields.selection([ - (1, "réponse A"), - (2, "réponse B"), - (3, "réponse C"), - (4, "réponse D"), - ]), - 'selection_str': fields.selection([ - ('A', "Qu'il n'est pas arrivé à Toronto"), - ('B', "Qu'il était supposé arriver à Toronto"), - ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), - ('D', "La réponse D"), - ], string="Lorsqu'un pancake prend l'avion à destination de Toronto et " - "qu'il fait une escale technique à St Claude, on dit:"), - 'html': fields.html(), - 'text': fields.text(), - } - - -class test_converter_sub(orm.Model): - _name = 'website.converter.test.sub' - - _columns = { - 'name': fields.char(), - } diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index ec60ce9d0e2..dd6a8327433 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -9,8 +9,10 @@ Also, adds methods to convert values back to openerp models. import itertools import werkzeug.utils +from lxml import etree, html from openerp.osv import orm, fields +from openerp.tools import ustr class QWeb(orm.AbstractModel): """ QWeb object for rendering stuff in the website context @@ -37,26 +39,71 @@ class Field(orm.AbstractModel): [('data-oe-translate', 1 if column.translate else 0)] ) + def value_from_string(self, value): + return value + + def from_html(self, cr, uid, model, column, element, context=None): + return self.value_from_string(element.text_content().strip()) + +class Integer(orm.AbstractModel): + _name = 'website.qweb.field.integer' + _inherit = ['website.qweb.field'] + + value_from_string = int + class Float(orm.AbstractModel): _name = 'website.qweb.field.float' _inherit = ['website.qweb.field', 'ir.qweb.field.float'] + value_from_string = float + class Text(orm.AbstractModel): _name = 'website.qweb.field.text' _inherit = ['website.qweb.field', 'ir.qweb.field.text'] + def from_html(self, cr, uid, model, column, element, context=None): + return element.text_content() + class Selection(orm.AbstractModel): _name = 'website.qweb.field.selection' _inherit = ['website.qweb.field', 'ir.qweb.field.selection'] + def from_html(self, cr, uid, model, column, element, context=None): + value = element.text_content().strip() + selection = column.reify(cr, uid, model, column, context=context) + for k, v in selection: + if isinstance(v, str): + v = ustr(v) + if value == v: + return k + + raise ValueError(u"No value found for label %s in selection %s" % ( + value, selection)) + class ManyToOne(orm.AbstractModel): _name = 'website.qweb.field.many2one' _inherit = ['website.qweb.field', 'ir.qweb.field.many2one'] + def from_html(self, cr, uid, model, column, element, context=None): + # FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget? + matches = self.pool[column._obj].name_search( + cr, uid, name=element.text_content().strip(), context=context) + # FIXME: no match? More than 1 match? + assert len(matches) == 1 + return matches[0][0] + class HTML(orm.AbstractModel): _name = 'website.qweb.field.html' _inherit = ['website.qweb.field', 'ir.qweb.field.html'] + def from_html(self, cr, uid, model, column, element, context=None): + content = [] + if element.text: content.append(element.text) + content.extend(html.tostring(child) + for child in element.iterchildren(tag=etree.Element)) + return '\n'.join(content) + + class Image(orm.AbstractModel): """ Widget options: diff --git a/addons/website/models/test_models.py b/addons/website/models/test_models.py new file mode 100644 index 00000000000..d028cdd9864 --- /dev/null +++ b/addons/website/models/test_models.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from openerp.osv import orm, fields + +class test_converter(orm.Model): + _name = 'website.converter.test' + + _columns = { + 'char': fields.char(), + 'integer': fields.integer(), + 'float': fields.float(), + 'numeric': fields.float(digits=(16, 2)), + 'many2one': fields.many2one('website.converter.test.sub'), + 'binary': fields.binary(), + 'date': fields.date(), + 'datetime': fields.datetime(), + 'selection': fields.selection([ + (1, "réponse A"), + (2, "réponse B"), + (3, "réponse C"), + (4, "réponse D"), + ]), + 'selection_str': fields.selection([ + ('A', "Qu'il n'est pas arrivé à Toronto"), + ('B', "Qu'il était supposé arriver à Toronto"), + ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), + ('D', "La réponse D"), + ], string="Lorsqu'un pancake prend l'avion à destination de Toronto et " + "qu'il fait une escale technique à St Claude, on dit:"), + 'html': fields.html(), + 'text': fields.text(), + } + + +class test_converter_sub(orm.Model): + _name = 'website.converter.test.sub' + + _columns = { + 'name': fields.char(), + } diff --git a/addons/website/models/view.py b/addons/website/models/view.py index 6ddae129185..aaf25511fde 100644 --- a/addons/website/models/view.py +++ b/addons/website/models/view.py @@ -60,11 +60,9 @@ class view(osv.osv): field = el.get('data-oe-field') column = Model._all_columns[field].column - convert = self.pool['ir.fields.converter'].to_field( - cr, uid, Model, column, fromtype="html", context=context) - value, warnings = convert(el) - # FIXME: report error - if warnings: return + converter = self.pool['website.qweb'].get_converter_for( + el.get('data-oe-type')) + value = converter.from_html(cr, uid, Model, column, el) Model.write(cr, uid, [int(el.get('data-oe-id'))], { field: value diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index bd7d44906bd..b2f2b690f98 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from collections import namedtuple from functools import partial from xml.dom.minidom import getDOMImplementation @@ -10,19 +9,10 @@ from openerp.tests import common impl = getDOMImplementation() document = impl.createDocument(None, None, None) -Request = namedtuple('Request', 'cr uid registry') -class RegistryProxy(object): - def __init__(self, func): - self.func = func - def __getitem__(self, name): - return self.func(name) - class TestConvertBack(common.TransactionCase): def setUp(self): super(TestConvertBack, self).setUp() - self.Converter = self.registry('ir.fields.converter') - def field_rountrip_result(self, field, value, expected): model = 'website.converter.test' Model = self.registry(model) @@ -36,7 +26,7 @@ class TestConvertBack(common.TransactionCase): field_value = 'record.%s' % field e.setAttribute('t-field', field_value) - rendered = self.registry('ir.qweb').render_tag_field( + rendered = self.registry('website.qweb').render_tag_field( e, {'field': field_value}, '', { 'record': record, }) @@ -44,12 +34,11 @@ class TestConvertBack(common.TransactionCase): rendered, parser=html.HTMLParser(encoding='utf-8')) column = Model._all_columns[field].column + converter = self.registry('website.qweb').get_converter_for( + element.get('data-oe-type')) - from_html = self.Converter.to_field( - self.cr, self.uid, model, column, 'html') - - value_back, warnings = from_html(element) - self.assertEqual(warnings, []) + value_back = converter.from_html( + self.cr, self.uid, model, column, element) if isinstance(expected, str): expected = expected.decode('utf-8') diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 8fc112899f6..812981cd706 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -31,8 +31,8 @@ class TestViewSaving(common.TransactionCase): h.H3("Column 2"), h.UL( h.LI("Item 1"), - h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), - h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) + h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name', type='char'))), + h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone', type='char'))) )) ) self.view_id = self.registry('ir.ui.view').create(self.cr, self.uid, { @@ -46,14 +46,15 @@ class TestViewSaving(common.TransactionCase): self.cr, self.uid, self.arch, context=None) expect = [ - h.SPAN("My Company", attrs(model='res.company', id=1, field='name')), - h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')), + h.SPAN("My Company", attrs(model='res.company', id=1, field='name', type='char')), + h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone', type='char')), ] for actual, expected in itertools.izip_longest(fields, expect): self.eq(actual, expected) def test_embedded_save(self): - embedded = h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')) + embedded = h.SPAN("+00 00 000 00 0 000", attrs( + model='res.company', id=1, field='phone', type='char')) self.registry('ir.ui.view').save_embedded_field(self.cr, self.uid, embedded) @@ -118,8 +119,8 @@ class TestViewSaving(common.TransactionCase): h.H3("Column 2"), h.UL( h.LI("Item 1"), - h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), - h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) + h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name', type='char'))), + h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone', type='char'))) )) )) @@ -137,8 +138,8 @@ class TestViewSaving(common.TransactionCase): h.H3("Column 2"), h.UL( h.LI("wob wob wob"), - h.LI(h.SPAN("Acme Corporation", attrs(model='res.company', id=1, field='name', expression="bob"))), - h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund"))), + h.LI(h.SPAN("Acme Corporation", attrs(model='res.company', id=1, field='name', expression="bob", type='char'))), + h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund", type='char'))), ) ), encoding='utf-8') View.save(self.cr, self.uid, res_id=self.view_id, value=replacement, @@ -173,7 +174,7 @@ class TestViewSaving(common.TransactionCase): node = html.tostring(h.SPAN( "Acme Corporation", - attrs(model='res.company', id=company_id, field="name", expression='bob'))) + attrs(model='res.company', id=company_id, field="name", expression='bob', type='char'))) self.registry('ir.ui.view').save(self.cr, self.uid, res_id=company_id,value=node) @@ -184,7 +185,9 @@ class TestViewSaving(common.TransactionCase): def test_field_tail(self): View = self.registry('ir.ui.view') replacement = ET.tostring( - h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone', expression="edmund")), + h.LI(h.SPAN("+12 3456789", attrs( + model='res.company', id=1, type='char', + field='phone', expression="edmund")), "whop whop" ), encoding="utf-8") View.save(self.cr, self.uid, res_id = self.view_id, value=replacement, @@ -203,7 +206,7 @@ class TestViewSaving(common.TransactionCase): h.H3("Column 2"), h.UL( h.LI("Item 1"), - h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), + h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name', type='char'))), h.LI(h.SPAN({'t-field': "edmund"}), "whop whop"), )) ) From 448c9077f53ed855c965bf3f91ae6de16902c43e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 9 Oct 2013 17:40:46 +0200 Subject: [PATCH 10/68] [FIX] correctly handle empty image fields (False value) as well as missing fields, instead of yielding a 500 error bzr revid: xmo@openerp.com-20131009154046-vdb4cldz215s4lyp --- addons/website/controllers/main.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index d66bc13ab75..70a7e75afa4 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -261,6 +261,15 @@ class Website(openerp.addons.web.controllers.main.Home): return request.website.render('website.sitemap', {'pages': request.website.list_pages()}) class Images(http.Controller): + def placeholder(self, response): + # file_open may return a StringIO. StringIO can be closed but are + # not context managers in Python 2 though that is fixed in 3 + with contextlib.closing(openerp.tools.misc.file_open( + os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), + mode='rb')) as f: + response.set_data(f.read()) + return response.make_conditional(request.httprequest) + @website.route('/website/image', auth="public") def image(self, model, id, field): Model = request.registry[model] @@ -275,13 +284,7 @@ class Images(http.Controller): [('id', '=', id), ('website_published', '=', True)], context=request.context) if not ids: - # file_open may return a StringIO. StringIO can be closed but are - # not context managers in Python 2 though that is fixed in 3 - with contextlib.closing(openerp.tools.misc.file_open( - os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), - mode='rb')) as f: - response.set_data(f.read()) - return response + return self.placeholder(response) concurrency = '__last_update' [record] = Model.read(request.cr, openerp.SUPERUSER_ID, [id], @@ -296,7 +299,12 @@ class Images(http.Controller): # just in case we have a timestamp without microseconds response.last_modified = datetime.datetime.strptime( record[concurrency], server_format) - # FIXME: no field in record? + + if not record.get(field): + # Field does not exist on model or field set to False + # FIXME: maybe a field which does not exist should be a 404? + return self.placeholder(response) + response.set_etag(hashlib.sha1(record[field]).hexdigest()) response.make_conditional(request.httprequest) From cb9f597d509e611852bf45db22369aea0cf28caa Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 10 Oct 2013 10:56:03 +0200 Subject: [PATCH 11/68] [FIX] http://dev.ckeditor.com/ticket/10912, necessary for image widget edition bzr revid: xmo@openerp.com-20131010085603-dfpxbdqwf4m75osb --- addons/website/static/lib/ckeditor/core/editable.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/website/static/lib/ckeditor/core/editable.js b/addons/website/static/lib/ckeditor/core/editable.js index a7bfb72531c..7a46406da48 100644 --- a/addons/website/static/lib/ckeditor/core/editable.js +++ b/addons/website/static/lib/ckeditor/core/editable.js @@ -580,8 +580,10 @@ // Prevent the browser opening read-only links. (#6032) this.attachListener( this, 'click', function( ev ) { ev = ev.data; - var target = ev.getTarget(); - if ( target.is( 'a' ) && ev.$.button != 2 && target.isReadOnly() ) + var target = ev.getTarget(), + link = new CKEDITOR.dom.elementPath( target ).contains( 'a' ); + + if ( link && ev.$.button != 2 && link.isReadOnly() ) ev.preventDefault(); } ); From 9cdbe94ce5006e5a2184941d8d31c1e808a02bcf Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 10 Oct 2013 12:30:05 +0200 Subject: [PATCH 12/68] [ADD] saving of image fields bzr revid: xmo@openerp.com-20131010103005-mp0afaps3ssqacm8 --- addons/website/models/ir_qweb.py | 37 +++++++++++++++++++ .../website/static/src/js/website.editor.js | 1 + 2 files changed, 38 insertions(+) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index dd6a8327433..f2d6c33eeb7 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -6,14 +6,21 @@ as well as render a few fields differently. Also, adds methods to convert values back to openerp models. """ +import cStringIO import itertools +import logging +import re +import urllib2 import werkzeug.utils from lxml import etree, html +from PIL import Image as I from openerp.osv import orm, fields from openerp.tools import ustr +logger = logging.getLogger(__name__) + class QWeb(orm.AbstractModel): """ QWeb object for rendering stuff in the website context """ @@ -133,6 +140,36 @@ class Image(orm.AbstractModel): return '' % ( cls, record._model._name, field_name, record.id) + def from_html(self, cr, uid, model, column, element, context=None): + url = element.find('img').get('src') + + m = re.match(r'^/website/attachment/(\d+)$', url) + if m: + attachment = self.pool['ir.attachment'].browse( + cr, uid, int(m.group(1)), context=context) + return attachment.datas + + # remote URL? + try: + # FIXME: what are the security issues to blindly opening a URL provided by a user? + # * infinite redirect + # * infinite (or huge) amount of data + # * very... slow... remote... + req = urllib2.urlopen(url) + # PIL needs a seekable file-like image, urlinfo is not seekable + image = I.open(cStringIO.StringIO(req.read())) + # force a complete load of the image data to validate it + image.load() + except Exception: + logger.exception("Failed to load remote image %r", url) + return False + + # don't use original data in case weird stuff was smuggled in, with + # luck PIL will remove some of it? + out = cStringIO.StringIO() + image.save(out, image.format) + return out.getvalue().encode('base64') + class Monetary(orm.AbstractModel): _name = 'website.qweb.field.monetary' _inherit = ['website.qweb.field', 'ir.qweb.field.monetary'] diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index f40ccea715b..217d7fd24ee 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -812,6 +812,7 @@ var style = data.style; element.setAttribute('src', data.url); + element.removeAttribute('data-cke-saved-src'); $(element.$).removeClass(this.image_styles.join(' ')); if (style) { element.addClass(style); } }, From c6c0609577363e4ce7698381d474a220d482f04f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 10 Oct 2013 14:26:52 +0200 Subject: [PATCH 13/68] [IMP] add notes and timeout to urlopen call bzr revid: xmo@openerp.com-20131010122652-ukjof14x2d0y3h89 --- addons/website/models/ir_qweb.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index f2d6c33eeb7..6d46c0d4473 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -19,6 +19,8 @@ from PIL import Image as I from openerp.osv import orm, fields from openerp.tools import ustr +REMOTE_CONNECTION_TIMEOUT = 2.5 + logger = logging.getLogger(__name__) class QWeb(orm.AbstractModel): @@ -151,12 +153,15 @@ class Image(orm.AbstractModel): # remote URL? try: - # FIXME: what are the security issues to blindly opening a URL provided by a user? - # * infinite redirect - # * infinite (or huge) amount of data - # * very... slow... remote... - req = urllib2.urlopen(url) - # PIL needs a seekable file-like image, urlinfo is not seekable + # should probably remove remote URLs entirely: + # * in fields, downloading them without blowing up the server is a + # challenge + # * in views, may trigger mixed content warnings if HTTPS CMS + # linking to HTTP images + # implement drag & drop image upload to mitigate? + + req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT) + # PIL needs a seekable file-like image, urllib result is not seekable image = I.open(cStringIO.StringIO(req.read())) # force a complete load of the image data to validate it image.load() From 9b40e849d6debcfb9fbca2ded70f0ed7cae2c878 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 10 Oct 2013 15:29:07 +0200 Subject: [PATCH 14/68] [WIP] new title managment in templates. expose main_object for seo bzr revid: fme@openerp.com-20131010132907-xxmg96qmcw9j5f2d --- addons/website/controllers/main.py | 7 +++ addons/website/models/view.py | 2 + addons/website/static/src/js/website.js | 10 ++-- addons/website/static/src/js/website.seo.js | 50 ++++++++++++++++++- addons/website/views/views.xml | 14 +++--- .../views/website_blog_templates.xml | 2 +- addons/website_crm/views/website_crm.xml | 4 +- .../views/website_crm_partner_assign.xml | 2 +- .../views/website_customer.xml | 2 +- addons/website_event/views/website_event.xml | 10 +--- .../views/website_hr_recruitment.xml | 11 ++-- .../views/website_membership.xml | 2 +- .../website_project/views/website_project.xml | 4 +- addons/website_sale/controllers/main.py | 1 + addons/website_sale/views/website_sale.xml | 10 ++-- 15 files changed, 91 insertions(+), 40 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 50163902b7a..869d51d85c7 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -122,6 +122,13 @@ class Website(openerp.addons.web.controllers.main.Home): values = { 'path': path, } + try: + module, xmlid = path.split('.', 1) + IMD = request.registry.get("ir.model.data") + obj = IMD.get_object_reference(request.cr, request.uid, module, xmlid) + values['main_object'] = request.registry[obj[0]].browse(request.cr, request.uid, obj[1]) + except Exception: + pass try: html = request.website.render(path, values) except ValueError: diff --git a/addons/website/models/view.py b/addons/website/models/view.py index 6ddae129185..b146b405757 100644 --- a/addons/website/models/view.py +++ b/addons/website/models/view.py @@ -10,6 +10,8 @@ class view(osv.osv): 'inherit_option_id': fields.many2one('ir.ui.view','Optional Inheritancy'), 'inherited_option_ids': fields.one2many('ir.ui.view','inherit_option_id','Optional Inheritancies'), 'page': fields.boolean("Whether this view is a web page template (complete)"), + 'website_description': fields.char("Website Description", translate=True), + 'website_keywords': fields.char("Website Keywords", translate=True), } _defaults = { 'page': False, diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index e524edfb389..1f4e54b43df 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -109,9 +109,13 @@ website.ready = function() { if (!all_ready) { var tpl = website.load_templates(templates); - // var session; - // var trads = openerp._t.database.load_translations(session, ['website'], website.get_context().lang); - all_ready = $.when(dom_ready, tpl); + all_ready = dom_ready.then(function () { + if ($('html').data('editable')) { + website.session = new openerp.Session(); + var modules = ['website']; + return openerp._t.database.load_translations(website.session, modules, website.get_context().lang); + } + }).then(tpl).promise(); } return all_ready; }; diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js index 945b0ed353d..6aa97110ff6 100644 --- a/addons/website/static/src/js/website.seo.js +++ b/addons/website/static/src/js/website.seo.js @@ -9,7 +9,7 @@ 'click a[data-action=promote-current-page]': 'launchSeo', }), launchSeo: function () { - (new website.seo.Configurator()).appendTo($(document.body)); + (new website.seo.Configurator(this)).appendTo($(document.body)); }, }); @@ -320,6 +320,9 @@ maxTitleSize: 65, maxDescriptionSize: 155, start: function () { + this.loadMetaData().then(function (data) { + console.log(data); + }); var self = this; var $modal = self.$el; var htmlPage = this.htmlPage = new website.seo.HtmlPage(); @@ -400,6 +403,51 @@ // TODO Persist changes this.$el.modal('hide'); }, + getMainObject: function () { + var repr = $('html').data('main-object'); + var m = repr.match(/.+\((.+), (\d+)\)/); + if (!m) { + return null; + } else { + return { + model: m[1], + id: m[2]|0 + }; + } + }, + loadMetaData: function () { + var self = this; + var obj = this.getMainObject(); + var def = $.Deferred(); + if (!obj) { + // return $.Deferred().reject(new Error("No main_object was found.")); + def.resolve(null); + } else { + var fields = ['name', 'website_description', 'website_keywords']; + var model = website.session.model(obj.model); + model.call('read', [[obj.id], fields, website.get_context()]).then(function (data) { + if (data.length) { + var meta = data[0]; + meta['model'] = obj.model; + def.resolve(meta); + } else { + def.resolve(null); + } + }).fail(function () { + def.reject(); + }); + } + return def; + }, + saveMetaData: function (data) { + var obj = this.getMainObject(); + if (!obj) { + return $.Deferred().reject(); + } else { + var model = website.session.model(obj.model); + return model.call('write', [[obj.id], data, website.get_context()]); + } + }, titleChanged: function () { var self = this; setTimeout(function () { diff --git a/addons/website/views/views.xml b/addons/website/views/views.xml index cb0be35dee7..12a6a1b2d8a 100644 --- a/addons/website/views/views.xml +++ b/addons/website/views/views.xml @@ -14,10 +14,13 @@