diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 88c8b85f321..8b8f82a3678 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -35,7 +35,7 @@ PIL_MIME_MAPPING = {'PNG': 'image/png', 'JPEG': 'image/jpeg', 'GIF': 'image/gif' # Completely arbitrary limits MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT = IMAGE_LIMITS = (1024, 768) class Website(openerp.addons.web.controllers.main.Home): - @website.route('/', type='http', auth="admin") + @website.route('/', type='http', auth="public", multilang=True) def index(self, **kw): return self.page("website.homepage") @@ -107,7 +107,7 @@ class Website(openerp.addons.web.controllers.main.Home): return request.website.render('website.themes', {'theme_changed': True}) - @website.route('/page/', type='http', auth="admin") + @website.route('/page/', type='http', auth="public", multilang=True) def page(self, path, **kwargs): values = { 'path': path, @@ -160,6 +160,51 @@ class Website(openerp.addons.web.controllers.main.Home): }) return result + @website.route('/website/get_view_translations', type='json', auth='admin') + def get_view_translations(self, xml_id, lang=None): + lang = lang or request.context.get('lang') + views = self.customize_template_get(xml_id, optional=False) + views_ids = [view.get('id') for view in views if view.get('active')] + domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)] + irt = request.registry.get('ir.translation') + return irt.search_read(request.cr, request.uid, domain, ['id', 'res_id', 'value'], context=request.context) + + @website.route('/website/set_translations', type='json', auth='admin') + def set_translations(self, data, lang): + irt = request.registry.get('ir.translation') + for view_id, trans in data.items(): + view_id = int(view_id) + for t in trans: + initial_content = t['initial_content'].strip() + new_content = t['new_content'].strip() + tid = t['translation_id'] + if not tid: + old_trans = irt.search_read( + request.cr, request.uid, + [ + ('type', '=', 'view'), + ('res_id', '=', view_id), + ('lang', '=', lang), + ('src', '=', initial_content), + ]) + if old_trans: + tid = old_trans[0]['id'] + if tid: + vals = {'value': new_content} + irt.write(request.cr, request.uid, [tid], vals) + else: + new_trans = { + 'name': 'website', + 'res_id': view_id, + 'lang': lang, + 'type': 'view', + 'source': initial_content, + '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') def attach(self, func, upload): @@ -258,18 +303,17 @@ class Website(openerp.addons.web.controllers.main.Home): pass return request.make_response(image_data, headers) - @website.route(['/website/publish/'], type='http', auth="public") - def publish(self, **post): - _id = int(post['id']) - _object = request.registry[post['object']] + @website.route(['/website/publish'], type='json', auth="public") + def publish(self, id, object): + _id = int(id) + _object = request.registry[object] obj = _object.browse(request.cr, request.uid, _id) _object.write(request.cr, request.uid, [_id], {'website_published': not obj.website_published}, context=request.context) obj = _object.browse(request.cr, request.uid, _id) - - return obj.website_published and "1" or "0" + return obj.website_published and True or False @website.route(['/website/kanban/'], type='http', auth="public") def kanban(self, **post): diff --git a/addons/website/static/src/css/editor.css b/addons/website/static/src/css/editor.css index 64f58c8db11..1fde0c25f3d 100644 --- a/addons/website/static/src/css/editor.css +++ b/addons/website/static/src/css/editor.css @@ -107,6 +107,31 @@ table.editorbar-panel td.selected { background-color: #b1c9d9; } +.oe_translate_examples li { + margin: 10px; + padding: 4px; +} + +.oe_translatable_text { + outline: 1px solid black; +} + +.oe_translatable_field { + outline: 1px dashed black; +} + +.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty { + outline-color: red; +} + +.oe_translatable_text.oe_dirty:empty { + padding: 0 10px; +} + +.oe_translatable_todo { + background: #ffffb6; +} + /* ---- RTE ---- */ .oe_editable .btn { -webkit-user-select: auto; diff --git a/addons/website/static/src/css/editor.sass b/addons/website/static/src/css/editor.sass index 90b02d3b83a..891630b63a4 100644 --- a/addons/website/static/src/css/editor.sass +++ b/addons/website/static/src/css/editor.sass @@ -93,6 +93,22 @@ table.editorbar-panel td.selected background-color: #b1c9d9 +// ---- TRANSLATIONS ---- {{{ +.oe_translate_examples li + margin: 10px + padding: 4px +.oe_translatable_text + outline: 1px solid black +.oe_translatable_field + outline: 1px dashed black +.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty + outline-color: red +.oe_translatable_text.oe_dirty:empty + padding: 0 10px +.oe_translatable_todo + background: rgb(255, 255, 182) +// }}} + /* ---- RTE ---- */ // bootstrap makes .btn elements unselectable -> RTE double-click can't know @@ -485,3 +501,5 @@ $navbar_height: 51px &.oe_ace_closed width: 0 opacity: 0 + +// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 5a064a40a9e..d5342cfdd74 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -337,6 +337,14 @@ footer { padding: 0 0 20px 0; } +/* Parallax Theme */ +.parallax_full { + background: #eeeeee url(/website/static/src/img/parallax/parallax_bg.jpg) bottom center repeat fixed; + min-height: 650px; + width: 100%; + z-index: -999; +} + /* -- Hack for removing double scrollbar from mobile preview -- */ div#mobile-preview.modal { overflow: hidden; @@ -352,27 +360,12 @@ ul.nav-stacked > li > a { } /* ---- PUBLISH ---- */ -a[data-publish] { - text-decoration: none !important; - z-index: 2; -} -a[data-publish] .label { - padding: 5px 8px; -} -a[data-publish] .css_unpublish, a[data-publish] .css_publish, a[data-publish] .css_unpublished, a[data-publish] .css_published { +.dropdown .css_unpublish, .dropdown .css_publish { 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_publish .css_unpublish, .dropdown.css_unpublish .css_publish { + display: block; } .unpublish { diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index cb0e93d9170..25f45e3f7c4 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -255,6 +255,14 @@ footer #themes-list .well padding: 0 0 20px 0 +/* Parallax Theme */ + +.parallax_full + background: #eee url(/website/static/src/img/parallax/parallax_bg.jpg) bottom center repeat fixed + min-height: 650px + width: 100% + z-index: -999 + /* -- Hack for removing double scrollbar from mobile preview -- */ div#mobile-preview.modal overflow: hidden @@ -267,23 +275,11 @@ ul.nav-stacked > li > a text-transform: uppercase /* ---- PUBLISH ---- */ -a[data-publish] - text-decoration: none !important - z-index: 2 - .label - padding: 5px 8px - .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 +.dropdown.css_publish .css_unpublish, .dropdown.css_unpublish .css_publish + display: block .unpublish opacity: 0.5 diff --git a/addons/website/static/src/img/parallax/parallax_bg.jpg b/addons/website/static/src/img/parallax/parallax_bg.jpg new file mode 100644 index 00000000000..aa1dd20f21a Binary files /dev/null and b/addons/website/static/src/img/parallax/parallax_bg.jpg differ diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 41280199e45..8b15422b90d 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -282,7 +282,7 @@ }); }).get(); return $.when.apply(null, defs).then(function () { - window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?|#.*/g, ''); + website.reload(); }); }, /** diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index b26a0a7e76c..bc0db10ce88 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -37,7 +37,9 @@ }); return $.when.apply(null, dones); }; - + website.reload = function () { + window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?|#.*/g, ''); + }; var all_ready = null; var dom_ready = website.dom_ready = $.Deferred(); @@ -78,7 +80,7 @@ $pagination.last().before(col); }); - var page_start = page - parseInt(Math.floor((scope-1)/2)); + var page_start = page - parseInt(Math.floor((scope-1)/2), 10); if (page_start < 1 ) page_start = 1; var page_end = page_start + (scope-1); if (page_end > page_count ) page_end = page_count; @@ -106,29 +108,34 @@ */ website.ready = function() { if (!all_ready) { - all_ready = dom_ready.then(function () { - // TODO: load translations - return website.load_templates(templates); - }); + var tpl = website.load_templates(templates); + // var session; + // var trads = openerp._t.database.load_translations(session, ['website'], website.get_context().lang); + all_ready = $.when(dom_ready, tpl); } return all_ready; }; dom_ready.then(function () { /* ----- PUBLISHING STUFF ---- */ - $('[data-publish]:has(.js_publish)').each(function () { - $(this).attr("data-publish", $(".js_publish li.active", this).size() ? "on" : 'off'); + $('[data-publish]:has(.js_publish_management)').each(function () { + $(this).attr("data-publish", $(".js_publish_management .btn-success", this).size() ? "on" : 'off'); }); - $(document).on('click', '.js_publish a.js_publish_btn', function (e) { - var $li = $(this).parent("li"); - var $data = $li.parents(".js_publish:first"); - var publish = $li.hasClass("active"); - $li.toggleClass("active"); - $.post('/website/publish', {'id': $data.data('id'), 'object': $data.data('object')}, function (result) { - $li.toggleClass("active", !!+result); - $li.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); - }); + $(document).on('click', '.js_publish_management .js_publish_btn', function (e) { + var $data = $(this).parents(".js_publish_management:first"); + var $btn = $data.find('.btn:first'); + var publish = $btn.hasClass("btn-success"); + + $data.toggleClass("css_unpublish css_publish"); + $btn.removeClass("btn-default btn-success"); + + openerp.jsonRpc('/website/publish', 'call', {'id': +$data.data('id'), 'object': $data.data('object')}) + .then(function (result) { + $btn.toggleClass("btn-default", !result).toggleClass("btn-success", result); + $data.toggleClass("css_unpublish", !result).toggleClass("css_publish", result); + $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); + }); }); /* ----- KANBAN WEBSITE ---- */ diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js new file mode 100644 index 00000000000..fddb5ef7b51 --- /dev/null +++ b/addons/website/static/src/js/website.translator.js @@ -0,0 +1,194 @@ +(function () { + 'use strict'; + + var website = openerp.website; + website.templates.push('/website/static/src/xml/website.translator.xml'); + var nodialog = 'website_translator_nodialog'; + + website.EditorBar.include({ + start: function () { + var self = this; + this.initial_content = {}; + return this._super.apply(this, arguments).then(function () { + self.$('button[data-action=edit]').text("Translate"); + self.$('[data-action=snippet]').hide(); + self.$('#customize-menu-button').hide(); + }); + }, + edit: function () { + var self = this; + var mysuper = this._super; + if (!localStorage[nodialog]) { + var dialog = new website.TranslatorDialog(); + dialog.appendTo($(document.body)); + dialog.on('activate', this, function () { + localStorage[nodialog] = dialog.$('input[name=do_not_show]').prop('checked') || ''; + dialog.$el.modal('hide'); + this.translate().then(function () { + mysuper.call(self); + }); + }); + } else { + this.translate().then(function () { + mysuper.call(self); + }); + } + }, + translate: function () { + var self = this; + this.translations = null; + return openerp.jsonRpc('/website/get_view_translations', 'call', { + 'xml_id': $(document.documentElement).data('view-xmlid'), + 'lang': website.get_context().lang, + }).then(function (translations) { + self.translations = translations; + self.processTranslatableNodes(); + // Disable non translatable t-fields + $('[data-oe-type][data-oe-translate=0]').removeAttr('data-oe-type'); + }); + }, + processTranslatableNodes: function () { + var self = this; + var $editables = $('[data-oe-model="ir.ui.view"]') + .not('link, script') + .not('.oe_snippets,.oe_snippet, .oe_snippet *') + .not('[data-oe-type]'); + + $editables.each(function () { + var $node = $(this); + var view_id = $node.attr('data-oe-source-id') || $node.attr('data-oe-id'); + self.transNode(this, view_id|0); + }); + $('.oe_translatable_text').on('paste', function () { + var node = $(this); + setTimeout(function () { + self.sanitizeNode(node); + }, 0); + }); + $(document).on('blur keyup paste', '.oe_translatable_text[contenteditable]', function(ev) { + var $node = $(this); + setTimeout(function () { + // Doing stuff next tick because paste and keyup events are + // fired before the content is changed + if (ev.type == 'paste') { + self.sanitizeNode($node[0]); + } + if (self.getInitialContent($node[0]) !== $node.text()) { + $node.addClass('oe_dirty').removeClass('oe_translatable_todo'); + } + }, 0); + }); + }, + getInitialContent: function (node) { + return this.initial_content[node.attributes['data-oe-nodeid'].value]; + }, + sanitizeNode: function (node) { + node.text(node.text()); + }, + isTextNode: function (node) { + return node.nodeType === 3 || node.nodeType === 4; + }, + isTranslatable: function (text) { + return text && _.str.trim(text) !== ''; + }, + markTranslatableNode: function (node, view_id) { + // TODO: link nodes with same content + node.className += ' oe_translatable_text'; + node.setAttribute('data-oe-translation-view-id', view_id); + var content = node.childNodes[0].data.trim(); + var trans = this.translations.filter(function (t) { + return t.res_id === view_id && t.value === content; + }); + if (trans.length) { + node.setAttribute('data-oe-translation-id', trans[0].id); + } else { + node.className += ' oe_translatable_todo'; + } + node.contentEditable = true; + var nid = _.uniqueId(); + $(node).attr('data-oe-nodeid', nid); + this.initial_content[nid] = content; + }, + save: function () { + var self = this; + var mysuper = this._super; + var trans = {}; + // this._super.apply(this, arguments); + $('.oe_translatable_text.oe_dirty').each(function () { + var $node = $(this); + var data = $node.data(); + if (!trans[data.oeTranslationViewId]) { + trans[data.oeTranslationViewId] = []; + } + trans[data.oeTranslationViewId].push({ + initial_content: self.getInitialContent(this), + new_content: $node.text(), + translation_id: data.oeTranslationId || null + }); + }); + openerp.jsonRpc('/website/set_translations', 'call', { + 'data': trans, + 'lang': website.get_context()['lang'], + }).then(function () { + mysuper.call(self); + }).fail(function () { + // TODO: bootstrap alert with error message + alert("Could not save translation"); + }); + }, + transNode: function (node, view_id) { + // Mostly handling text and cdata nodes here + // so avoid jquery usage in this function + if (node.attributes['data-oe-type']) { + 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')) { + this.markTranslatableNode(node, view_id); + } else { + for (var i = 0, l = node.childNodes.length; i < l; i ++) { + var n = node.childNodes[i]; + if (this.isTextNode(n)) { + if (this.isTranslatable(n.data)) { + var container = document.createElement('span'); + node.insertBefore(container, n); + container.appendChild(n); + this.markTranslatableNode(container, view_id); + } + } else { + this.transNode(n, view_id); + } + } + } + }, + }); + + website.RTE.include({ + start: function () { + this._super.apply(this, arguments); + this.$el.hide(); + }, + fetch_editables: function (root) { + $(root).click(function (ev) { + ev.preventDefault(); + }); + return $('[data-oe-translate=1]'); + } + }); + + website.TranslatorDialog = openerp.Widget.extend({ + events: _.extend({}, website.EditorBar.prototype.events, { + 'hidden.bs.modal': 'destroy', + 'click button[data-action=activate]': function (ev) { + this.trigger('activate'); + }, + }), + template: 'website.TranslatorDialog', + start: function () { + this.$el.modal(); + }, + }); +})(); diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/website/static/src/xml/website.snippets.xml index 5a1ad2db4c5..987f824432b 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/website/static/src/xml/website.snippets.xml @@ -60,11 +60,13 @@
  • Structure
  • Content
  • Business
  • +
  • Effects
  • +
    @@ -369,12 +371,16 @@
    Title
    -
    -
    -

    Your Website Title

    -

    Great products for great people

    +
    +
    +
    +
    +

    Your Website Title

    +

    Great products for great people

    +
    +
    -
    +
    @@ -789,4 +795,22 @@
    +
    +
    Parallax
    +
    +
    +
    +
    +

    + Our References +

    +

    + More than 500 happy customers. +

    +
    +
    +
    +
    +
    + diff --git a/addons/website/static/src/xml/website.translator.xml b/addons/website/static/src/xml/website.translator.xml new file mode 100644 index 00000000000..f35cf583da9 --- /dev/null +++ b/addons/website/static/src/xml/website.translator.xml @@ -0,0 +1,53 @@ + + + + + + diff --git a/addons/website/static/src/xml/website.xml b/addons/website/static/src/xml/website.xml index 04c2403eda2..c2f7abe6ed2 100644 --- a/addons/website/static/src/xml/website.xml +++ b/addons/website/static/src/xml/website.xml @@ -22,7 +22,6 @@
  • Promote
  • -
  • Translate
  • @@ -257,8 +260,8 @@

                         
                     
                   
    @@ -281,7 +284,7 @@
                                     

    We'll do our best to get back to you as soon as possible.

    @@ -338,13 +341,17 @@ -