diff --git a/addons/decimal_precision/decimal_precision.py b/addons/decimal_precision/decimal_precision.py index b237f053ebb..5576038d54d 100644 --- a/addons/decimal_precision/decimal_precision.py +++ b/addons/decimal_precision/decimal_precision.py @@ -22,9 +22,9 @@ import openerp from openerp import SUPERUSER_ID from openerp import tools -from openerp.osv import osv, fields +from openerp.osv import orm, fields -class decimal_precision(osv.osv): +class decimal_precision(orm.Model): _name = 'decimal.precision' _columns = { 'name': fields.char('Usage', size=50, select=True, required=True), @@ -71,4 +71,29 @@ def get_precision(application): return (16, res) return change_digit +class DecimalPrecisionFloat(orm.AbstractModel): + """ Override qweb.field.float to add a `decimal_precision` domain option + and use that instead of the column's own value if it is specified + """ + _inherit = 'ir.qweb.field.float' + + + def precision(self, cr, uid, column, options=None, context=None): + dp = options and options.get('decimal_precision') + if dp: + return self.pool['decimal.precision'].precision_get( + cr, uid, dp) + + return super(DecimalPrecisionFloat, self).precision( + cr, uid, column, options=options, context=context) + +class DecimalPrecisionTestModel(orm.Model): + _name = 'decimal.precision.test' + + _columns = { + 'float': fields.float(), + 'float_2': fields.float(digits=(16, 2)), + 'float_4': fields.float(digits=(16, 4)), + } + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/decimal_precision/tests/__init__.py b/addons/decimal_precision/tests/__init__.py new file mode 100644 index 00000000000..feee24c8f15 --- /dev/null +++ b/addons/decimal_precision/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +import test_qweb_float + +checks = [ + test_qweb_float +] diff --git a/addons/decimal_precision/tests/test_qweb_float.py b/addons/decimal_precision/tests/test_qweb_float.py new file mode 100644 index 00000000000..93334170f37 --- /dev/null +++ b/addons/decimal_precision/tests/test_qweb_float.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +from openerp.tests import common + +class TestFloatExport(common.TransactionCase): + def setUp(self): + super(TestFloatExport, self).setUp() + self.Model = self.registry('decimal.precision.test') + + def get_converter(self, name): + converter = self.registry('ir.qweb.field.float') + column = self.Model._all_columns[name].column + + return lambda value, options=None: converter.value_to_html( + self.cr, self.uid, value, column, options=options, context=None) + + def test_basic_float(self): + converter = self.get_converter('float') + self.assertEqual( + converter(42.0), + "42.0") + self.assertEqual( + converter(42.12345), + "42.12345") + + converter = self.get_converter('float_2') + self.assertEqual( + converter(42.0), + "42.00") + self.assertEqual( + converter(42.12345), + "42.12") + + converter = self.get_converter('float_4') + self.assertEqual( + converter(42.0), + '42.0000') + self.assertEqual( + converter(42.12345), + '42.1234') + + def test_precision_domain(self): + DP = self.registry('decimal.precision') + DP.create(self.cr, self.uid, { + 'name': 'A', + 'digits': 2, + }) + DP.create(self.cr, self.uid, { + 'name': 'B', + 'digits': 6, + }) + + converter = self.get_converter('float') + self.assertEqual( + converter(42.0, {'decimal_precision': 'A'}), + '42.00') + self.assertEqual( + converter(42.0, {'decimal_precision': 'B'}), + '42.000000') + + converter = self.get_converter('float_4') + self.assertEqual( + converter(42.12345, {'decimal_precision': 'A'}), + '42.12') + self.assertEqual( + converter(42.12345, {'decimal_precision': 'B'}), + '42.123450') diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index f348bd7d9c3..948422e8296 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -39,12 +39,12 @@ class Field(orm.AbstractModel): _inherit = 'ir.qweb.field' def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context): + source_element, g_att, t_att, qweb_context, context=None): 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), + qweb_context, context=context), [('data-oe-translate', 1 if column.translate else 0)] ) @@ -67,7 +67,27 @@ class Float(orm.AbstractModel): _name = 'website.qweb.field.float' _inherit = ['website.qweb.field', 'ir.qweb.field.float'] - value_from_string = float + def from_html(self, cr, uid, model, column, element, context=None): + lang = self.user_lang(cr, uid, context=context) + + value = element.text_content().strip() + + return float(value.replace(lang.thousands_sep, '') + .replace(lang.decimal_point, '.')) + +class Date(orm.AbstractModel): + _name = 'website.qweb.field.date' + _inherit = ['website.qweb.field', 'ir.qweb.field.date'] + + def from_html(self, cr, uid, model, column, element, context=None): + raise NotImplementedError("Can not parse and save localized dates") + +class DateTime(orm.AbstractModel): + _name = 'website.qweb.field.datetime' + _inherit = ['website.qweb.field', 'ir.qweb.field.datetime'] + + def from_html(self, cr, uid, model, column, element, context=None): + raise NotImplementedError("Can not parse and save localized datetimes") class Text(orm.AbstractModel): _name = 'website.qweb.field.text' @@ -127,7 +147,7 @@ class Image(orm.AbstractModel): _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): + source_element, t_att, g_att, qweb_context, context=None): 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 " \ @@ -135,9 +155,9 @@ class Image(orm.AbstractModel): return super(Image, self).to_html( cr, uid, field_name, record, options, - source_element, t_att, g_att, qweb_context) + source_element, t_att, g_att, qweb_context, context=context) - def record_to_html(self, cr, uid, field_name, record, column, options=None): + def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None): cls = '' if 'class' in options: cls = ' class="%s"' % werkzeug.utils.escape(options['class']) @@ -183,4 +203,9 @@ class Monetary(orm.AbstractModel): _inherit = ['website.qweb.field', 'ir.qweb.field.monetary'] def from_html(self, cr, uid, model, column, element, context=None): - return float(element.find('span').text) + lang = self.user_lang(cr, uid, context=context) + + value = element.find('span').text.strip() + + return float(value.replace(lang.thousands_sep, '') + .replace(lang.decimal_point, '.')) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index d6ad607fa3f..955ca4f758b 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -2,6 +2,7 @@ from lxml import etree, html from openerp.osv import osv, fields +from urlparse import urlparse class view(osv.osv): @@ -91,6 +92,23 @@ class view(osv.osv): arch = previous_arch return arch + def _normalize_urls(self, element): + attr = None + if element.tag == 'form': + attr = 'action' if 'action' in element.attrib else None + elif element.tag in ['a', 'link']: + attr = 'href' if 'href' in element.attrib else None + elif element.tag in ['frame', 'iframe', 'img', 'input', 'script']: + attr = 'src' if 'src' in element.attrib else None + if attr: + value = element.attrib[attr] + if not urlparse(value).scheme: + element.attrib.pop(attr) + element.attrib['t-' + attr] = value + for el in list(element): + self._normalize_urls(el) + return element + def save(self, cr, uid, res_id, value, xpath=None, context=None): """ Update a view section. The view section may embed fields to write @@ -103,6 +121,8 @@ class view(osv.osv): arch_section = html.fromstring( value, parser=html.HTMLParser(encoding='utf-8')) + self._normalize_urls(arch_section) + 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) diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 4fe9a0888b4..431170e462b 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -5,7 +5,10 @@ access_website_converter_test,access_website_converter_test,model_website_conver 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_integer,access_website_qweb_field_integer,model_website_qweb_field_integer,,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_date,access_website_qweb_field_date,model_website_qweb_field_date,,0,0,0,0 +access_website_qweb_field_datetime,access_website_qweb_field_datetime,model_website_qweb_field_datetime,,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 diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index a009bf61f23..a3151d14855 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -608,7 +608,6 @@ table.editorbar-panel td.selected { right: 0; z-index: 1000; height: 100%; - width: 720px; } .oe_ace_view_editor .oe_ace_view_editor_title { width: 100%; @@ -637,11 +636,13 @@ table.editorbar-panel td.selected { right: 0; bottom: 51px; left: 0; - width: 720px; +} +.oe_ace_view_editor .ace_editor .ace_gutter { + cursor: ew-resize; } .oe_ace_view_editor.oe_ace_open { - filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); - opacity: 1; + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=97); + opacity: 0.97; } .oe_ace_view_editor.oe_ace_closed { z-index: -1000; diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index 461667d8e54..dcfa88b89e7 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -466,7 +466,6 @@ $highlighted_text_color: #ffffff /* ---- ACE EDITOR ---- */ -$ace_width: 720px $editorbar_height: 30px // TODO Fix => might break with themes $navbar_height: 51px @@ -481,7 +480,6 @@ $navbar_height: 51px right: 0 z-index: 1000 height: 100% - width: $ace_width .oe_ace_view_editor_title width: 100% padding-top: 0 @@ -502,9 +500,10 @@ $navbar_height: 51px right: 0 bottom: $navbar_height left: 0 - width: $ace_width + .ace_gutter + cursor: ew-resize &.oe_ace_open - +opacity(1) + +opacity(0.97) &.oe_ace_closed z-index: -1000 +opacity(0) diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index faf3e8f7e04..02f5eb68d5e 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -100,6 +100,13 @@ self.aceEditor.resize(); self.$el.width(width); } + function storeEditorWidth() { + window.localStorage.setItem('ace_editor_width', self.$el.width()); + } + function readEditorWidth() { + var width = window.localStorage.getItem('ace_editor_width'); + return parseInt(width || 720, 10); + } function startResizing (e) { self.refX = e.pageX; self.resizing = true; @@ -113,16 +120,23 @@ var width = self.$el.width() - offset; self.refX = e.pageX; resizeEditor(width); + storeEditorWidth(); } } document.body.addEventListener('mouseup', stopResizing, true); self.$('.ace_gutter').mouseup(stopResizing).mousedown(startResizing).click(stopResizing); $(document).mousemove(updateWidth); + $('button[data-action=edit]').click(function () { + self.close(); + }); + resizeEditor(readEditorWidth()); }, loadViews: function (views) { var self = this; - var activeViews = _.filter(views, function (view) { + var activeViews = _.uniq(_.filter(views, function (view) { return view.active; + }), false, function (view) { + return view.id; }); var $viewList = self.$('#ace-view-list'); _.each(activeViews, function (view) { diff --git a/addons/website/static/src/js/website.snippets.js b/addons/website/static/src/js/website.snippets.js index e411ee7065c..ee759e3d153 100644 --- a/addons/website/static/src/js/website.snippets.js +++ b/addons/website/static/src/js/website.snippets.js @@ -341,13 +341,13 @@ }); }, stop: function(ev, ui){ - if (action === 'insert' && ! dropped) { - var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first() - if (el) { - el.after($toInsert) - dropped = true; - } - } + if (action === 'insert' && ! dropped) { + var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first() + if (el) { + el.after($toInsert) + dropped = true; + } + } $('.oe_drop_zone').droppable('destroy').remove(); if (dropped) { @@ -1077,14 +1077,6 @@ self.$target.carousel(); }); - this.$target.on('dblclick', '.item.active .carousel-image img', function (event) { - var $img = $(event.currentTarget); - var editor = new website.editor.ImageDialog(); - editor.on('start', self, function (o) {o.url = $img.attr("src");}); - editor.on('save', self, function (o) {$img.attr("src", o.url);}); - editor.appendTo($('body')); - }); - this.rebind_event(); }, // rebind event to active carousel on edit mode diff --git a/addons/website/static/src/js/website.tour.basic.js b/addons/website/static/src/js/website.tour.basic.js new file mode 100644 index 00000000000..82ad4790c25 --- /dev/null +++ b/addons/website/static/src/js/website.tour.basic.js @@ -0,0 +1,161 @@ +(function () { + 'use strict'; + + var website = openerp.website; + + var render = website.tour.render; + + website.EditorBasicTour = website.EditorTour.extend({ + id: 'add_banner_tour', + name: "Insert a banner", + init: function (editor) { + var self = this; + self.steps = [ + { + stepId: 'welcome', + orphan: true, + backdrop: true, + title: "Welcome to your website!", + content: "This tutorial will guide you to build your first page. We will start by adding a banner.", + template: render('website.tour_popover', { next: "Start Tutorial", end: "Skip It" }), + }, + { + stepId: 'edit-page', + element: 'button[data-action=edit]', + placement: 'bottom', + reflex: true, + title: "Edit this page", + content: "Every page of your website can be modified through the Edit button.", + template: render('website.tour_popover'), + }, + { + stepId: 'add-block', + element: 'button[data-action=snippet]', + placement: 'bottom', + title: "Insert building blocks", + content: "To add content in a page, you can insert building blocks.", + template: render('website.tour_popover'), + onShow: function () { + function refreshAddBlockStep () { + self.tour.showStep(self.indexOfStep('add-block')); + editor.off('rte:ready', editor, refreshAddBlockStep); + } + editor.on('rte:ready', editor, refreshAddBlockStep); + $('button[data-action=snippet]').click(function () { + self.movetoStep('drag-banner'); + }); + } + }, + { + stepId: 'drag-banner', + element: '#website-top-navbar [data-snippet-id=carousel].ui-draggable', + placement: 'bottom', + title: "Drag & Drop a Banner", + content: "Drag the Banner block and drop it in your page.
Tip: release the mouse button when you are in a valid zone, with a preview of the banner.
", + template: render('website.tour_popover'), + onShow: function () { + function beginDrag () { + $('.popover.tour').remove(); + $('body').off('mousedown', beginDrag); + function goToNextStep () { + $('#oe_snippets').hide(); + self.movetoStep('edit-title'); + $('body').off('mouseup', goToNextStep); + } + $('body').on('mouseup', goToNextStep); + } + $('body').on('mousedown', beginDrag); + }, + }, + { + stepId: 'edit-title', + element: '#wrap [data-snippet-id=carousel]:first .carousel-caption', + placement: 'left', + title: "Customize banner's text", + content: "Click in the text and start editing it. Click continue once it's done.", + template: render('website.tour_popover', { next: "Continue" }), + onHide: function () { + var $banner = $("#wrap [data-snippet-id=carousel]:first"); + if ($banner.length) { + $banner.click(); + } + }, + }, + { + stepId: 'customize-banner', + element: '.oe_overlay_options .oe_options', + placement: 'left', + title: "Customize the banner", + content: "You can customize components of your page through the Customize menu. Try to change the background of your banner.", + template: render('website.tour_popover', { next: "Continue" }), + onShow: function () { + $('.dropdown-menu [name=carousel-background]').click(function () { + self.movetoStep('save-changes'); + }); + }, + }, + { + stepId: 'save-changes', + element: 'button[data-action=save]', + placement: 'right', + reflex: true, + title: "Save your modifications", + content: "Once you click on save, your website page is updated.", + template: render('website.tour_popover'), + onHide: function () { + self.saveStep('part-2'); + }, + + }, + { + stepId: 'part-2', + orphan: true, + title: "Congratulation!", + content: "Your homepage have been updated. Now, we suggest you to insert others building blocks like texts and images to structure your page.", + template: render('website.tour_popover', { next: "Continue" }), + }, + { + stepId: 'show-tutorials', + element: '#help-menu-button', + placement: 'left', + title: "Help is always available", + content: "But you can always click here if you want more tutorials.", + template: render('website.tour_popover', { end: "Close" }), + }, + ]; + return this._super(); + }, + startOfPart2: function () { + var currentStepIndex = this.currentStepIndex(); + var secondPartIndex = this.indexOfStep('part-2'); + var showTutorialsIndex = this.indexOfStep('show-tutorials'); + return (currentStepIndex === secondPartIndex || currentStepIndex === showTutorialsIndex) && !this.tour.ended(); + }, + canResume: function () { + return this.startOfPart2() || this._super(); + }, + }); + + website.EditorBar.include({ + start: function () { + var menu = $('#help-menu'); + var basicTour = new website.EditorBasicTour(this); + var $menuItem = $($.parseHTML('Tip: release the mouse button when you are in a valid zone, with a preview of the banner.
", - template: render('website.tour_popover'), - onShow: function () { - function beginDrag () { - $('.popover.tour').remove(); - $('body').off('mousedown', beginDrag); - function goToNextStep () { - $('#oe_snippets').hide(); - self.movetoStep('edit-title'); - $('body').off('mouseup', goToNextStep); - } - $('body').on('mouseup', goToNextStep); - } - $('body').on('mousedown', beginDrag); - }, - }, - { - stepId: 'edit-title', - element: '#wrap [data-snippet-id=carousel]:first .carousel-caption', - placement: 'left', - title: "Customize banner's text", - content: "Click in the text and start editing it. Click continue once it's done.", - template: render('website.tour_popover', { next: "Continue" }), - onHide: function () { - var $banner = $("#wrap [data-snippet-id=carousel]:first"); - if ($banner.length) { - $banner.click(); - } - }, - }, - { - stepId: 'customize-banner', - element: '.oe_overlay_options .oe_options', - placement: 'left', - title: "Customize the banner", - content: "You can customize components of your page through the Customize menu. Try to change the background of your banner.", - template: render('website.tour_popover', { next: "Continue" }), - onShow: function () { - $('.dropdown-menu [name=carousel-background]').click(function () { - self.movetoStep('save-changes'); - }); - }, - }, - { - stepId: 'save-changes', - element: 'button[data-action=save]', - placement: 'right', - reflex: true, - title: "Save your modifications", - content: "Once you click on save, your website page is updated.", - template: render('website.tour_popover'), - onHide: function () { - self.saveStep('part-2'); - }, - - }, - { - stepId: 'part-2', - orphan: true, - title: "Congratulation!", - content: "Your homepage have been updated. Now, we suggest you to insert others building blocks like texts and images to structure your page.", - template: render('website.tour_popover', { next: "Continue" }), - }, - { - stepId: 'show-tutorials', - element: '#help-menu-button', - placement: 'left', - title: "Help is always available", - content: "But you can always click here if you want more tutorials.", - template: render('website.tour_popover', { end: "Close" }), - }, - ]; - return this._super(); - }, - startOfPart2: function () { - var currentStepIndex = this.currentStepIndex(); - var secondPartIndex = this.indexOfStep('part-2'); - var showTutorialsIndex = this.indexOfStep('show-tutorials'); - return (currentStepIndex === secondPartIndex || currentStepIndex === showTutorialsIndex) && !this.tour.ended(); - }, - canResume: function () { - return this.startOfPart2() || this._super(); - }, - }); - website.UrlParser = openerp.Class.extend({ init: function (url) { var a = document.createElement('a'); @@ -227,30 +98,4 @@ }, }); - website.EditorBar.include({ - start: function () { - website.tutorials = { - basic: new website.EditorBasicTour(this), - }; - var menu = $('#help-menu'); - _.each(website.tutorials, function (tutorial) { - var $menuItem = $($.parseHTML('