summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2018-08-10 15:37:17 +0200
committerMartin Trigaux <mat@odoo.com>2018-08-10 16:32:50 +0200
commitf7130556bbcbc0a0b93ba07ef1da9c9d11b68b54 (patch)
tree33a455e6ad8219595b8fcb786e88d44cc1d60e75
parentb6f7ad3adb23fff1aa888d8315aca8157b555b03 (diff)
[REF] *: add support for RTL languages
מזל טוב The direction parameter will now have a real impact on the web interface and completly switch the direction of the interface. Add the dependency rtlcss to switch the direction with a css postprocessor. Two different assets will be generated for ltr or rtl /web/content/<...>/web.assets_backend.0.css and /web/content/<...>/rtl/web.assets_backend.0.css to be able to differentiate it easily Adapt the css and views to be easier to switch direction Render graph as it is, do not move y-axis right side, as per mathematical rule value goes left from origin are considered negative, so keep graph as it is for right to left language add test cases for language direction specific assets bundle if language direction is rtl then we also store css files in assets bundle now we store two assets bundle, one for ltr and one for rtl language direction test two assets bundles generated, also test two assets bundle generated for en_US and ar_SY
-rw-r--r--addons/account/static/src/scss/account_reconciliation.scss16
-rw-r--r--addons/delivery/views/delivery_view.xml4
-rw-r--r--addons/im_livechat/static/src/js/im_livechat.js4
-rw-r--r--addons/mail/static/src/js/services/mail_window_manager.js5
-rw-r--r--addons/mail/static/src/js/thread_windows/abstract_thread_window.js3
-rw-r--r--addons/mail/static/src/scss/abstract_thread_window.scss1
-rw-r--r--addons/mail/static/src/scss/systray.scss1
-rw-r--r--addons/point_of_sale/static/src/css/pos.css2
-rw-r--r--addons/point_of_sale/views/point_of_sale.xml4
-rw-r--r--addons/portal/views/portal_templates.xml3
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js1
-rw-r--r--addons/web/static/src/js/core/dom.js10
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js1
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js5
-rw-r--r--addons/web/static/src/scss/datepicker.scss12
-rw-r--r--addons/web/static/src/scss/dropdown.scss6
-rw-r--r--addons/web/static/src/scss/fontawesome_overriden.scss36
-rw-r--r--addons/web/static/src/scss/graph_view.scss1
-rw-r--r--addons/web/static/src/scss/report.scss1
-rw-r--r--addons/web/static/src/scss/search_view.scss1
-rw-r--r--addons/web/static/src/scss/ui.scss16
-rw-r--r--addons/web/static/src/scss/webclient.scss1
-rw-r--r--addons/web/static/src/scss/webclient_extra.scss6
-rw-r--r--addons/web/views/webclient_templates.xml4
-rw-r--r--addons/web_editor/views/iframe.xml3
-rw-r--r--addons/web_tour/static/src/js/tip.js19
-rw-r--r--addons/web_tour/static/src/scss/tip.scss15
-rw-r--r--addons/website/static/src/scss/website.backend.scss1
-rw-r--r--addons/website/static/src/scss/website.ui.scss1
-rw-r--r--addons/website_forum/views/website_forum.xml6
-rw-r--r--odoo/addons/base/data/res.lang.csv6
-rw-r--r--odoo/addons/base/models/assetsbundle.py185
-rw-r--r--odoo/addons/test_assetsbundle/static/src/css/test_cssfile3.css3
-rw-r--r--odoo/addons/test_assetsbundle/tests/test_assetsbundle.py211
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 &amp;&amp; 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 &amp;&amp; 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.