summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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.