diff --git a/addons/website/__init__.py b/addons/website/__init__.py index d9cff35c01a..185183a2257 100644 --- a/addons/website/__init__.py +++ b/addons/website/__init__.py @@ -1,5 +1,3 @@ import controllers -import res_config -import website -import view +import models diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py new file mode 100644 index 00000000000..94d4412b1cf --- /dev/null +++ b/addons/website/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +import ir_fields +import res_config +import view +import website diff --git a/addons/website/models/ir_fields.py b/addons/website/models/ir_fields.py new file mode 100644 index 00000000000..fbccf9c296d --- /dev/null +++ b/addons/website/models/ir_fields.py @@ -0,0 +1,87 @@ +# -*- 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/res_config.py b/addons/website/models/res_config.py similarity index 100% rename from addons/website/res_config.py rename to addons/website/models/res_config.py diff --git a/addons/website/view.py b/addons/website/models/view.py similarity index 66% rename from addons/website/view.py rename to addons/website/models/view.py index 6290a326fcf..6ddae129185 100644 --- a/addons/website/view.py +++ b/addons/website/models/view.py @@ -55,57 +55,19 @@ class view(osv.osv): def extract_embedded_fields(self, cr, uid, arch, context=None): return arch.xpath('//*[@data-oe-model != "ir.ui.view"]') - def convert_embedded_field(self, cr, uid, el, column, - type_override=None, context=None): - """ Converts the content of an embedded field to a value acceptable - for writing in the column - - :param etree._Element el: embedded field being saved - :param fields._column column: column object corresponding to the field - :param type type_override: column type to dispatch on instead of the - column's actual type (for proxy column types - e.g. relateds) - :return: converted value - """ - column_type = type_override or type(column) - - if issubclass(column_type, fields.html): - content = [] - if el.text: content.append(el.text) - content.extend(html.tostring(child) - for child in el.iterchildren(tag=etree.Element)) - return '\n'.join(content) - elif issubclass(column_type, fields.integer): - return int(el.text_content()) - elif issubclass(column_type, fields.float): - return float(el.text_content()) - elif issubclass(column_type, (fields.char, fields.text, - fields.date, fields.datetime)): - return el.text_content() - # TODO: fields.selection - elif issubclass(column_type, fields.many2one): - matches = self.pool[column._obj].name_search( - cr, uid, name=el.text_content().strip(), context=context) - # FIXME: more than one match, error reporting - return matches[0][0] - elif issubclass(column_type, fields.function): - # FIXME: special-case selection as in get_pg_type? - return self.convert_embedded_field( - cr, uid, el, column, - type_override=getattr(fields, column._type), - context=context) - # TODO?: fields.many2many, fields.one2many - # TODO?: fields.reference - else: - raise TypeError("Un-convertable column type %s" % column_type) - def save_embedded_field(self, cr, uid, el, context=None): Model = self.pool[el.get('data-oe-model')] 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 + Model.write(cr, uid, [int(el.get('data-oe-id'))], { - field: self.convert_embedded_field(cr, uid, el, column, context=context) + field: value }, context=context) def to_field_ref(self, cr, uid, el, context=None): @@ -137,7 +99,8 @@ class view(osv.osv): """ res_id = int(res_id) - arch_section = html.fromstring(value) + arch_section = html.fromstring( + value, parser=html.HTMLParser(encoding='utf-8')) if xpath is None: # value is an embedded field on its own, not a view section diff --git a/addons/website/website.py b/addons/website/models/website.py similarity index 100% rename from addons/website/website.py rename to addons/website/models/website.py diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 80057dbf21d..1796729f18c 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -367,6 +367,25 @@ ul.nav-stacked > li > a { } /* ---- PUBLISH ---- */ +a[data-publish] { + text-decoration: none !important; +} +a[data-publish] .css_unpublish, a[data-publish] .css_publish, a[data-publish] .css_unpublished, a[data-publish] .css_published { + display: none; +} +a[data-publish][data-publish='off'] .css_unpublished, a[data-publish][data-publish='off']:hover .css_publish { + display: inline; +} +a[data-publish][data-publish='off']:hover .css_unpublished { + display: none; +} +a[data-publish][data-publish='on'] .css_published, a[data-publish][data-publish='on']:hover .css_unpublish { + display: inline; +} +a[data-publish][data-publish='on']:hover .css_published { + display: none; +} + .dropdown .css_unpublish, .dropdown .css_publish { display: none; } diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index b1f43b543a3..9834fb66aed 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -283,6 +283,20 @@ ul.nav-stacked > li > a text-transform: uppercase /* ---- PUBLISH ---- */ +a[data-publish] + text-decoration: none !important + .css_unpublish, .css_publish, .css_unpublished, .css_published + display: none + &[data-publish='off'] + .css_unpublished, &:hover .css_publish + display: inline + &:hover .css_unpublished + display: none + &[data-publish='on'] + .css_published, &:hover .css_unpublish + display: inline + &:hover .css_published + display: none .dropdown .css_unpublish, .dropdown .css_publish display: none diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 8b15422b90d..b8140b5a1c1 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -911,6 +911,7 @@ while (node && !$(node).hasClass('oe_editable')) { node = node.parentNode; } + $(m.target).trigger('node_changed'); return node; }) .compact() diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index bc0db10ce88..e524edfb389 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -117,9 +117,29 @@ }; dom_ready.then(function () { + /* ----- PUBLISHING STUFF ---- */ + $('[data-publish]:has(.js_publish)').each(function () { + var $pub = $("[data-publish]", this); + if($pub.size()) + $(this).attr("data-publish", $pub.attr("data-publish")); + else + $(this).removeAttr("data-publish"); + }); + $('[data-publish]:has(.js_publish_management)').each(function () { $(this).attr("data-publish", $(".js_publish_management .btn-success", this).size() ? "on" : 'off'); + $(this).attr("data-publish", $(".js_publish_management .btn-success", this).size() ? "on" : 'off'); + }); + + $(document).on('click', '.js_publish', function (e) { + e.preventDefault(); + var $data = $(":first", this).parents("[data-publish]"); + $data.attr("data-publish", $data.first().attr("data-publish") == 'off' ? 'on' : 'off'); + openerp.jsonRpc('/website/publish', 'call', {'id': $(this).data('id'), 'object': $(this).data('object')}) + .then(function (result) { + $data.attr("data-publish", +result ? 'on' : 'off'); + }); }); $(document).on('click', '.js_publish_management .js_publish_btn', function (e) { diff --git a/addons/website/static/src/js/website.snippets.js b/addons/website/static/src/js/website.snippets.js index 9a6ade27992..3ef98d53d2d 100644 --- a/addons/website/static/src/js/website.snippets.js +++ b/addons/website/static/src/js/website.snippets.js @@ -573,7 +573,6 @@ onBlurEdit : function () {}, }); - website.snippet.editorRegistry = {}; website.snippet.Editor = openerp.Class.extend({ init: function (parent, dom) { @@ -785,6 +784,8 @@ */ onFocus : function () { this.$overlay.addClass('oe_active'); + if (this.$background_img) + this.$background_img.prependTo(this.$target); }, /* onFocus @@ -792,6 +793,75 @@ */ onBlur : function () { this.$overlay.removeClass('oe_active'); + if (this.$background_img) + this.$background_img.detach(); + }, + + /* + * active the cke rte editor for change image url + */ + active_change_image: function ($image) { + var self = this; + // cke select image with mousedown on image + var $image = $($image); + var image = $image[0]; + var event = document.createEvent( 'HTMLEvents' ); + event.initEvent( 'mousedown', true, true ); + image.dispatchEvent( event ); + + // hack to active editor with doubleclick on editor + var dblclick = null; + $image.on("dblclick", function (evt) { + evt.preventDefault(); + dblclick = new CKEDITOR.dom.event(evt.originalEvent); + return; + }); + var event = document.createEvent( 'HTMLEvents' ); + event.initEvent( 'dblclick', true, true ); + image.dispatchEvent( event ); + }, + + change_background: function (bg, ul_options) { + var self = this; + var $ul = this.$editor.find(ul_options); + var bg_value = $(bg).css("background-image").replace(/url\(|\)/g, ""); + + // must add an image in the DOM for rte editor + this.$background_img = $("").css({position: "absolute", zIndex: -10, left:-1000000, top:-1000000}); + this.$background_img.prependTo(this.$target); + this.$background_img.on("node_changed", function () { + $(bg).css("background-image", "url(" + $(this).attr('src') + ")"); + }); + + // select in ul options + $ul.find("li").removeClass("active"); + var selected = $ul.find('[data-value="' + bg_value + '"], [data-value="' + bg_value.replace(/.*:\/\/[^\/]+/, '') + '"]'); + selected.addClass('active'); + if (!selected.length) { + $ul.find('.oe_custom_bg b').html(bg_value); + } + + // bind envent on options + var $li = $ul.find("li"); + $li.on('click', function (event) { + if ($(this).data("value")) { + $li.removeClass("active"); + $(this).addClass("active"); + self.$editor.find('input').val(""); + } else { + self.active_change_image(self.$background_img); + } + }) + .on('mouseover', function (event) { + if ($(this).data("value")) { + var src = $(this).data("value"); + self.$background_img.attr('src', src); + } + }) + .on('mouseout', function (event) { + var src = $ul.find('li.active').data("value"); + self.$background_img.attr('src', src); + }); }, }); @@ -991,7 +1061,7 @@ this.$editor.find(".js_add").on('click', _.bind(this.on_add, this)); this.$editor.find(".js_remove").on('click', _.bind(this.on_remove, this)); - this.change_background(); + this.change_background(".item.active", 'ul[name="carousel-background"]'); this.change_style(); this.change_size(); }, @@ -1022,40 +1092,9 @@ } }, onFocus: function () { - this.set_options_background(); this.set_options_style(); this._super(); }, - set_options_background: function () { - var $image = this.$target.find('.carousel-inner .item.active > img'); - var bg = $image.attr('src'); - var $ul = this.$editor.find('ul[name="carousel-background"]'); - $ul.find("li").removeClass("active"); - var selected = $ul.find('[data-value="' + bg + '"], [data-value="' + bg.replace(/.*:\/\/[^\/]+/, '') + '"]') - .addClass('active').length; - this.$editor.find('.oe_custom_bg b').html(selected ? "" : bg); - }, - change_background: function () { - var self = this; - var $ul = this.$editor.find('ul[name="carousel-background"]'); - var $li = $ul.find("li"); - - $li.on('click', function (event) { - $li.removeClass("active"); - $(this).addClass("active"); - self.$editor.find('input').val(""); - }) - .on('mouseover', function (event) { - if ($(this).data("value")) { - self.$target.find('.carousel-inner .item.active > img') - .attr('src', $(this).data("value")); - } - }) - .on('mouseout', function (event) { - self.$target.find('.carousel-inner .item.active > img') - .attr('src', $ul.find('li.active').data("value")); - }); - }, set_options_style: function () { var style = false; var $el = this.$target.find('.carousel-inner .item.active'); @@ -1124,50 +1163,7 @@ website.snippet.editorRegistry.parallax = website.snippet.editorRegistry.resize.extend({ start : function () { this._super(); - this.change_background(); - }, - onFocus: function () { - this.$imagebg = $(""); - this.$imagebg.prependTo(this.$target); - this.set_options_background(); - this._super(); - }, - onBlur : function () { - this._super(); - this.$imagebg.remove(); - }, - set_options_background: function () { - var bg = this.$target.css("background-image").replace(/url\(|\)/g, ""); - this.$imagebg.attr("src", bg); - - var $ul = this.$editor.find('ul[name="parallax-background"]'); - $ul.find("li").removeClass("active"); - var selected = $ul.find('[data-value="' + bg + '"], [data-value="' + bg.replace(/.*:\/\/[^\/]+/, '') + '"]') - .addClass('active').length; - this.$editor.find('.oe_custom_bg b').html(selected ? "" : bg); - }, - change_background: function () { - var self = this; - var $ul = this.$editor.find('ul[name="parallax-background"]'); - var $li = $ul.find("li"); - - $li.on('click', function (event) { - $li.removeClass("active"); - $(this).addClass("active"); - self.$editor.find('input').val(""); - }) - .on('mouseover', function (event) { - if ($(this).data("value")) { - var src = $(this).data("value"); - self.$imagebg.attr('src', src); - self.$target.css("background-image", "url(" + src + ")"); - } - }) - .on('mouseout', function (event) { - var src = $ul.find('li.active').data("value"); - self.$imagebg.attr('src', src); - self.$target.css("background-image", "url(" + src + ")"); - }); + this.change_background(this.$target, 'ul[name="parallax-background"]'); }, }); diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/website/static/src/xml/website.snippets.xml index a5599a506e0..b0ee431ea5c 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/website/static/src/xml/website.snippets.xml @@ -139,7 +139,7 @@
  • Velour
  • Wood
  • Yellow Green
  • -
  • +
  • Enter your image
  • Landscape
  • Mango
  • Purple
  • -
  • +
  • Enter your image
  • diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index 9ff1f01db62..b00c5984aa0 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- -import test_views +import test_views, test_converter -checks = [ test_views ] +checks = [ test_views, test_converter, ] diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py new file mode 100644 index 00000000000..7f042988916 --- /dev/null +++ b/addons/website/tests/test_converter.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +from collections import namedtuple +from functools import partial +from xml.dom.minidom import getDOMImplementation + +from lxml import etree, html + +from openerp.tests import common +from openerp.tools import qweb +import unittest2 + +renderer = qweb.QWebXml() +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) + id = Model.create( + self.cr, self.uid, { + field: value + }) + [record] = Model.browse(self.cr, self.uid, [id]) + + e = document.createElement('span') + field_value = 'record.%s' % field + e.setAttribute('t-field', field_value) + + rendered = renderer.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')) + + column = Model._all_columns[field].column + + from_html = self.Converter.to_field( + self.cr, self.uid, model, column, 'html') + + value_back, warnings = from_html(element) + self.assertEqual(warnings, []) + + if isinstance(expected, str): + expected = expected.decode('utf-8') + self.assertEqual(value_back, expected) + + def field_roundtrip(self, field, value): + self.field_rountrip_result(field, value, value) + + def test_integer(self): + self.field_roundtrip('integer', 42) + + def test_float(self): + self.field_roundtrip('float', 42.567890) + + def test_numeric(self): + self.field_roundtrip('numeric', 42.77) + + def test_char(self): + self.field_roundtrip('char', "foo bar") + + self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ") + + def test_m2o(self): + Sub = self.registry('website.converter.test.sub') + sub = partial(Sub.create, self.cr, self.uid) + ids = [ + sub({'name': "Foo"}), + sub({'name': "Bar"}), + sub({'name': "Baz"}), + ] + + self.field_rountrip_result( + 'many2one', + ids[2], + ids[2]) + + def test_selection(self): + self.field_roundtrip('selection', 3) + + def test_selection_str(self): + self.field_roundtrip('selection_str', 'B') + + def test_text(self): + self.field_roundtrip('text', """ + You must obey the dance commander + Givin' out the order for fun + You must obey the dance commander + You know that he's the only one + Who gives the orders here, + Alright + Who gives the orders here, + Alright + + It would be awesome + If we could dance-a + It would be awesome, yeah + Let's take the chance-a + It would be awesome, yeah + Let's start the show + Because you never know + You never know + You never know until you go + """) diff --git a/addons/website/views/views.xml b/addons/website/views/views.xml index 3854b54b936..f8e175a96df 100644 --- a/addons/website/views/views.xml +++ b/addons/website/views/views.xml @@ -361,22 +361,37 @@ + + + + + + + + + + - + + + +