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/controllers/main.py b/addons/website/controllers/main.py index 083f9f3aef7..2f49071adf6 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -51,8 +51,7 @@ class Website(openerp.addons.web.controllers.main.Home): def index(self, **kw): return self.page("website.homepage") - # FIXME: auth, if /pagenew known anybody can create new empty page - @website.route('/pagenew/', type='http', auth="admin") + @website.route('/pagenew/', type='http', auth="user") def pagenew(self, path, noredirect=NOPE): module = 'website' # completely arbitrary max_length @@ -131,7 +130,7 @@ class Website(openerp.addons.web.controllers.main.Home): html = request.website.render('website.404', values) return html - @website.route('/website/customize_template_toggle', type='json', auth='admin') # FIXME: auth + @website.route('/website/customize_template_toggle', type='json', auth='user') def customize_template_set(self, view_id): view_obj = request.registry.get("ir.ui.view") view = view_obj.browse(request.cr, request.uid, int(view_id), @@ -145,7 +144,7 @@ class Website(openerp.addons.web.controllers.main.Home): }, context=request.context) return True - @website.route('/website/customize_template_get', type='json', auth='admin') # FIXME: auth + @website.route('/website/customize_template_get', type='json', auth='user') def customize_template_get(self, xml_id, optional=True): imd = request.registry['ir.model.data'] view_model, view_theme_id = imd.get_object_reference( @@ -215,11 +214,9 @@ class Website(openerp.addons.web.controllers.main.Home): 'value': new_content, } irt.create(request.cr, request.uid, new_trans) - irt._get_source.clear_cache(irt) # FIXME: find why ir.translation does not invalidate return True - # # FIXME: auth, anybody can upload an attachment if URL known/found - @website.route('/website/attach', type='http', auth='admin') + @website.route('/website/attach', type='http', auth='user') def attach(self, func, upload): req = request.httprequest if req.method != 'POST': @@ -229,12 +226,18 @@ class Website(openerp.addons.web.controllers.main.Home): try: attachment_id = request.registry['ir.attachment'].create(request.cr, request.uid, { 'name': upload.filename, - 'datas': base64.encodestring(upload.read()), + 'datas': upload.read().encode('base64'), 'datas_fname': upload.filename, 'res_model': 'ir.ui.view', }, request.context) - # FIXME: auth=user... no good. - url = '/website/attachment/%d' % attachment_id + + url = website.urlplus('/website/image', { + 'model': 'ir.attachment', + 'id': attachment_id, + 'field': 'datas', + 'max_height': MAX_IMAGE_HEIGHT, + 'max_width': MAX_IMAGE_WIDTH, + }) except Exception, e: logger.exception("Failed to upload image to attachment") message = str(e) @@ -283,7 +286,7 @@ class Images(http.Controller): return response.make_conditional(request.httprequest) @website.route('/website/image', auth="public") - def image(self, model, id, field): + def image(self, model, id, field, max_width=maxint, max_height=maxint): Model = request.registry[model] response = werkzeug.wrappers.Response() @@ -291,7 +294,7 @@ class Images(http.Controller): id = int(id) ids = Model.search(request.cr, request.uid, - [('id', '=', id)], context=request.context)\ + [('id', '=', id)], context=request.context) \ or Model.search(request.cr, openerp.SUPERUSER_ID, [('id', '=', id), ('website_published', '=', True)], context=request.context) @@ -312,8 +315,8 @@ class Images(http.Controller): response.last_modified = datetime.datetime.strptime( record[concurrency], server_format) + # Field does not exist on model or field set to False 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) @@ -324,31 +327,13 @@ class Images(http.Controller): if response.status_code == 304: return response - return self.set_image_data(response, record[field].decode('base64')) + data = record[field].decode('base64') + fit = int(max_width), int(max_height) - # FIXME: auth - # FIXME: delegate to image? - @website.route('/website/attachment/', auth='admin') - def attachment(self, id): - attachment = request.registry['ir.attachment'].browse( - request.cr, request.uid, id, request.context) - - return self.set_image_data( - werkzeug.wrappers.Response(), - attachment.datas.decode('base64'), - fit=IMAGE_LIMITS,) - - def set_image_data(self, response, data, fit=(maxint, maxint)): - """ Sets an inferred mime type on the response object, and puts the - provided image's data in it, possibly after resizing if requested - - Returns the response object after setting its mime and content, so - the result of ``get_final_image`` can be returned directly. - """ buf = cStringIO.StringIO(data) - # FIXME: unknown format or not an image image = Image.open(buf) + image.load() response.mimetype = Image.MIME[image.format] w, h = image.size @@ -356,10 +341,13 @@ class Images(http.Controller): if w < max_w and h < max_h: response.set_data(data) - return response + else: + image.thumbnail(fit, Image.ANTIALIAS) + image.save(response.stream, image.format) + # invalidate content-length computed by make_conditional as writing + # to response.stream does not do it (as of werkzeug 0.9.3) + del response.headers['Content-Length'] - image.thumbnail(fit, Image.ANTIALIAS) - image.save(response.stream, image.format) return response diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index f348bd7d9c3..d7bd704b895 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -7,17 +7,20 @@ Also, adds methods to convert values back to openerp models. """ import cStringIO +import datetime import itertools import logging import re import urllib2 +import urlparse import werkzeug.utils +from dateutil import parser from lxml import etree, html from PIL import Image as I from openerp.osv import orm, fields -from openerp.tools import ustr +from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT REMOTE_CONNECTION_TIMEOUT = 2.5 @@ -39,12 +42,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 +70,56 @@ 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, '.')) + + +def parse_fuzzy(in_format, value): + day_first = in_format.find('%d') < in_format.find('%m') + + if '%y' in in_format: + year_first = in_format.find('%y') < in_format.find('%d') + else: + year_first = in_format.find('%Y') < in_format.find('%d') + + return parser.parse(value, dayfirst=day_first, yearfirst=year_first) + +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): + lang = self.user_lang(cr, uid, context=context) + in_format = lang.date_format.encode('utf-8') + + value = element.text_content().strip() + try: + dt = datetime.datetime.strptime(in_format, value) + except ValueError: + dt = parse_fuzzy(in_format, value) + + return dt.strftime(DEFAULT_SERVER_DATE_FORMAT) + +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): + lang = self.user_lang(cr, uid, context=context) + in_format = (u"%s %s" % (lang.date_format, lang.time_format)).encode('utf-8') + + value = element.text_content().strip() + try: + dt = datetime.datetime.strptime(in_format, value) + except ValueError: + dt = parse_fuzzy(in_format, value) + + return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) class Text(orm.AbstractModel): _name = 'website.qweb.field.text' @@ -127,7 +179,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 +187,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']) @@ -148,10 +200,11 @@ class Image(orm.AbstractModel): 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: + url_object = urlparse.urlsplit(url) + query = urlparse.parse_qs(url_object.query) + if url_object.path == '/website/image' and query['model'] == 'ir.attachment': attachment = self.pool['ir.attachment'].browse( - cr, uid, int(m.group(1)), context=context) + cr, uid, int(query['id']), context=context) return attachment.datas # remote URL? @@ -183,4 +236,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..ab09336d6cb 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- +from urlparse import urlparse from lxml import etree, html -from openerp.osv import osv, fields +from openerp.osv import osv, fields +from openerp.addons.base import ir class view(osv.osv): _inherit = "ir.ui.view" @@ -82,15 +84,41 @@ class view(osv.osv): return out def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None): - arch = replacement - if section_xpath: - previous_arch = etree.fromstring(self.browse(cr, uid, view_id, context=context).arch.encode('utf-8')) + # remove branding from replacement section + for att in ir.ir_ui_view.MOVABLE_BRANDING: + replacement.attrib.pop(att, None) + + if not section_xpath: + # replace all of the arch, not just a fragment + arch = replacement + else: + arch = etree.fromstring(self.browse(cr, uid, view_id, context=context).arch.encode('utf-8')) # ensure there's only one match - [previous_section] = previous_arch.xpath(section_xpath) + [previous_section] = arch.xpath(section_xpath) + previous_section.getparent().replace(previous_section, replacement) - arch = previous_arch + return arch + URL_ATTRS = { + 'form': 'action', + 'a': 'href', + 'link': 'href', + 'frame': 'src', + 'iframe': 'src', + 'script': 'src', + } + def _normalize_urls(self, root): + for element in root.iter(): + attr = self.URL_ATTRS.get(element.tag) + if attr is None or attr not in element.attrib: + continue + + value = element.get(attr) + if not urlparse(value).scheme: + element.attrib.pop(attr) + element.set('t-' + attr, value) + 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 +131,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/models/website.py b/addons/website/models/website.py index c79ada6e9e4..d7f47c01b25 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -238,10 +238,26 @@ class website(osv.osv): return { "page_count": page_count, "offset": (page - 1) * step, - "page": {'url': get_url(page), 'num': page}, - "page_start": {'url': get_url(pmin), 'num': pmin}, - "page_end": {'url': get_url(min(pmax, page + 1)), - 'num': min(pmax, page + 1)}, + "page": { + 'url': get_url(page), + 'num': page + }, + "page_start": { + 'url': get_url(pmin), + 'num': pmin + }, + "page_previous": { + 'url': get_url(max(pmin, page - 1)), + 'num': max(pmin, page - 1) + }, + "page_next": { + 'url': get_url(min(pmax, page + 1)), + 'num': min(pmax, page + 1) + }, + "page_end": { + 'url': get_url(pmax), + 'num': pmax + }, "pages": [ {'url': get_url(page), 'num': page} for page in xrange(pmin, pmax+1) @@ -348,13 +364,18 @@ class website(osv.osv): class ir_attachment(osv.osv): _inherit = "ir.attachment" def _website_url_get(self, cr, uid, ids, name, arg, context=None): - context = context or {} result = {} for attach in self.browse(cr, uid, ids, context=context): - if attach.type=='url': + if attach.type == 'url': result[attach.id] = attach.url else: - result[attach.id] = "/website/attachment/"+str(attach.id) + result[attach.id] = urlplus('/website/image', { + 'model': 'ir.attachment', + 'field': 'datas', + 'id': attach.id, + 'max_width': 1024, + 'max_height': 768, + }) return result _columns = { 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char') @@ -386,6 +407,17 @@ class base_language_install(osv.osv): 'website_ids': fields.many2many('website', string='Websites to translate'), } + def default_get(self, cr, uid, fields, context=None): + if context is None: + context = {} + defaults = super(base_language_install, self).default_get(cr, uid, fields, context) + website_id = context.get('params', {}).get('website_id') + if website_id: + if 'website_ids' not in defaults: + defaults['website_ids'] = [] + defaults['website_ids'].append(website_id) + return defaults + def lang_install(self, cr, uid, ids, context=None): if context is None: 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/lib/ckeditor/core/dom/range.js b/addons/website/static/lib/ckeditor/core/dom/range.js index d567a50ad43..9a330fc8a70 100644 --- a/addons/website/static/lib/ckeditor/core/dom/range.js +++ b/addons/website/static/lib/ckeditor/core/dom/range.js @@ -1360,7 +1360,7 @@ CKEDITOR.dom.range = function( root ) { * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text * node, range remains if there's no text nodes on boundaries at all. * - * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node. + * @param {Boolean} [selectContents] Whether result range anchors at the inner OR outer boundary of the node. */ shrink: function( mode, selectContents, shrinkOnBlockBoundary ) { // Unable to shrink a collapsed range. diff --git a/addons/website/static/lib/ckeditor/core/selection.js b/addons/website/static/lib/ckeditor/core/selection.js index e888c3d557c..f3e47c9debb 100644 --- a/addons/website/static/lib/ckeditor/core/selection.js +++ b/addons/website/static/lib/ckeditor/core/selection.js @@ -798,7 +798,7 @@ * * @method * @member CKEDITOR.editor - * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one. + * @param {Boolean} [forceRealSelection] Return real selection, instead of saved or fake one. * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment. */ CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) { diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index a009bf61f23..8fa0ad80f5f 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -1,4 +1,3 @@ -@charset "utf-8"; /* ---- CKEditor Minimal Reset ---- */ .navbar.navbar-inverse .cke_chrome { border: none; @@ -112,6 +111,11 @@ table.editorbar-panel td.selected { background-color: #b1c9d9; } +.oe_translate_or { + color: white; + padding: 0 0.2em; +} + .oe_translate_examples li { margin: 10px; padding: 4px; @@ -169,6 +173,11 @@ table.editorbar-panel td.selected { white-space: pre-wrap; } +.oe_carlos_danger { + outline: 1px solid red !important; + background-color: #ffd9dd !important; +} + /* ---- SNIPPET EDITOR ---- */ #oe_snippets { position: fixed; @@ -608,7 +617,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 +645,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..5dbe7a8eee9 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -106,6 +106,9 @@ table.editorbar-panel background-color: #b1c9d9 // ---- TRANSLATIONS ---- {{{ +.oe_translate_or + color: white + padding: 0 0.2em .oe_translate_examples li margin: 10px padding: 4px @@ -151,6 +154,10 @@ table.editorbar-panel content: " " white-space: pre-wrap +.oe_carlos_danger + outline: 1px solid red !important + background-color: #ffd9dd !important + /* ---- SNIPPET EDITOR ---- */ #oe_snippets @@ -466,7 +473,6 @@ $highlighted_text_color: #ffffff /* ---- ACE EDITOR ---- */ -$ace_width: 720px $editorbar_height: 30px // TODO Fix => might break with themes $navbar_height: 51px @@ -481,7 +487,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 +507,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.editor.js b/addons/website/static/src/js/website.editor.js index c1f05d9b661..6fb0f459a22 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -56,7 +56,7 @@ //noinspection JSValidateTypes editor.addCommand('link', { - exec: function (editor, data) { + exec: function (editor) { link_dialog(editor); return true; }, @@ -65,7 +65,7 @@ }); //noinspection JSValidateTypes editor.addCommand('image', { - exec: function (editor, data) { + exec: function (editor) { image_dialog(editor); return true; }, @@ -177,7 +177,7 @@ var editor = new website.EditorBar(); var $body = $(document.body); editor.prependTo($body).then(function () { - if (location.search.indexOf("unable_editor") >= 0) { + if (location.search.indexOf("enable_editor") >= 0) { editor.edit(); } }); @@ -219,7 +219,7 @@ var view_id = $(event.currentTarget).data('view-id'); openerp.jsonRpc('/website/customize_template_toggle', 'call', { 'view_id': view_id - }).then( function(result) { + }).then( function() { window.location.reload(); }); }); @@ -253,7 +253,6 @@ ); }, edit: function () { - var self = this; this.$buttons.edit.prop('disabled', true); this.$('#website-top-view').hide(); this.$('#website-top-edit').show(); @@ -273,30 +272,58 @@ editor.destroy(); // FIXME: select editables then filter by dirty? var defs = this.rte.fetch_editables(root) - .removeClass('oe_editable cke_focus') - .removeAttr('contentEditable') .filter('.oe_dirty') + .removeAttr('contentEditable') + .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger') .map(function () { var $el = $(this); // TODO: Add a queue with concurrency limit in webclient // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js return self.saving_mutex.exec(function () { return self.saveElement($el) - .fail(function () { - var data = $el.data(); - console.error(_.str.sprintf('Could not save %s(%d).%s', data.oeModel, data.oeId, data.oeField)); + .then(undefined, function (thing, response) { + // because ckeditor regenerates all the dom, + // we can't just setup the popover here as + // everything will be destroyed by the DOM + // regeneration. Add markings instead, and + // returns a new rejection with all relevant + // info + var id = _.uniqueId('carlos_danger_'); + $el.addClass('oe_dirty oe_carlos_danger'); + $el.addClass(id); + return $.Deferred().reject({ + id: id, + error: response.data, + }); }); }); }).get(); return $.when.apply(null, defs).then(function () { website.reload(); + }, function (failed) { + // If there were errors, re-enable edition + self.rte.start_edition(true).then(function () { + // jquery's deferred being a pain in the ass + if (!_.isArray(failed)) { failed = [failed]; } + + _(failed).each(function (failure) { + $(root).find('.' + failure.id) + .removeClass(failure.id) + .popover({ + trigger: 'hover', + content: failure.error.message, + placement: 'auto top', + }) + // Force-show popovers so users will notice them. + .popover('show'); + }) + }); }); }, /** * Saves an RTE content, which always corresponds to a view section (?). */ saveElement: function ($el) { - $el.removeClass('oe_dirty'); var markup = $el.prop('outerHTML'); return openerp.jsonRpc('/web/dataset/call', 'call', { model: 'ir.ui.view', @@ -350,6 +377,7 @@ // The detail attribute indicates the number of times a mouse button has been pressed // we just want the triple click if (e.detail !== 3) { return; } + e.preventDefault(); // Get closest block-level element to the triple-clicked // element (using ckeditor's block list because why not) @@ -422,15 +450,25 @@ return true; }, - start_edition: function ($elements) { + /** + * Makes the page editable + * + * @param {Boolean} [restart=false] in case the edition was already set + * up once and is being re-enabled. + * @returns {$.Deferred} deferred indicating when the RTE is ready + */ + start_edition: function (restart) { var self = this; // create a single editor for the whole page var root = document.getElementById('wrapwrap'); - $(root).on('dragstart', 'img', function (e) { - e.preventDefault(); - }); - this.webkitSelectionFixer(root); - this.tableNavigation(root); + if (!restart) { + $(root).on('dragstart', 'img', function (e) { + e.preventDefault(); + }); + this.webkitSelectionFixer(root); + this.tableNavigation(root); + } + var def = $.Deferred(); var editor = this.editor = CKEDITOR.inline(root, self._config()); editor.on('instanceReady', function () { editor.setReadOnly(false); @@ -446,7 +484,9 @@ document.execCommand("enableInlineTableEditing", false, "false"); self.trigger('rte:ready'); + def.resolve(); }); + return def; }, setup_editables: function (root) { @@ -477,13 +517,12 @@ fetch_editables: function (root) { return $(root).find('[data-oe-model]') - // FIXME: propagation should make "meta" blocks non-editable in the first place... .not('link, script') .not('.oe_snippet_editor') .filter(function () { var $this = $(this); // keep view sections and fields which are *not* in - // view sections for toplevel editables + // view sections for top-level editables return $this.data('oe-model') === 'ir.ui.view' || !$this.closest('[data-oe-model = "ir.ui.view"]').length; }); @@ -854,7 +893,7 @@ this.preview_image(); }, - file_selection: function (e) { + file_selection: function () { this.$('button.filepicker').removeClass('btn-danger btn-success'); var self = this; diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 963a0b1664d..682a6175086 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -21,9 +21,6 @@ templates.push(template); }; website.load_templates = function(templates) { - var def = $.Deferred(); - var count = templates.length; - var dones = _(templates).map(function (t) { return new $.Deferred(function (d) { openerp.qweb.add_template(t, function(err) { @@ -39,8 +36,8 @@ }; website.reload = function () { location.hash = "scrollTop=" + window.document.body.scrollTop; - if (location.search.indexOf("unable_editor") > -1) { - window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?/g, ''); + if (location.search.indexOf("enable_editor") > -1) { + window.location.href = window.location.href.replace(/enable_editor(=[^&]*)?/g, ''); } else { window.location.reload(); } @@ -151,7 +148,7 @@ }); }); - $(document).on('click', '.js_publish_management .js_publish_btn', function (e) { + $(document).on('click', '.js_publish_management .js_publish_btn', function () { var $data = $(this).parents(".js_publish_management:first"); var $btn = $data.find('.btn:first'); var publish = $btn.hasClass("btn-success"); diff --git a/addons/website/static/src/js/website.snippets.js b/addons/website/static/src/js/website.snippets.js index e411ee7065c..c29325c8de8 100644 --- a/addons/website/static/src/js/website.snippets.js +++ b/addons/website/static/src/js/website.snippets.js @@ -25,8 +25,8 @@ save: function () { this.snippets.make_active(false); - // TODO TOFIX: call clean_for_save on all snippets of the page, not only modified ones - // important for banner of parallax that changes data automatically. + // FIXME: call clean_for_save on all snippets of the page, not only modified ones + // important for banner of parallax that changes data automatically. this.snippets.clean_for_save(); remove_added_snippet_id(); this._super(); @@ -196,7 +196,7 @@ } self.make_active($target); }); - $("[data-oe-model]").on('click', function (ev) { + $("[data-oe-model]").on('click', function () { if (!snipped_event_flag && self.$active_snipped_id && !self.$active_snipped_id.parents("[data-snippet-id]:first")) { self.make_active(false); } @@ -224,6 +224,7 @@ }, clean_for_save: function () { for (var k in this.snippets) { + if (!this.snippets.hasOwnProperty(k)) { continue; } var editor = $(this.snippets[k]).data("snippet-editor"); if (editor) { editor.clean_for_save(); @@ -341,13 +342,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) { @@ -460,7 +461,7 @@ // count += $zones.length; // $zones.remove(); - $zones = $('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove(); // no recusrive zones + $zones = $('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove(); // no recursive zones count += $zones.length; $zones.remove(); } while (count > 0); @@ -471,14 +472,13 @@ var zone = $(this); var prev = zone.prev(); var next = zone.next(); - var float_prev = zone.prev().css('float') || 'none'; - var float_next = zone.next().css('float') || 'none'; - var disp_prev = zone.prev().css('display') || null; - var disp_next = zone.next().css('display') || null; + var float_prev = prev.css('float') || 'none'; + var float_next = next.css('float') || 'none'; + var disp_prev = prev.css('display') || null; + var disp_next = next.css('display') || null; if( (float_prev === 'left' || float_prev === 'right') && (float_next === 'left' || float_next === 'right') ){ zone.remove(); - return; }else if( !( disp_prev === null || disp_next === null || disp_prev === 'block' @@ -689,7 +689,7 @@ var self = this; var $styles = this.$overlay.find('.oe_options'); var $ul = $styles.find('ul:first'); - _.each(this.parent.style_templates, function (val, key) { + _.each(this.parent.style_templates, function (val) { if (!self.parent.dom_filter(val.selector).is(self.$target)) { return; } @@ -757,7 +757,7 @@ /* * build_snippet - * This method is called just after that a thumbnail is drag and droped into a drop zone + * This method is called just after that a thumbnail is drag and dropped into a drop zone * (after the insertion of this.$body, if this.$body exists) */ build_snippet: function ($target) { @@ -771,7 +771,7 @@ }, /* onFocus - * This method is called when the user click outide the snippet in the dom, after a focus + * This method is called when the user click outside the snippet in the dom, after a focus */ onBlur : function () { this.$overlay.removeClass('oe_active'); @@ -790,7 +790,7 @@ var $ul = this.$editor.find(ul_options); var bg_value = (typeof bg === 'string' ? this.$target.find(bg) : $(bg)).css("background-image").replace(/url\(['"]*|['"]*\)/g, ""); - // bind envent on options + // bind event on options var $li = $ul.find("li"); $li.on('click', function (event) { if ($(this).data("value")) { @@ -893,7 +893,8 @@ var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g'); var cursor = $handle.css("cursor")+'-important'; - $("body").addClass(cursor); + var $body = $(document.body); + $body.addClass(cursor); var body_mousemove = function (event){ event.preventDefault(); @@ -920,15 +921,15 @@ self.parent.cover_target(self.$overlay, self.$target); } }; - $('body').mousemove(body_mousemove); var body_mouseup = function(){ - $('body').unbind('mousemove', body_mousemove); - $('body').unbind('mouseup', body_mouseup); - $("body").removeClass(cursor); + $body.unbind('mousemove', body_mousemove); + $body.unbind('mouseup', body_mouseup); + $body.removeClass(cursor); self.parent.editor_busy = false; }; - $('body').mouseup(body_mouseup); + $body.mousemove(body_mousemove); + $body.mouseup(body_mouseup); }); }, getSize: function () { @@ -998,7 +999,7 @@ if (compass !== 'w') return; - // don't change the rigth border position when we change the offset (replace col size) + // don't change the right border position when we change the offset (replace col size) var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); @@ -1077,14 +1078,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 @@ -1136,7 +1129,6 @@ var style = false; var $el = this.$inner.find('.item.active'); var $ul = this.$editor.find('ul[name="carousel-style"]'); - var $li = $ul.find("li"); if ($el.hasClass('text_only')) style = 'text_only'; @@ -1168,7 +1160,6 @@ }); }, change_size: function () { - var self = this; var $el = this.$target; var size = 'oe_big'; @@ -1205,7 +1196,6 @@ this.change_size(); }, scroll: function(){ - var self = this; var $ul = this.$editor.find('ul[name="parallax-scroll"]'); var $li = $ul.find("li"); var $parallax = this.$target.find('.parallax'); @@ -1215,7 +1205,7 @@ $li.on('click', function (event) { $li.removeClass("active"); $(this).addClass("active"); - var speed = $(this).data('value') + var speed = $(this).data('value'); $parallax.attr('data-stellar-background-ratio', speed); }); 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..876d2a7b5b9 --- /dev/null +++ b/addons/website/static/src/js/website.tour.basic.js @@ -0,0 +1,163 @@ +(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; + var $body = $(document.body); + 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(); + function goToNextStep () { + $('#oe_snippets').hide(); + self.movetoStep('edit-title'); + $body.off('mouseup', goToNextStep); + } + $body.off('mousedown', beginDrag); + $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('
  • '+basicTour.name+'
  • ')); + $menuItem.click(function () { + basicTour.reset(); + basicTour.start(); + }); + menu.append($menuItem); + var url = new website.UrlParser(window.location.href); + if (url.search.indexOf('?tutorial=true') === 0 || basicTour.startOfPart2()) { + basicTour.start(); + } + $('.tour-backdrop').click(function (e) { + e.stopImmediatePropagation(); + e.preventDefault(); + }); + return this._super(); + }, + }); + +}()); diff --git a/addons/website/static/src/js/website.tour.js b/addons/website/static/src/js/website.tour.js index bfb9a0461d9..f5d3237c28d 100644 --- a/addons/website/static/src/js/website.tour.js +++ b/addons/website/static/src/js/website.tour.js @@ -4,9 +4,11 @@ var website = openerp.website; website.templates.push('/website/static/src/xml/website.tour.xml'); - function render (template, dict) { - return openerp.qweb.render(template, dict); - } + website.tour = { + render: function render (template, dict) { + return openerp.qweb.render(template, dict); + } + }; website.EditorTour = openerp.Class.extend({ tour: undefined, @@ -19,7 +21,7 @@ keyboard: false, }); this.tour.addSteps(_.map(this.steps, function (step) { - step.title = render('website.tour_popover_title', { title: step.title }); + step.title = website.tour.render('website.tour_popover_title', { title: step.title }); return step; })); this.monkeyPatchTour(); @@ -81,137 +83,6 @@ }, }); - 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.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('
  • '+tutorial.name+'
  • ')); - $menuItem.click(function () { - tutorial.reset(); - tutorial.start(); - }); - menu.append($menuItem); - }); - var url = new website.UrlParser(window.location.href); - if (url.search.indexOf('?tutorial=true') === 0 || website.tutorials.basic.startOfPart2()) { - website.tutorials.basic.start(); - } - $('.tour-backdrop').click(function (e) { - e.stopImmediatePropagation(); - e.preventDefault(); - }); - return this._super(); - }, - }); - }()); diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js index fddb5ef7b51..7e237598530 100644 --- a/addons/website/static/src/js/website.translator.js +++ b/addons/website/static/src/js/website.translator.js @@ -6,11 +6,16 @@ var nodialog = 'website_translator_nodialog'; website.EditorBar.include({ + events: _.extend({}, website.EditorBar.prototype.events, { + 'click a[data-action=edit_master]': 'edit_master', + }), start: function () { var self = this; this.initial_content = {}; return this._super.apply(this, arguments).then(function () { - self.$('button[data-action=edit]').text("Translate"); + self.$('button[data-action=edit]') + .text("Translate") + .after(openerp.qweb.render('website.TranslatorAdditionalButtons')); self.$('[data-action=snippet]').hide(); self.$('#customize-menu-button').hide(); }); @@ -24,7 +29,7 @@ dialog.on('activate', this, function () { localStorage[nodialog] = dialog.$('input[name=do_not_show]').prop('checked') || ''; dialog.$el.modal('hide'); - this.translate().then(function () { + self.translate().then(function () { mysuper.call(self); }); }); @@ -34,6 +39,14 @@ }); } }, + edit_master: function (ev) { + ev.preventDefault(); + var link = $('.js_language_selector a[data-default-lang]')[0]; + if (link) { + link.search += (link.search ? '&' : '?') + 'enable_editor=1'; + window.location = link.attributes.href.value; + } + }, translate: function () { var self = this; this.translations = null; @@ -44,7 +57,7 @@ self.translations = translations; self.processTranslatableNodes(); // Disable non translatable t-fields - $('[data-oe-type][data-oe-translate=0]').removeAttr('data-oe-type'); + $('[data-oe-type][data-oe-translate="0"]').removeAttr('data-oe-type'); }); }, processTranslatableNodes: function () { @@ -143,7 +156,6 @@ if (node.attributes['data-oe-translate'].value == '1') { node.className += ' oe_translatable_field'; } - return; } else if (node.childNodes.length === 1 && this.isTextNode(node.childNodes[0]) && !node.getAttribute('data-oe-model')) { @@ -175,7 +187,7 @@ $(root).click(function (ev) { ev.preventDefault(); }); - return $('[data-oe-translate=1]'); + return $('[data-oe-translate="1"]'); } }); diff --git a/addons/website/static/src/xml/website.translator.xml b/addons/website/static/src/xml/website.translator.xml index f35cf583da9..dac074688d8 100644 --- a/addons/website/static/src/xml/website.translator.xml +++ b/addons/website/static/src/xml/website.translator.xml @@ -1,5 +1,9 @@ + + or + Edit Master +