diff options
34 files changed, 511 insertions, 83 deletions
diff --git a/addons/account/static/src/scss/account_reconciliation.scss b/addons/account/static/src/scss/account_reconciliation.scss index baae6a584f1..6c21d14e8f2 100644 --- a/addons/account/static/src/scss/account_reconciliation.scss +++ b/addons/account/static/src/scss/account_reconciliation.scss @@ -342,3 +342,19 @@ } } } + +// This is rtl language specific fix +// It will flip the fa-fa play icon in left direction +.o_rtl { + .o_reconciliation { + .o_reconciliation_line { + .toggle_match, .toggle_create { + transform: rotate(180deg); + transition: transform 300ms; + } + .visible_toggle, &[data-mode="match"] .toggle_match, &[data-mode="create"] .toggle_create { + transform: rotate(270deg); + } + } + } +}
\ No newline at end of file diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml index ec1f4e1d475..715884304fb 100644 --- a/addons/delivery/views/delivery_view.xml +++ b/addons/delivery/views/delivery_view.xml @@ -290,8 +290,8 @@ </div> <div> <field name='delivery_price' widget='monetary' class="oe_inline" options="{'currency_field': 'currency_id'}" nolabel="1" attrs="{'invisible': [('carrier_id','=', False)]}" force_save="1"/> - <button name="get_delivery_price" string="Get rate" type="object" class="oe_inline fa fa-arrow-right oe_link" attrs="{'invisible':['|',('carrier_id','=',False),('state','not in',('draft','sent'))]}"/> - <button name="set_delivery_line" string="Add to order" type="object" class="oe_inline fa fa-arrow-right oe_link" attrs="{'invisible':['|','|',('carrier_id','=',False),('state','not in',('draft','sent')),('delivery_rating_success','=',False)]}"/> + <button name="get_delivery_price" string="Get rate" type="object" class="oe_inline oe_link" attrs="{'invisible':['|',('carrier_id','=',False),('state','not in',('draft','sent'))]}"><i class="fa fa-arrow-right"/></button> + <button name="set_delivery_line" string="Add to order" type="object" class="oe_inline oe_link" attrs="{'invisible':['|','|',('carrier_id','=',False),('state','not in',('draft','sent')),('delivery_rating_success','=',False)]}"><i class="fa fa-arrow-right"/></button> <field name='delivery_rating_success' invisible="1" force_save="1"/> </div> <div class="alert alert-info" role="status" attrs="{'invisible': ['|',('carrier_id','=', False),('delivery_message','=',False)]}"> diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 3e3555516c5..3179986e88a 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -230,7 +230,9 @@ var LivechatButton = Widget.extend({ }; this._chatWindow = new WebsiteLivechatWindow(this, this._livechat, options); this._chatWindow.appendTo($('body')).then(function () { - self._chatWindow.$el.css({right: 0, bottom: 0}); + var cssProps = {bottom: 0}; + cssProps[_t.database.parameters.direction === 'rtl' ? 'left' : 'right'] = 0; + self._chatWindow.$el.css(cssProps); self.$el.hide(); }); this._chatWindow.on('close', this, function () { diff --git a/addons/mail/static/src/js/services/mail_window_manager.js b/addons/mail/static/src/js/services/mail_window_manager.js index d65675c42a7..733ca366a95 100644 --- a/addons/mail/static/src/js/services/mail_window_manager.js +++ b/addons/mail/static/src/js/services/mail_window_manager.js @@ -9,6 +9,7 @@ var core = require('web.core'); var utils = require('web.utils'); var QWeb = core.qweb; +var _t = core._t; var THREAD_WINDOW_WIDTH = 325 + 5; // 5 pixels between windows @@ -518,7 +519,9 @@ MailManager.include({ var index = 0; while (index < count && index < this._threadWindows.length) { var threadWindow = this._threadWindows[index]; - threadWindow.$el.css({ right: THREAD_WINDOW_WIDTH*index, bottom: 0 }); + var cssProps = {bottom: 0}; + cssProps[_t.database.parameters.direction === 'rtl' ? 'left' : 'right'] = THREAD_WINDOW_WIDTH*index; + threadWindow.$el.css(cssProps); threadWindow.do_show(); index++; } diff --git a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js index 5b354ed2aa8..8ec8aa84b31 100644 --- a/addons/mail/static/src/js/thread_windows/abstract_thread_window.js +++ b/addons/mail/static/src/js/thread_windows/abstract_thread_window.js @@ -86,7 +86,8 @@ var AbstractThreadWindow = Widget.extend({ this._focusInput(); } if (!config.device.isMobile) { - this.$el.css('margin-right', $.position.scrollbarWidth()); + var margin_dir = _t.database.parameters.direction === "rtl" ? "margin-left" : "margin-right"; + this.$el.css(margin_dir, $.position.scrollbarWidth()); } var def = this.threadWidget.replace(this.$('.o_thread_window_content')); return $.when(this._super(), def); diff --git a/addons/mail/static/src/scss/abstract_thread_window.scss b/addons/mail/static/src/scss/abstract_thread_window.scss index 02d41948f12..127b7b8adaf 100644 --- a/addons/mail/static/src/scss/abstract_thread_window.scss +++ b/addons/mail/static/src/scss/abstract_thread_window.scss @@ -1,6 +1,7 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default; .o_thread_window { + direction: ltr; display: flex; flex-flow: column nowrap; position: fixed; diff --git a/addons/mail/static/src/scss/systray.scss b/addons/mail/static/src/scss/systray.scss index 1383d462a38..542e6ded473 100644 --- a/addons/mail/static/src/scss/systray.scss +++ b/addons/mail/static/src/scss/systray.scss @@ -27,6 +27,7 @@ font-size: 0.7em; } .o_mail_systray_dropdown { + direction: ltr; width: 350px; padding: 0; diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 904e5f1b432..2510c25656e 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -110,6 +110,7 @@ td { } .pos { + direction: ltr; padding: 0; margin: 0; background-color: #f0eeee; @@ -1431,6 +1432,7 @@ td { .pos .pos-receipt-container { font-size: 0.75em; text-align: center; + direction: ltr; } .pos .pos-sale-ticket { diff --git a/addons/point_of_sale/views/point_of_sale.xml b/addons/point_of_sale/views/point_of_sale.xml index 85c7e310eec..e6428ca8611 100644 --- a/addons/point_of_sale/views/point_of_sale.xml +++ b/addons/point_of_sale/views/point_of_sale.xml @@ -6,6 +6,10 @@ <link rel="stylesheet" href="/point_of_sale/static/src/css/pos.css" id="pos-stylesheet"/> <link rel="stylesheet" href="/point_of_sale/static/src/css/keyboard.css"/> + <t t-if="request and request.env['res.lang'].search([('code', '=', request.env.lang)]).direction == 'rtl'"> + <link rel="stylesheet" type="text/less" href="/web/static/src/scss/fontawesome_overriden.scss"/> + </t> + <script type="text/javascript" src="/point_of_sale/static/lib/waitfont.js"></script> <script type="text/javascript" src="/point_of_sale/static/src/js/main.js"></script> <script type="text/javascript" src="/point_of_sale/static/src/js/db.js"></script> diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 840b44275e6..cd8e774171f 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -22,7 +22,8 @@ </t> <t t-set="head" t-value="head_website + (head or '')"/> - <div id="wrapwrap" t-att-class="pageName or ''"> + <t t-set="direction" t-value="request.env['res.lang'].search([('code', '=', request.env.lang)]).direction"/> + <div id="wrapwrap" t-att-class="(pageName or '') + ' ' + (direction == 'rtl' and 'o_rtl' or '')"> <header> <nav class="navbar navbar-expand-md navbar-light bg-light"> <div class="container"> diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index f88d96e4223..2ada433fbc8 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -111,6 +111,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi return session.is_bound .then(function () { + self.$el.toggleClass('o_rtl', _t.database.parameters.direction === "rtl"); self.bind_events(); return $.when( self.set_action_manager(), diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 94144cb25ec..7e3cb6128b9 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -22,6 +22,7 @@ odoo.define('web.dom', function (require) { var config = require('web.config'); var core = require('web.core'); +var _t = core._t; /** * Private function to notify that something has been attached in the DOM @@ -90,14 +91,15 @@ return { minHeight = (options && options.min_height) || 50; $fixedTextarea = $('<textarea disabled>', { - class: $textarea[0].className, - }).css({ + class: $textarea[0].className,}); + + var direction = _t.database.parameters.direction === 'rtl' ? 'right' : 'left'; + $fixedTextarea.css({ position: 'absolute', opacity: 0, height: 10, top: -10000, - left: -10000, - }); + }).css(direction, -10000); $fixedTextarea.data("auto_resize", true); var style = window.getComputedStyle($textarea[0], null); diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index ffb4f41ad6b..e76a487b73b 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -413,6 +413,7 @@ return AbstractRenderer.extend({ }, height: 'parent', unselectAuto: false, + isRTL: _t.database.parameters.direction === "rtl", }); this.$calendar.fullCalendar(fc_options); diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index 945cc132b03..94f4f3c1876 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -164,8 +164,11 @@ var PivotController = AbstractController.extend({ fields: fields })); + var cssProps = {top: top}; + cssProps[_t.database.parameters.direction === 'rtl' ? 'right' : 'left'] = + _t.database.parameters.direction === 'rtl' ? this.$el.width() - left : left; this.$fieldSelection.find('.dropdown-menu').first() - .css({top: top, left: left}) + .css(cssProps) .addClass('show'); }, /** diff --git a/addons/web/static/src/scss/datepicker.scss b/addons/web/static/src/scss/datepicker.scss index c4df0ca2ed2..a9642c91df4 100644 --- a/addons/web/static/src/scss/datepicker.scss +++ b/addons/web/static/src/scss/datepicker.scss @@ -79,3 +79,15 @@ div.dropdown-menu.bootstrap-datetimepicker-widget{ } } } + +// This is rtl language specific fix +// it will fix the user agent stylesheet padding +// and transform the prev and next icons +.o_rtl { + .bootstrap-datetimepicker-widget .list-unstyled { + padding: 0; + .prev , .next { + transform: rotate(180deg); + } + } +} diff --git a/addons/web/static/src/scss/dropdown.scss b/addons/web/static/src/scss/dropdown.scss index bec4b288f9e..105c3afedbf 100644 --- a/addons/web/static/src/scss/dropdown.scss +++ b/addons/web/static/src/scss/dropdown.scss @@ -81,3 +81,9 @@ .navbar-nav { @include dropdown-menu-mixin; } + +.o_rtl { + .dropdown-menu { + left: auto; + } +}
\ No newline at end of file diff --git a/addons/web/static/src/scss/fontawesome_overriden.scss b/addons/web/static/src/scss/fontawesome_overriden.scss new file mode 100644 index 00000000000..5fde7a750cd --- /dev/null +++ b/addons/web/static/src/scss/fontawesome_overriden.scss @@ -0,0 +1,36 @@ +// This is rtl language specific fix +// It will override the font awesome symbols and flip them 180 degree +.fa { + &.fa-align-right, + &.fa-align-left, + &.fa-chevron-right, + &.fa-chevron-left, + &.fa-arrow-right, + &.fa-arrow-left, + &.fa-hand-o-right, + &.fa-hand-o-left, + &.fa-arrow-circle-right, + &.fa-arrow-circle-left, + &.fa-caret-right, + &.fa-caret-left, + &.fa-rotate-right, + &.fa-rotate-left, + &.fa-angle-double-right, + &.fa-angle-double-left, + &.fa-angle-right, + &.fa-angle-left, + &.fa-quote-right, + &.fa-quote-left, + &.fa-chevron-circle-right, + &.fa-chevron-circle-left, + &.fa-long-arrow-right, + &.fa-long-arrow-left, + &.fa-toggle-right, + &.fa-toggle-left, + &.fa-caret-square-o-right, + &.fa-arrow-circle-o-left, + &.fa-arrow-circle-o-right, + &.fa-caret-square-o-left { + transform: rotate(180deg); + } +} diff --git a/addons/web/static/src/scss/graph_view.scss b/addons/web/static/src/scss/graph_view.scss index b30e066b278..900299949e9 100644 --- a/addons/web/static/src/scss/graph_view.scss +++ b/addons/web/static/src/scss/graph_view.scss @@ -1,5 +1,6 @@ .o_graph { .o_graph_svg_container { + direction: ltr#{"/*rtl:ignore*/"}; height: 100%; overflow: auto; svg { diff --git a/addons/web/static/src/scss/report.scss b/addons/web/static/src/scss/report.scss index cce5c6d3be5..8e2986e331d 100644 --- a/addons/web/static/src/scss/report.scss +++ b/addons/web/static/src/scss/report.scss @@ -1,5 +1,6 @@ html, body { height: 100%; + direction: ltr; } body { diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss index 15071a156b8..fc1d84d8247 100644 --- a/addons/web/static/src/scss/search_view.scss +++ b/addons/web/static/src/scss/search_view.scss @@ -36,6 +36,7 @@ } .o_facet_values { + direction: ltr#{"/*rtl:ignore*/"}; padding: 0 $o-searchview-facet-remove-width 0 5px; .o_facet_values_sep { font-style: italic; diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss index d8c68853c0a..7d28bf5ccae 100644 --- a/addons/web/static/src/scss/ui.scss +++ b/addons/web/static/src/scss/ui.scss @@ -23,3 +23,19 @@ } } } + +// This is rtl language specific fix +// It will fix the extra space in ui-autocomplete class +// and flip the next and previous symbols of jquery ui. +.o_rtl { + .ui-autocomplete { + direction: ltr; + right: 0; + left: auto; + } + .ui-datepicker-next, .ui-datepicker-prev { + -webkit-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); + } +} diff --git a/addons/web/static/src/scss/webclient.scss b/addons/web/static/src/scss/webclient.scss index 8c213a57682..7b7162d986a 100644 --- a/addons/web/static/src/scss/webclient.scss +++ b/addons/web/static/src/scss/webclient.scss @@ -6,6 +6,7 @@ // General // ------------------------------------------------------------------ .o_web_client { + direction: ltr; position: relative; // normally useless but required by bootstrap-datepicker background-color: $o-webclient-background-color; } diff --git a/addons/web/static/src/scss/webclient_extra.scss b/addons/web/static/src/scss/webclient_extra.scss index 80e9390c534..7c11ac32a84 100644 --- a/addons/web/static/src/scss/webclient_extra.scss +++ b/addons/web/static/src/scss/webclient_extra.scss @@ -44,3 +44,9 @@ select { @include o-text-overflow; max-width: 150px; } + +.o_rtl { + .navbar-right { + padding: 0; // fix for user agent stylesheet -webkit-padding-start: 40px in rtl language + } +} diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 924c5353b9d..dd43d18e79d 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -62,6 +62,10 @@ <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/banner.scss"/> <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/colorpicker.scss"/> + <t t-if="request and request.env['res.lang'].search([('code', '=', request.env.lang)]).direction == 'rtl'"> + <link rel="stylesheet" type="text/less" href="/web/static/src/scss/fontawesome_overriden.scss"/> + </t> + <script type="text/javascript" src="/web/static/lib/es5-shim/es5-shim.min.js"></script> <script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script> <script type="text/javascript" src="/web/static/lib/underscore.string/lib/underscore.string.js"></script> diff --git a/addons/web_editor/views/iframe.xml b/addons/web_editor/views/iframe.xml index 4c496da0d31..100cb1c1fbb 100644 --- a/addons/web_editor/views/iframe.xml +++ b/addons/web_editor/views/iframe.xml @@ -59,7 +59,8 @@ </script> <t t-raw="head or ''"/> </head> - <body id="web_editor_inside_iframe" onload="var fn=window.top.odoo && window.top.odoo[callback+'_content']; if(fn) {fn();} else {console.warn('Please don\'t open iframe without the odoo backend');}"> + <t t-set="direction" t-value="request.env['res.lang'].search([('code', '=', request.env.lang)]).direction"/> + <body id="web_editor_inside_iframe" onload="var fn=window.top.odoo && window.top.odoo[callback+'_content']; if(fn) {fn();} else {console.warn('Please don\'t open iframe without the odoo backend');}" t-attf-style="direction: #{direction};"> <div id="wrapwrap"> <main> <t t-raw="0"/> diff --git a/addons/web_tour/static/src/js/tip.js b/addons/web_tour/static/src/js/tip.js index 79a4d1cf11e..2ee188ddd2a 100644 --- a/addons/web_tour/static/src/js/tip.js +++ b/addons/web_tour/static/src/js/tip.js @@ -4,6 +4,7 @@ odoo.define('web_tour.Tip', function(require) { var config = require('web.config'); var core = require('web.core'); var Widget = require('web.Widget'); +var _t = core._t; var Tip = Widget.extend({ template: "Tip", @@ -139,19 +140,27 @@ var Tip = Widget.extend({ if (this.tip_opened) return; this.$el.removeClass("o_animated"); + // Reverse left/right position if direction is right to left + var appendAt = this.info.position; + if (_t.database.parameters.direction === 'rtl') { + appendAt = appendAt === 'right' ? 'left': 'right'; + } this.$el.position({ - my: this._get_spaced_inverted_position(this.info.position), - at: this.info.position, + my: this._get_spaced_inverted_position(appendAt), + at: appendAt, of: this.$anchor, collision: "none", }); + // Reverse overlay if direction is right to left + var positionRight = _t.database.parameters.direction === 'rtl' ? "right" : "left"; + var positionLeft = _t.database.parameters.direction === 'rtl' ? "left" : "right"; var offset = this.$el.offset(); this.$tooltip_overlay.css({ top: -Math.min((this.info.position === "bottom" ? this.info.space : this.info.overlay.y), offset.top), - right: -Math.min((this.info.position === "left" ? this.info.space : this.info.overlay.x), this.$window.width() - (offset.left + this.init_width + this.double_border_width)), + right: -Math.min((this.info.position === positionRight ? this.info.space : this.info.overlay.x), this.$window.width() - (offset.left + this.init_width + this.double_border_width)), bottom: -Math.min((this.info.position === "top" ? this.info.space : this.info.overlay.y), this.$window.height() - (offset.top + this.init_height + this.double_border_width)), - left: -Math.min((this.info.position === "right" ? this.info.space : this.info.overlay.x), offset.left), + left: -Math.min((this.info.position === positionLeft ? this.info.space : this.info.overlay.x), offset.left), }); this.position = this.$el.position(); @@ -217,7 +226,7 @@ var Tip = Widget.extend({ } else { overflow = (offset.top + this.content_height + this.double_border_width + this.info.overlay.y > this.$window.height()); } - if (posVertical && overflow || this.info.position === "left") { + if (posVertical && overflow || this.info.position === "left" || (_t.database.parameters.direction === 'rtl' && this.info.position == "right")) { mbLeft -= (this.content_width - this.init_width); } if (!posVertical && overflow || this.info.position === "top") { diff --git a/addons/web_tour/static/src/scss/tip.scss b/addons/web_tour/static/src/scss/tip.scss index bb11d516c01..5dbdc272329 100644 --- a/addons/web_tour/static/src/scss/tip.scss +++ b/addons/web_tour/static/src/scss/tip.scss @@ -54,7 +54,9 @@ $o-tip-arrow-delay-out: floor($o-tip-duration-out / 2); box-sizing: content-box; + /*rtl:begin:ignore*/ @include o-position-absolute($top: 50%, $left: 50%); + /*rtl:end:ignore*/ opacity: 0; width: $o-tip-size; height: $o-tip-size; @@ -89,7 +91,9 @@ border-right-color: $o-tip-color; } &.top::before { + /*rtl:begin:ignore*/ @include o-position-absolute($bottom: -$o-tip-arrow-length+$o-tip-arrow-integration, $left: ($o-tip-size - $o-tip-arrow-width) / 2); + /*rtl:end:ignore*/ border-width: $o-tip-arrow-length $o-tip-arrow-width/2 0 $o-tip-arrow-width/2; border-top-color: $o-tip-color; } @@ -99,7 +103,9 @@ border-left-color: $o-tip-color; } &.bottom::before { + /*rtl:begin:ignore*/ @include o-position-absolute($top: -$o-tip-arrow-length+$o-tip-arrow-integration, $left: ($o-tip-size - $o-tip-arrow-width) / 2); + /*rtl:end:ignore*/ border-width: 0 $o-tip-arrow-width/2 $o-tip-arrow-length $o-tip-arrow-width/2; border-bottom-color: $o-tip-color; } @@ -169,6 +175,7 @@ display: block; } > .o_tooltip_content { + direction: ltr; visibility: visible; transition: visibility 0s ease $o-tip-duration-in; } @@ -191,7 +198,9 @@ border-top-color: $o-tip-color; } &::after { + /*rtl:begin:ignore*/ @include o-position-absolute($bottom: -$o-tip-arrow-length+$o-tip-border-width*2, $left: ($o-tip-size - $o-tip-arrow-width) / 2); + /*rtl:end:ignore*/ border-width: $o-tip-arrow-length $o-tip-arrow-width/2 0 $o-tip-arrow-width/2; border-top-color: white; transition: border-top-color $o-tip-white-arrow-duration-in ease $o-tip-white-arrow-delay-in; @@ -215,7 +224,9 @@ border-bottom-color: $o-tip-color; } &::after { + /*rtl:begin:ignore*/ @include o-position-absolute($top: -$o-tip-arrow-length+$o-tip-border-width*2, $left: ($o-tip-size - $o-tip-arrow-width) / 2); + /*rtl:end:ignore*/ border-width: 0 $o-tip-arrow-width/2 $o-tip-arrow-length $o-tip-arrow-width/2; border-bottom-color: white; transition: border-bottom-color $o-tip-white-arrow-duration-in ease $o-tip-white-arrow-delay-in; @@ -232,8 +243,8 @@ } &.top, &.bottom { &::before, &.active::after { - left: auto; - right: ($o-tip-size - $o-tip-arrow-width) / 2; + left: auto#{"/*rtl:ignore*/"}; + right: ($o-tip-size - $o-tip-arrow-width) / 2#{"/*rtl:ignore*/"}; } } } diff --git a/addons/website/static/src/scss/website.backend.scss b/addons/website/static/src/scss/website.backend.scss index 16dbe353ed2..4945b670db1 100644 --- a/addons/website/static/src/scss/website.backend.scss +++ b/addons/website/static/src/scss/website.backend.scss @@ -166,6 +166,7 @@ } .o_graph_sales { + direction: ltr#{"/*rtl:ignore*/"}; .o_legend0 { background-color: $o-brand-primary; } diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss index 91e6675c82f..2bdfa2632b5 100644 --- a/addons/website/static/src/scss/website.ui.scss +++ b/addons/website/static/src/scss/website.ui.scss @@ -5,6 +5,7 @@ // LAYOUTING body.o_connected_user { + direction: ltr; padding-top: $o-navbar-height!important; } diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 0403f9d99fb..0eaf0e93bca 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -158,8 +158,8 @@ <h3 class="card-header">About This Community</h3> <div class="card-body"> <span t-field="forum.description"/> - <a t-if="not header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}/faq" class="fa fa-arrow-right"> Read Guidelines</a> - <a t-if="header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}" class="fa fa-arrow-right"> Back to <span t-field="forum.name"/></a> + <a t-if="not header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}/faq"><i class="fa fa-arrow-right"/> Read Guidelines</a> + <a t-if="header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}"><i class="fa fa-arrow-right"/> Back to <span t-field="forum.name"/></a> </div> </div> <div class="card" id="moderation_tools" t-if="user.karma>=forum.karma_moderate"> @@ -1413,7 +1413,7 @@ </div> <t t-if="uid == user.id"> <div> - <a class="fa fa-arrow-right" t-attf-href="/forum/#{slug(forum)}/user/#{slug(user)}/edit">Edit Your Profile</a> + <a t-attf-href="/forum/#{slug(forum)}/user/#{slug(user)}/edit"><i class="fa fa-arrow-right"/>Edit Your Profile</a> </div> </t> </div> diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv index bea539d9325..e1f8ff52642 100644 --- a/odoo/addons/base/data/res.lang.csv +++ b/odoo/addons/base/data/res.lang.csv @@ -1,7 +1,7 @@ "id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","translatable","week_start" "base.lang_en","English","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","True","7" "base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","True","7" -"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar","Left-to-Right","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" +"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar","Right-to-Left","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6" "base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","True","1" "base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","7" "base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","True","1" @@ -29,7 +29,7 @@ base.lang_de_CH,"German (CH) / Deutsch (CH)",de_CH,de_CH,"Left-to-Right","[3,3]",".","'","%d.%m.%Y","%H:%M:%S","True","1" "base.lang_el_GR","Greek / Ελληνικά","el_GR","el_GR","Left-to-Right","[]",",",".","%d/%m/%Y","%I:%M:%S %p","True","1" "base.lang_gu_IN","Gujarati / ગુજરાતી","gu_IN","gu","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","True","7" -"base.lang_he_IL","Hebrew / עִבְרִי","he_IL","he","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7" +"base.lang_he_IL","Hebrew / עִבְרִי","he_IL","he","Right-to-Left","[3,0]",".",",","%d/%m/%Y","%H:%M:%S","True","7" "base.lang_hi_IN","Hindi / हिंदी","hi_IN","hi","Left-to-Right","[]",".",",","%A %d %b %Y","%I:%M:%S","True","7" "base.lang_hu","Hungarian / Magyar","hu_HU","hu","Left-to-Right","[3,0]",",",".","%Y-%m-%d","%H.%M.%S","True","1" "base.lang_id","Indonesian / Bahasa Indonesia","id_ID","id","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7" @@ -46,7 +46,7 @@ base.lang_de_CH,"German (CH) / Deutsch (CH)",de_CH,de_CH,"Left-to-Right","[3,3]" "base.lang_mn","Mongolian / монгол","mn_MN","mn","Left-to-Right","[3,0]",",",".","%Y.%m.%d","%H:%M:%S","True","7" base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",","%Y %b %d %A","%I:%M:%S %p","True","7" "base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d. %b %Y","%H:%M:%S","True","1" -"base.lang_fa_IR","Persian / فارس","fa_IR","fa","Left-to-Right","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6" +"base.lang_fa_IR","Persian / فارس","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6" "base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1" "base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7" "base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1" diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py index 1cd5997cc77..2cec27197e9 100644 --- a/odoo/addons/base/models/assetsbundle.py +++ b/odoo/addons/base/models/assetsbundle.py @@ -98,15 +98,18 @@ class AssetsBundle(object): self.css_errors = [] self._checksum = None self.files = files + self.user_direction = self.env['res.lang'].search( + [('code', '=', (self.env.context.get('lang') or self.env.user.lang))] + ).direction for f in files: if f['atype'] == 'text/sass': - self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) + self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction)) elif f['atype'] == 'text/scss': - self.stylesheets.append(ScssStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) + self.stylesheets.append(ScssStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction)) elif f['atype'] == 'text/less': - self.stylesheets.append(LessStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) + self.stylesheets.append(LessStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction)) elif f['atype'] == 'text/css': - self.stylesheets.append(StylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'])) + self.stylesheets.append(StylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction)) elif f['atype'] == 'text/javascript': self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])) @@ -211,7 +214,10 @@ class AssetsBundle(object): """ ira = self.env['ir.attachment'] domain = [ - ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.name, type)), # The wilcards are id, version and pagination number (if any) + ('url', '=like', '/web/content/%-%/{0}{1}%.{2}'.format( # The wilcards are id, version and pagination number (if any) + ('rtl/' if type == 'css' and self.user_direction == 'rtl' else ''), + self.name, + type)), '!', ('url', '=like', '/web/content/%-{}/%'.format(self.version)) ] @@ -229,7 +235,13 @@ class AssetsBundle(object): by file name and only return the one with the max id for each group. """ version = "%" if ignore_version else self.version - url_pattern = '/web/content/%-{0}/{1}{2}.{3}'.format(version, self.name, '.%' if type == 'css' else '', type) + url_pattern = '/web/content/%-{0}/{1}{2}{3}.{4}'.format( + version, + ('rtl/' if type == 'css' and self.user_direction == 'rtl' else ''), + self.name, + '.%' if type == 'css' else '', + type + ) self.env.cr.execute(""" SELECT max(id) FROM ir_attachment @@ -245,7 +257,15 @@ class AssetsBundle(object): assert type in ('js', 'css') ira = self.env['ir.attachment'] - fname = '%s%s.%s' % (self.name, ('' if inc is None else '.%s' % inc), type) + # Set user direction in name to store two bundles + # 1 for ltr and 1 for rtl, this will help during cleaning of assets bundle + # and allow to only clear the current direction bundle + # (this applies to css bundles only) + fname = '%s%s.%s' % ( + self.name, + ('' if inc is None else '.%s' % inc), + type + ) mimetype = 'application/javascript' if type == 'js' else 'text/css' values = { 'name': "/web/content/%s" % type, @@ -259,7 +279,7 @@ class AssetsBundle(object): } attachment = ira.sudo().create(values) - url = '/web/content/%s-%s/%s' % (attachment.id, self.version, fname) + url = '/web/content/%s-%s%s/%s' % (attachment.id, self.version, ('/rtl' if type == 'css' and self.user_direction == 'rtl' else ''), fname) values = { 'name': url, 'url': url, @@ -351,7 +371,11 @@ class AssetsBundle(object): def is_css_preprocessed(self): preprocessed = True attachments = None - for atype in (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset): + asset_types = [SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset] + if self.user_direction == 'rtl': + asset_types.append(StylesheetAsset) + + for atype in asset_types: outdated = False assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype)) if assets: @@ -378,47 +402,57 @@ class AssetsBundle(object): def preprocess_css(self, debug=False, old_attachments=None): """ Checks if the bundle contains any sass/less content, then compiles it to css. + If user language direction is Right to Left then consider css files to call run_rtlcss, + css files are also stored in ir.attachment after processing done by rtlcss. Returns the bundle's flat css. """ - for atype in (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset): - assets = [asset for asset in self.stylesheets if isinstance(asset, atype)] - if assets: - source = '\n'.join([asset.get_source() for asset in assets]) - compiled = self.compile_css(assets[0].compile, source) - - if not self.css_errors and old_attachments: - old_attachments.unlink() - - fragments = self.rx_css_split.split(compiled) - at_rules = fragments.pop(0) - if at_rules: - # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible - self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules)) - while fragments: - asset_id = fragments.pop(0) - asset = next(asset for asset in self.stylesheets if asset.id == asset_id) - asset._content = fragments.pop(0) - - if debug: - try: - fname = os.path.basename(asset.url) - url = asset.html_url - with self.env.cr.savepoint(): - self.env['ir.attachment'].sudo().create(dict( - datas=base64.b64encode(asset.content.encode('utf8')), - mimetype='text/css', - type='binary', - name=url, - url=url, - datas_fname=fname, - res_model=False, - res_id=False, - )) - - if self.env.context.get('commit_assetsbundle') is True: - self.env.cr.commit() - except psycopg2.Error: - pass + if self.stylesheets: + compiled = "" + for atype in (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset): + assets = [asset for asset in self.stylesheets if isinstance(asset, atype)] + if assets: + source = '\n'.join([asset.get_source() for asset in assets]) + compiled = self.compile_css(assets[0].compile, source) + + # We want to run rtlcss on normal css, so merge it in compiled + if self.user_direction == 'rtl': + stylesheet_assets = [asset for asset in self.stylesheets if not isinstance(asset, (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset))] + compiled += '\n'.join([asset.get_source() for asset in stylesheet_assets]) + compiled = self.run_rtlcss(compiled) + + if not self.css_errors and old_attachments: + old_attachments.unlink() + + fragments = self.rx_css_split.split(compiled) + at_rules = fragments.pop(0) + if at_rules: + # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible + self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules)) + while fragments: + asset_id = fragments.pop(0) + asset = next(asset for asset in self.stylesheets if asset.id == asset_id) + asset._content = fragments.pop(0) + + if debug: + try: + fname = os.path.basename(asset.url) + url = asset.html_url + with self.env.cr.savepoint(): + self.env['ir.attachment'].sudo().create(dict( + datas=base64.b64encode(asset.content.encode('utf8')), + mimetype='text/css', + type='binary', + name=url, + url=url, + datas_fname=fname, + res_model=False, + res_id=False, + )) + + if self.env.context.get('commit_assetsbundle') is True: + self.env.cr.commit() + except psycopg2.Error: + pass return '\n'.join(asset.minify() for asset in self.stylesheets) @@ -459,6 +493,41 @@ class AssetsBundle(object): return compiled + def run_rtlcss(self, source): + rtlcss = 'rtlcss' + cmd = [rtlcss, '-'] + + + try: + rtlcss = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + except Exception: + + # Check the presence of rtlcss, if rtlcss not available then we should return normal less file + try: + process = Popen( + ['rtlcss', '--version'], stdout=PIPE, stderr=PIPE + ) + except (OSError, IOError): + _logger.warning('You need rtlcss to convert css file to right to left compatiblity.') + return source + + msg = "Could not execute command %r" % cmd[0] + _logger.error(msg) + self.css_errors.append(msg) + return '' + + result = rtlcss.communicate(input=source.encode('utf-8')) + if rtlcss.returncode: + cmd_output = ''.join(misc.ustr(result)) + if not cmd_output: + cmd_output = "Process exited with return code %d\n" % rtlcss.returncode + error = self.get_rtlcss_error(cmd_output, source=source) + _logger.warning(error) + self.css_errors.append(error) + return '' + rtlcss_result = result[0].strip().decode('utf8') + return rtlcss_result + def get_preprocessor_error(self, stderr, source=None): """Improve and remove sensitive information from sass/less compilator error messages""" error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '') @@ -471,6 +540,11 @@ class AssetsBundle(object): error += '\n - %s' % (asset.url if asset.url else '<inline sass>') return error + def get_rtlcss_error(self, stderr, source=None): + """Improve and remove sensitive information from sass/less compilator error messages""" + error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '') + error += "This error occured while compiling the bundle '%s' containing:" % self.name + return error class WebAsset(object): html_url_format = '%s' @@ -607,7 +681,12 @@ class StylesheetAsset(WebAsset): def __init__(self, *args, **kw): self.media = kw.pop('media', None) + self.direction = kw.pop('direction', None) super(StylesheetAsset, self).__init__(*args, **kw) + if self.direction == 'rtl' and self.url: + self.html_url_args = self.url.rsplit('.', 1) + self.html_url_format = '%%s/%s/%s.%%s' % ('rtl', self.bundle.name) + self.html_url_args = tuple(self.html_url_args) @property def content(self): @@ -642,6 +721,10 @@ class StylesheetAsset(WebAsset): self.bundle.css_errors.append(str(e)) return '' + def get_source(self): + content = self.inline or self._fetch_content() + return "/*! %s */\n%s" % (self.id, content) + def minify(self): # remove existing sourcemaps, make no sense after re-mini content = self.rx_sourceMap.sub('', self.content) @@ -674,12 +757,8 @@ class PreprocessedCSS(StylesheetAsset): def __init__(self, *args, **kw): super(PreprocessedCSS, self).__init__(*args, **kw) - self.html_url_format = '%%s/%s/%%s.css' % self.bundle.name self.html_url_args = tuple(self.url.rsplit('/', 1)) - - def get_source(self): - content = self.inline or self._fetch_content() - return "/*! %s */\n%s" % (self.id, content) + self.html_url_format = '%%s/%s%s/%%s.css' % ('rtl/' if self.direction == 'rtl' else '', self.bundle.name) def get_command(self): raise NotImplementedError diff --git a/odoo/addons/test_assetsbundle/static/src/css/test_cssfile3.css b/odoo/addons/test_assetsbundle/static/src/css/test_cssfile3.css new file mode 100644 index 00000000000..4a1908dfa7d --- /dev/null +++ b/odoo/addons/test_assetsbundle/static/src/css/test_cssfile3.css @@ -0,0 +1,3 @@ +.rule4 { + color: green; +} diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py index 3f4e75b7b69..a2b03940e2b 100644 --- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py +++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py @@ -34,18 +34,20 @@ class TestJavascriptAssetsBundle(TransactionCase): super(TestJavascriptAssetsBundle, self).setUp() self.jsbundle_xmlid = 'test_assetsbundle.bundle1' self.cssbundle_xmlid = 'test_assetsbundle.bundle2' + self.env['res.lang'].load_lang('ar_SY') def _get_asset(self, xmlid, env=None): env = (env or self.env) files, remains = env['ir.qweb']._get_asset_content(xmlid, env.context) return AssetsBundle(xmlid, files, env=env) - def _any_ira_for_bundle(self, type): + def _any_ira_for_bundle(self, type, lang=None): """ Returns all ir.attachments associated to a bundle, regardless of the verion. """ + user_direction = self.env['res.lang'].search([('code', '=', (lang or self.env.user.lang))]).direction bundle = self.jsbundle_xmlid if type == 'js' else self.cssbundle_xmlid return self.env['ir.attachment'].search([ - ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(bundle, type)) + ('url', '=like', '/web/content/%-%/{0}{1}%.{2}'.format(('rtl/' if type == 'css' and user_direction == 'rtl' else ''), bundle, type)) ]) def test_01_generation(self): @@ -312,7 +314,207 @@ class TestJavascriptAssetsBundle(TransactionCase): content = bundle0.to_html() self.assertEqual(content.count('test_assetsbundle.bundle2.0.css'), 1) - def test_15_exteral_lib_assets(self): + # Language direction specific tests + + def test_15_rtl_css_generation(self): + """ Checks that a bundle creates an ir.attachment record when its `css` method is called + for the first time for language with different direction and separate bundle is created for rtl direction. + """ + self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + + # there shouldn't be any attachment associated to this bundle + self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 0) + self.assertEquals(len(self.bundle.get_attachments('css')), 0) + + # trigger the first generation and, thus, the first save in database + self.bundle.css() + + # there should be one attachment associated to this bundle + self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1) + self.assertEquals(len(self.bundle.get_attachments('css')), 1) + + def test_16_ltr_and_rtl_css_access(self): + """ Checks that the bundle's cache is working, i.e. that the bundle creates only one + ir.attachment record when rendered multiple times for rtl direction also check we have two css bundles, + one for ltr and one for rtl. + """ + # Assets access for en_US language + ltr_bundle0 = self._get_asset(self.cssbundle_xmlid) + ltr_bundle0.css() + + self.assertEquals(len(self._any_ira_for_bundle('css')), 1) + + ltr_version0 = ltr_bundle0.version + ltr_ira0 = self._any_ira_for_bundle('css') + ltr_date0 = ltr_ira0.create_date + + ltr_bundle1 = self._get_asset(self.cssbundle_xmlid) + ltr_bundle1.css() + + self.assertEquals(len(self._any_ira_for_bundle('css')), 1) + + ltr_version1 = ltr_bundle1.version + ltr_ira1 = self._any_ira_for_bundle('css') + ltr_date1 = ltr_ira1.create_date + + self.assertEquals(ltr_version0, ltr_version1) + self.assertEquals(ltr_date0, ltr_date1) + + # Assets access for ar_SY language + rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + rtl_bundle0.css() + + self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1) + + rtl_version0 = rtl_bundle0.version + rtl_ira0 = self._any_ira_for_bundle('css', lang='ar_SY') + rtl_date0 = rtl_ira0.create_date + + rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + rtl_bundle1.css() + + self.assertEquals(len(self._any_ira_for_bundle('css', lang='ar_SY')), 1) + + rtl_version1 = rtl_bundle1.version + rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY') + rtl_date1 = rtl_ira1.create_date + + self.assertEquals(rtl_version0, rtl_version1) + self.assertEquals(rtl_date0, rtl_date1) + + # Checks rtl and ltr bundles are different + self.assertNotEquals(ltr_ira1.id, rtl_ira1.id) + + # Check two bundles are available, one for ltr and one for rtl + css_bundles = self.env['ir.attachment'].search([ + ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css')) + ]) + self.assertEquals(len(css_bundles), 2) + + def test_17_css_bundle_date_invalidation(self): + """ Checks that both css bundles are invalidated when one of its assets' modification date is changed + """ + # Assets access for en_US language + ltr_bundle0 = self._get_asset(self.cssbundle_xmlid) + ltr_bundle0.css() + ltr_last_modified0 = ltr_bundle0.last_modified + ltr_version0 = ltr_bundle0.version + + # Assets access for ar_SY language + rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + rtl_bundle0.css() + rtl_last_modified0 = rtl_bundle0.last_modified + rtl_version0 = rtl_bundle0.version + + # Touch test_cssfile1.css + # Note: No lang specific context given while calling _get_asset so it will load assets for en_US + path = get_resource_path('test_assetsbundle', 'static', 'src', 'css', 'test_cssfile1.css') + ltr_bundle1 = self._get_asset(self.cssbundle_xmlid) + _touch(path, ltr_bundle1) + + ltr_bundle1.css() + ltr_last_modified1 = ltr_bundle1.last_modified + ltr_version1 = ltr_bundle1.version + ltr_ira1 = self._any_ira_for_bundle('css') + self.assertNotEquals(ltr_last_modified0, ltr_last_modified1) + self.assertNotEquals(ltr_version0, ltr_version1) + + rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + + rtl_bundle1.css() + rtl_last_modified1 = rtl_bundle1.last_modified + rtl_version1 = rtl_bundle1.version + rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY') + self.assertNotEquals(rtl_last_modified0, rtl_last_modified1) + self.assertNotEquals(rtl_version0, rtl_version1) + + # Checks rtl and ltr bundles are different + self.assertNotEquals(ltr_ira1.id, rtl_ira1.id) + + # check if the previous attachment is correctly cleaned + css_bundles = self.env['ir.attachment'].search([ + ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css')) + ]) + self.assertEquals(len(css_bundles), 2) + + def test_18_css_bundle_content_invalidation(self): + """ Checks that a bundle is invalidated when its content is modified by adding a file to + source. + """ + # Assets for en_US + ltr_bundle0 = self._get_asset(self.cssbundle_xmlid) + ltr_bundle0.css() + ltr_files0 = ltr_bundle0.files + ltr_version0 = ltr_bundle0.version + + rtl_bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + rtl_bundle0.css() + rtl_files0 = rtl_bundle0.files + rtl_version0 = rtl_bundle0.version + + css_bundles = self.env['ir.attachment'].search([ + ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css')) + ]) + self.assertEquals(len(css_bundles), 2) + + view_arch = """ + <data> + <xpath expr="." position="inside"> + <script type="text/css" src="/test_assetsbundle/static/src/css/test_cssfile3.css"/> + </xpath> + </data> + """ + bundle = self.browse_ref(self.cssbundle_xmlid) + view = self.env['ir.ui.view'].create({ + 'name': 'test bundle inheritance', + 'type': 'qweb', + 'arch': view_arch, + 'inherit_id': bundle.id, + }) + + ltr_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids})) + ltr_bundle1.css() + ltr_files1 = ltr_bundle1.files + ltr_version1 = ltr_bundle1.version + ltr_ira1 = self._any_ira_for_bundle('css') + + self.assertNotEquals(ltr_files0, ltr_files1) + self.assertNotEquals(ltr_version0, ltr_version1) + + rtl_bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': view.ids, 'lang': 'ar_SY'})) + rtl_bundle1.css() + rtl_files1 = rtl_bundle1.files + rtl_version1 = rtl_bundle1.version + rtl_ira1 = self._any_ira_for_bundle('css', lang='ar_SY') + + self.assertNotEquals(rtl_files0, rtl_files1) + self.assertNotEquals(rtl_version0, rtl_version1) + + # Checks rtl and ltr bundles are different + self.assertNotEquals(ltr_ira1.id, rtl_ira1.id) + + # check if the previous attachment are correctly cleaned + css_bundles = self.env['ir.attachment'].search([ + ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.cssbundle_xmlid, 'css')) + ]) + self.assertEquals(len(css_bundles), 2) + + def test_19_css_in_debug_assets(self): + """ Checks that a bundle rendered in debug mode(assets) with right to left language direction stores css files in assets bundle. + """ + debug_bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'lang': 'ar_SY'})) + content = debug_bundle.to_html(debug='assets') + + # css file should be available in assets bundle as user's lang direction is rtl + self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1/rtl/{0}.css'.format(self.cssbundle_xmlid), content) + + # there should be assets(css) created in debug mode as user's lang direction is rtl + css_bundle = self.env['ir.attachment'].search([ + ('url', '=', '/test_assetsbundle/static/src/css/test_cssfile1/rtl/{0}.css'.format(self.cssbundle_xmlid)) + ]) + self.assertEquals(len(css_bundle), 1) + + def test_20_exteral_lib_assets(self): html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2') attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')]) self.assertEquals(len(attachments), 2) @@ -331,7 +533,7 @@ class TestJavascriptAssetsBundle(TransactionCase): </body> </html>""" % {"js": attachments[0].url, "css": attachments[1].url}).encode('utf8')) - def test_16_exteral_lib_assets_debug_mode(self): + def test_21_exteral_lib_assets_debug_mode(self): html = self.env['ir.ui.view'].render_template('test_assetsbundle.template2', {"debug": "assets"}) attachments = self.env['ir.attachment'].search([('url', '=like', '/web/content/%-%/test_assetsbundle.bundle4.%')]) self.assertEquals(len(attachments), 0) @@ -354,7 +556,6 @@ class TestJavascriptAssetsBundle(TransactionCase): </html>""").encode('utf8')) - class TestAssetsBundleInBrowser(HttpCase): def test_01_js_interpretation(self): """ Checks that the javascript of a bundle is correctly interpreted. |
