summaryrefslogtreecommitdiff
path: root/odoo/addons/base/models/assetsbundle.py
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 /odoo/addons/base/models/assetsbundle.py
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
Diffstat (limited to 'odoo/addons/base/models/assetsbundle.py')
-rw-r--r--odoo/addons/base/models/assetsbundle.py185
1 files changed, 132 insertions, 53 deletions
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